Skip to content

Commit 45e9037

Browse files
committed
new: [pdns-import-cof] New importer for the Passive DNS backend.
- Importer for COF NDJSON file or fifo stream - Importer from websocket using COF NDJSON format per message `python3 pdns-import-cof.py --websocket ws://crh.circl.lu:8888`
1 parent ecd0562 commit 45e9037

File tree

2 files changed

+142
-0
lines changed

2 files changed

+142
-0
lines changed

bin/pdns-import-cof.py

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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()

requirements

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
redis
22
iptools
33
tornado
4+
ndjson
5+
websocket-client

0 commit comments

Comments
 (0)