Heading towards report sending
This commit is contained in:
parent
94d8748fd8
commit
d229e1b2bf
144
analyze.py
144
analyze.py
|
@ -1,6 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from email import message_from_bytes
|
||||
from email.header import decode_header
|
||||
|
@ -17,14 +18,29 @@ from zipfile import ZipFile, is_zipfile
|
|||
from yaml import CLoader as Loader
|
||||
from yaml import load
|
||||
|
||||
_INSERTED_FIELDS = (
|
||||
'checked_at, org_name, domain, report_id, unixtime_start, unixtime_end, '
|
||||
'offending_ip, count, failed_types')
|
||||
_FETCHED_FIELDS = f'rowid, {_INSERTED_FIELDS}'
|
||||
LogRow = namedtuple(typename='LogRow', field_names=_FETCHED_FIELDS)
|
||||
_CFG_TEMPLATE = """\
|
||||
sqlite_path: dmarc-analyzer.db
|
||||
# Imap account where the aggregate reports are stored in
|
||||
imap:
|
||||
ssl_host: mail.example.com
|
||||
host: mail.example.com
|
||||
username: imap-username
|
||||
password: my-super-secret-password
|
||||
folder_path: INBOX
|
||||
# Host settings for sending email reports
|
||||
smtp:
|
||||
host: mail.example.com
|
||||
port: 587
|
||||
login: sender-login
|
||||
password: password
|
||||
"""
|
||||
_DESCRIPTION = (
|
||||
'DMARC Report analyzer and reporter. '
|
||||
'The default mode is to analyze what\'s in the IMAP folder.')
|
||||
_STARTTIME = time()
|
||||
|
||||
|
||||
|
@ -40,6 +56,43 @@ class NotAReport(DmarcReporterBase):
|
|||
'Raised when the passed email is not a DMARC report.'
|
||||
|
||||
|
||||
def _get_sql_connection(config: dict) -> Connection:
|
||||
'Return an sqlite `Connection` object from the settings.'
|
||||
return connect(database=Path(config['sqlite_path']).absolute())
|
||||
|
||||
|
||||
def _get_loaded_cfg(parsed_args: Namespace) -> dict:
|
||||
with Path(parsed_args.cfg_file).absolute().open('r') as fd:
|
||||
data = load(stream=fd, Loader=Loader)
|
||||
return data
|
||||
|
||||
|
||||
def _init_cfg(parsed_args: Namespace):
|
||||
'Initialize a config file template.'
|
||||
path_cfg = Path(parsed_args.cfg_file).absolute()
|
||||
with path_cfg.open('w') as fd:
|
||||
fd.write(_CFG_TEMPLATE)
|
||||
print((
|
||||
f'Config file template written to {path_cfg}. Please edit it '
|
||||
'before running this program.'))
|
||||
|
||||
|
||||
def _init_db(parsed_args: Namespace, config: dict):
|
||||
'Initialize a blank DB.'
|
||||
conn = connect(database=Path(config['sqlite_path']).absolute())
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DROP TABLE IF EXISTS reports')
|
||||
cursor.execute(
|
||||
'CREATE TABLE reports (checked_at INTEGER, org_name TEXT, '
|
||||
'domain TEXT, report_id TEXT, unixtime_start INTEGER, '
|
||||
'unixtime_end INTEGER, offending_ip TEXT, count INTEGER, '
|
||||
'failed_types TEXT)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(
|
||||
'Database initialized. You can now run the program without --init-db')
|
||||
|
||||
|
||||
class XmlParser(object):
|
||||
'Parse one report here.'
|
||||
|
||||
|
@ -52,10 +105,8 @@ class XmlParser(object):
|
|||
def _parse_header(self):
|
||||
'Parse data headers.'
|
||||
date_range = self._root.find(path='./report_metadata/date_range')
|
||||
self._datetime_start = datetime.utcfromtimestamp(int(
|
||||
date_range.find(path='begin').text))
|
||||
self._datetime_end = datetime.utcfromtimestamp(int(
|
||||
date_range.find(path='end').text))
|
||||
self._unixtime_start = int(date_range.find(path='begin').text)
|
||||
self._unixtime_end = int(date_range.find(path='end').text)
|
||||
self._org_name = self._root.find(
|
||||
path='./report_metadata/org_name').text
|
||||
self._domain = self._root.find(path='./policy_published/domain').text
|
||||
|
@ -65,11 +116,10 @@ class XmlParser(object):
|
|||
def _note_failed_records(self, ip: str, failed: list, count: int):
|
||||
'Add the failed records to the sqlite DB.'
|
||||
self._cursor.execute(
|
||||
'INSERT INTO reports(checked_at, org_name, domain, report_id, '
|
||||
'datetime_start, datetime_end, offending_ip, count, failed_types) '
|
||||
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', (
|
||||
f'INSERT INTO reports({_INSERTED_FIELDS}) VALUES (?, ?, ?, ?, ?, '
|
||||
'?, ?, ?, ?)', (
|
||||
_STARTTIME, self._org_name, self._domain, self._report_id,
|
||||
self._datetime_start, self._datetime_end, ip, count,
|
||||
self._unixtime_start, self._unixtime_end, ip, count,
|
||||
', '.join(failed)))
|
||||
self._sql_conn.commit()
|
||||
|
||||
|
@ -88,12 +138,14 @@ class XmlParser(object):
|
|||
for record in self._root.findall(path='record'):
|
||||
self._parse_record(record=record)
|
||||
|
||||
def start(self):
|
||||
'Start parsing.'
|
||||
def process(self):
|
||||
'Start processing.'
|
||||
self._root = XML(text=self._content)
|
||||
self._parse_header()
|
||||
print('\n', self._datetime_start.strftime(format='%c'))
|
||||
print(self._datetime_end.strftime(format='%c'))
|
||||
|
||||
print('\n', datetime.fromtimestamp(
|
||||
self._unixtime_start).strftime(format='%c'))
|
||||
print(datetime.fromtimestamp(self._unixtime_end).strftime(format='%c'))
|
||||
print(self._org_name, self._domain, self._report_id)
|
||||
self._parse_records()
|
||||
|
||||
|
@ -104,8 +156,7 @@ class ImapHandler(object):
|
|||
|
||||
def __init__(self, config: dict):
|
||||
self._config = config
|
||||
self._sql_conn = connect(
|
||||
database=Path(config['sqlite_path']).absolute())
|
||||
self._sql_conn = _get_sql_connection(config=config)
|
||||
|
||||
def _get_subject(self, email: Message) -> str:
|
||||
'Extract and return the subject.'
|
||||
|
@ -183,50 +234,43 @@ class ImapHandler(object):
|
|||
parser = XmlParser(
|
||||
content=content_item, config=self._config,
|
||||
sql_conn=self._sql_conn)
|
||||
parser.start()
|
||||
parser.process()
|
||||
|
||||
def start(self):
|
||||
def process(self):
|
||||
'Start processing.'
|
||||
with IMAP4_SSL(host=self._config['imap']['ssl_host']) as self._conn:
|
||||
with IMAP4_SSL(host=self._config['imap']['host']) as self._conn:
|
||||
self._login_and_run()
|
||||
self._sql_conn.close()
|
||||
|
||||
|
||||
def _get_loaded_cfg(parsed_args: Namespace):
|
||||
with Path(parsed_args.cfg_file).absolute().open('r') as fd:
|
||||
data = load(stream=fd, Loader=Loader)
|
||||
return data
|
||||
class ReportSender(object):
|
||||
'Sending reports per email.'
|
||||
|
||||
def __init__(self, config: dict):
|
||||
self._config = config
|
||||
self._sql_conn = _get_sql_connection(config=config)
|
||||
self._cursor = self._sql_conn.cursor()
|
||||
|
||||
def _init_cfg(parsed_args: Namespace):
|
||||
'Initialize a config file template.'
|
||||
path_cfg = Path(parsed_args.cfg_file).absolute()
|
||||
with path_cfg.open('w') as fd:
|
||||
fd.write(_CFG_TEMPLATE)
|
||||
print((
|
||||
f'Config file template written to {path_cfg}. Please edit it '
|
||||
'before running this program.'))
|
||||
def _read_records(self):
|
||||
'Read records from the DB.'
|
||||
self._cursor.execute(f'SELECT {_FETCHED_FIELDS} FROM reports')
|
||||
rows = list()
|
||||
while True:
|
||||
row = self._cursor.fetchone()
|
||||
if row is None:
|
||||
break
|
||||
rows.append(LogRow(*row))
|
||||
return rows
|
||||
|
||||
|
||||
def _init_db(parsed_args: Namespace, config: dict):
|
||||
'Initialize a blank DB.'
|
||||
conn = connect(database=Path(config['sqlite_path']).absolute())
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DROP TABLE IF EXISTS reports')
|
||||
cursor.execute(
|
||||
'CREATE TABLE reports (checked_at INTEGER, org_name TEXT, '
|
||||
'domain TEXT, report_id TEXT, datetime_start INTEGER, '
|
||||
'datetime_end INTEGER, offending_ip TEXT, count INTEGER, '
|
||||
'failed_types TEXT)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
print(
|
||||
'Database initialized. You can now run the program without --init-db')
|
||||
def process(self):
|
||||
'Start processing.'
|
||||
rows = self._read_records()
|
||||
# self._filter_and_sort_rows()
|
||||
|
||||
|
||||
def main():
|
||||
'Startup of program.'
|
||||
parser = ArgumentParser(description='DMARC Report analyzer')
|
||||
parser = ArgumentParser(description=_DESCRIPTION)
|
||||
parser.add_argument(
|
||||
'-c', '--config', required=True, help='Configuration file path',
|
||||
dest='cfg_file')
|
||||
|
@ -236,14 +280,20 @@ def main():
|
|||
parser.add_argument(
|
||||
'--recreate', action='store_true', required=False,
|
||||
help='Recreate the config file')
|
||||
parser.add_argument(
|
||||
'-r', '--report', action='store_true', required=False,
|
||||
help='Report from the collected logs')
|
||||
parsed_args = parser.parse_args()
|
||||
if parsed_args.recreate:
|
||||
return _init_cfg(parsed_args=parsed_args)
|
||||
config = _get_loaded_cfg(parsed_args=parsed_args)
|
||||
if parsed_args.init_db:
|
||||
return _init_db(parsed_args=parsed_args, config=config)
|
||||
if parsed_args.report:
|
||||
report_sender = ReportSender(config=config)
|
||||
return report_sender.process()
|
||||
handler = ImapHandler(config=config)
|
||||
handler.start()
|
||||
handler.process()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
Loading…
Reference in New Issue