|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# |
| 3 | +# pdns-import is a simple import from Passive DNS cof format (from NDJSON) |
| 4 | +# and import these back into a Passive DNS backend |
| 5 | +# |
| 6 | +# This software is part of the D4 project. |
| 7 | +# |
| 8 | +# The software is released under the GNU Affero General Public version 3. |
| 9 | +# |
| 10 | +# Copyright (c) 2019-2022 Alexandre Dulaunoy - [email protected] |
| 11 | +# Copyright (c) 2019 Computer Incident Response Center Luxembourg (CIRCL) |
| 12 | + |
| 13 | + |
| 14 | +import redis |
| 15 | +import json |
| 16 | +import logging |
| 17 | +import sys |
| 18 | +import argparse |
| 19 | +import os |
| 20 | +import ndjson |
| 21 | + |
| 22 | +# ! websocket-client not websocket |
| 23 | +import websocket |
| 24 | + |
| 25 | +parser = argparse.ArgumentParser( |
| 26 | + description='Import array of standard Passive DNS cof format into your Passive DNS server' |
| 27 | +) |
| 28 | +parser.add_argument('--file', dest='filetoimport', help='JSON file to import') |
| 29 | +parser.add_argument( |
| 30 | + '--websocket', dest='websocket', help='Import from a websocket stream' |
| 31 | +) |
| 32 | +args = parser.parse_args() |
| 33 | + |
| 34 | + |
| 35 | +logger = logging.getLogger('pdns ingestor') |
| 36 | +ch = logging.StreamHandler() |
| 37 | +logger.setLevel(logging.DEBUG) |
| 38 | +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
| 39 | +ch.setFormatter(formatter) |
| 40 | +logger.addHandler(ch) |
| 41 | + |
| 42 | +logger.info("Starting COF ingestor") |
| 43 | + |
| 44 | +analyzer_redis_host = os.getenv('D4_ANALYZER_REDIS_HOST', '127.0.0.1') |
| 45 | +analyzer_redis_port = int(os.getenv('D4_ANALYZER_REDIS_PORT', 6400)) |
| 46 | + |
| 47 | +r = redis.Redis(host='127.0.0.1', port=6400) |
| 48 | + |
| 49 | +excludesubstrings = ['spamhaus.org', 'asn.cymru.com'] |
| 50 | +with open('../etc/records-type.json') as rtypefile: |
| 51 | + rtype = json.load(rtypefile) |
| 52 | + |
| 53 | +dnstype = {} |
| 54 | + |
| 55 | +stats = True |
| 56 | + |
| 57 | +for v in rtype: |
| 58 | + dnstype[(v['type'])] = v['value'] |
| 59 | + |
| 60 | +expiration = None |
| 61 | +if (not (args.filetoimport)) and (not (args.websocket)): |
| 62 | + parser.print_help() |
| 63 | + sys.exit(0) |
| 64 | + |
| 65 | + |
| 66 | +def add_record(rdns=None): |
| 67 | + if rdns is None: |
| 68 | + return False |
| 69 | + logger.debug("parsed record: {}".format(rdns)) |
| 70 | + if 'rrname' not in rdns: |
| 71 | + logger.debug( |
| 72 | + 'Parsing of passive DNS line is incomplete: {}'.format(rdns.strip()) |
| 73 | + ) |
| 74 | + return False |
| 75 | + if rdns['rrname'] and rdns['rrtype']: |
| 76 | + rdns['type'] = dnstype[rdns['rrtype']] |
| 77 | + rdns['v'] = rdns['rdata'] |
| 78 | + excludeflag = False |
| 79 | + for exclude in excludesubstrings: |
| 80 | + if exclude in rdns['rrname']: |
| 81 | + excludeflag = True |
| 82 | + if excludeflag: |
| 83 | + logger.debug('Excluded {}'.format(rdns['rrname'])) |
| 84 | + return False |
| 85 | + if rdns['type'] == '16': |
| 86 | + rdns['v'] = rdns['v'].replace("\"", "", 1) |
| 87 | + query = "r:{}:{}".format(rdns['rrname'], rdns['type']) |
| 88 | + logger.debug('redis sadd: {} -> {}'.format(query, rdns['v'])) |
| 89 | + r.sadd(query, rdns['v']) |
| 90 | + res = "v:{}:{}".format(rdns['v'], rdns['type']) |
| 91 | + logger.debug('redis sadd: {} -> {}'.format(res, rdns['rrname'])) |
| 92 | + r.sadd(res, rdns['rrname']) |
| 93 | + |
| 94 | + firstseen = "s:{}:{}:{}".format(rdns['rrname'], rdns['v'], rdns['type']) |
| 95 | + if not r.exists(firstseen): |
| 96 | + r.set(firstseen, int(float(rdns['time_first']))) |
| 97 | + logger.debug('redis set: {} -> {}'.format(firstseen, rdns['time_first'])) |
| 98 | + |
| 99 | + lastseen = "l:{}:{}:{}".format(rdns['rrname'], rdns['v'], rdns['type']) |
| 100 | + last = r.get(lastseen) |
| 101 | + if last is None or int(float(last)) < int(float(rdns['time_last'])): |
| 102 | + r.set(lastseen, int(float(rdns['time_last']))) |
| 103 | + logger.debug('redis set: {} -> {}'.format(lastseen, rdns['time_last'])) |
| 104 | + |
| 105 | + occ = "o:{}:{}:{}".format(rdns['rrname'], rdns['v'], rdns['type']) |
| 106 | + if 'count' in rdns: |
| 107 | + r.set(occ, rdns['count']) |
| 108 | + else: |
| 109 | + r.incrby(occ, amount=1) |
| 110 | + |
| 111 | + if stats: |
| 112 | + r.incrby('stats:processed', amount=1) |
| 113 | + if not r: |
| 114 | + logger.info('empty passive dns record') |
| 115 | + return False |
| 116 | + |
| 117 | + |
| 118 | +def on_open(ws): |
| 119 | + logger.debug('[websocket] connection open') |
| 120 | + |
| 121 | + |
| 122 | +def on_close(ws): |
| 123 | + logger.debug('[websocket] connection closed') |
| 124 | + |
| 125 | + |
| 126 | +def on_message(ws, message): |
| 127 | + logger.debug('Message received via websocket') |
| 128 | + add_record(rdns=json.loads(message)) |
| 129 | + |
| 130 | + |
| 131 | +if args.filetoimport: |
| 132 | + with open(args.filetoimport, "r") as dnsimport: |
| 133 | + reader = ndjson.load(dnsimport) |
| 134 | + for rdns in reader: |
| 135 | + add_record(rdns=rdns) |
| 136 | +elif args.websocket: |
| 137 | + ws = websocket.WebSocketApp( |
| 138 | + args.websocket, on_open=on_open, on_close=on_close, on_message=on_message |
| 139 | + ) |
| 140 | + ws.run_forever() |
0 commit comments