├── .gitignore
├── README.md
├── tool1
├── html-output.py
└── parse.py
├── tool2
├── counters.py
├── dmarc-EmailTest-client.py
├── dmarc-EmailTest-server.py
├── dns-record-tracker.py
├── domain-status.py
├── graph.py
├── sendTestMail.py
├── spf-ip-extract.py
└── trusted-list.txt
└── visualizations
├── asn-mapping.dat
├── bubble-chart-AS.py
├── bubble-chart.py
├── heatMap-in.py
└── heatMap-out.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Description
2 | ======
3 |
4 | A DMARC deployment/monitoring tool written in python.
5 |
6 | Requirements
7 | =======
8 |
9 | * Bootstrap
10 | * Rickshaw JS
11 | * w3data.js. Can be found at: http://www.w3schools.com/lib/w3data.js
12 | * python 2.7 (not tested with python 3)
13 | * dnspython
14 | * pythone MySQLdb
15 | * python netaddr
16 | * For DMARC email test:
17 | * pydkim 3
18 | * pyspf 2.0.12
19 | * python email
20 | * ssl
21 | * Techsneeze's dmarc parser. Can be found at: https://github.com/techsneeze/dmarcts-report-parser/blob/master/dmarcts-report-parser.pl
22 | * For Visualizations:
23 | * Matplotlib
24 | * numpy
25 | * python Tkinter
26 | * pyasn
27 | * urllib
28 | * json
29 | * MySQL database
30 |
31 | Make sure you place the database user credentials in the following files:
32 |
33 | * parse.py
34 | * counters.py
35 | * graph.py
36 | * dns-record-tracker.py
37 | * bubble-chart.py
38 | * heatMap-in.py
39 | * heatMap-out.py
40 |
41 | For use of `dns-record-tracker.py`, insert the following table into the datbase named `dmarc` which is created by the dmarc parser from Techsneeze:
42 |
43 | | Column name | Type |
44 | |---|---|
45 | | dmarc | varchar(255) |
46 | | spf | varchar(255) |
47 | | dkim | varchar(255) |
48 | | dateStamp | timestamp |
49 |
50 |
51 | Tool 1
52 | ============
53 |
54 | This tool is meant to give the domain owner an overview of the possible sources
55 | that can deliver DMARC reports.
56 |
57 | Phase 1 consist of two files:
58 |
59 | * `parse.py`
60 | * `html-output.py`
61 |
62 | Use `parse.py` to generate a CSV file that is accepted by `html-output.py`.
63 | `parse.py` should be supplied with file containing the mail log. An example:
64 |
65 | python parse.py /var/log/mail.log
66 |
67 | This will create a CSV file called `output.csv` which must be supplied to `html-output.py`:
68 |
69 | python html-output.py output.CSV
70 |
71 | After running `html-output.py`, a HTML file called `dm-phase1.html` is created that holds the result.
72 |
73 |
74 |
75 | Tool 2
76 | ===========
77 |
78 | This tool is meant to monitor the domain during the deployment/operation of DMARC.
79 | It provides various tools which include: current DMARC status, DMARC tester, authentication
80 | results and a DNS history tool.
81 |
82 | Phase 2 consist of the following files with their corresponding output:
83 |
84 | * `domain-status.py` -> `domainstatus.html`
85 | * `dmarc-EmailTest-client.py`
86 | * `dmarc-EmailTest-server.py` -> `dmarcCheck.html`
87 | * `sendTestMail.py`
88 | * `counters.py` -> `counterTrust.html`, `counterForeign.html`
89 | * `graph.py` -> `graphTrust.js`, `graphForeign.js`
90 | * `dns-record-tracker.py`
91 | * `spf-ip-extract.py`
92 | * `trusted-list.txt`
93 | * `dm-ph2.html`
94 |
95 | `domain-status.py` checks the presence of several important DMARC parameters and warns the user if any of these are not configured. Additionally the current DMARC record is displayed.
96 |
97 | `dmarc-EmailTest-client.py` is the client application that should have access to a remote mailbox. It polls the mailbox for any new message. If a message is found, it sends the message including the headers to the server over a secure channel. `dmarc-EmailTest-server.py` implements the server. The server listens for a client connection. If the client sends a message, its evaluated on SPF and DKIM allignment. The results are written to `dmarcCheck.html`. For the secure connection, the user needs to create a certificate and a key. OpenSSL can do this:
98 |
99 |
100 | openssl genrsa 2048 > key
101 | openssl req -new -x509 -nodes -sha1 -days 365 -key key > cert
102 |
103 |
104 | `sendTestMail.py` can automatically send an test mail to the reserved mailbox that
105 | is used by `dmarc-EmailTest-client.py`
106 |
107 | `counters.py` generates statistics about authentication results. The results are dived into
108 | two sections: Trusted and Unknown sources. Each section contains an IP list that displays
109 | the IP addresses that fall within this category and the number of messages that they have sent.
110 | Additionally, a set of counters shows the aggregate authentication results. These include SPF, DKIM and DMARC results. This script relies on the trusted host which must be defined in `trusted-list.txt`. This can be done manually or automatically using `spf-ip-extract.py`.
111 |
112 |
113 | `graph.py` generates the graphs which show DMARC authentication results over the last 30 days (by default). Like `counters.py`, this done for both trusted and unknown sources. Additionally it also generates the DNS history time line. This script relies on the trusted host which must be defined in `trusted-list.txt`. This can be done manually or automatically using `spf-ip-extract.py`.
114 |
115 | `spf-ip-extract.py` can automatically extract IP addresses from a SPF record. Network addresses are also supported. All IP addresses are written to `trusted-list.txt`
116 |
117 | `dns-record-tracker.py` tracks the SPF, DKIM and DMARC records of a domain. Any record change is saved
118 | in the MySQL database. These records are used by `graph.py` to generate the DNS history time line.
119 | It advised to run this script frequently when one is changing one of the 3 records (SPF, DKIM, DMARC) frequently (for example during the deployment).
120 |
121 | The individual generated files for each widget are combined into one web interface in `dm-ph2.html`.
122 |
123 |
124 | Visualizations
125 | =============
126 |
127 | The visualizations consist of the following files:
128 |
129 | * bubble-chart-ASN.py
130 | * heatMap-in.py
131 | * heatMap-out.py
132 | * bubble-chart.py
133 |
134 |
135 | `bubble-chart-ASN.py` generates a bubble chart to review where emails come from, in which quantities and the ratio of successful DMARC authentication results. The categorization is based on ASN numbers obtained using `pyasn`. This libary requires a BGP/MRT dump file as input. These dumps can be found at http://archive.routeviews.org/. The ASN <-> IP mapping for IPv4 and IPv6 is found in seppreate files which are not autmatically merged by `pyasn`. An mergeged file of this mapping can be found under `asn-mapping.dat` (version of 12-08-15). Additionally, a text file with the results of each AS is generated. This file is named `asn-mapping.dat`. This file also contains the ip addresses found in each AS togheter with authentication results. The user can optionally call this script with the `--asn-lookup` argument which will lookup the corrosponding (orginizational) name of the AS.
136 |
137 | `heatMap-in.py` generates a heat map that displays the authentication results of different domains based on incoming reports. Each tile is awarded a color based on the ratio of successful authentication results against the total amount of emails. Each tile contains text fields that indicate the total number of emails, volume of emails that passed DMARC and volume of email that failed DMARC.
138 |
139 | `heatMap-out.py` has the same functionality as `heatMap-in.py` but than for outgoing reports. Based on OpenDmarc's import functionality.
140 |
141 | `bubble-chart.py` similar to `bubble-chart-ASN.py` but works on IP chunks rather than ASs.
142 |
--------------------------------------------------------------------------------
/tool1/html-output.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 16-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that parses a csv file created with #
7 | # parse.py. The contents of the csv file are #
8 | # converted to an webpage. #
9 | # #
10 | # To be set by user: #
11 | # #
12 | # None #
13 | # #
14 | #################################################
15 |
16 | import sys
17 | import csv
18 |
19 |
20 |
21 | def init():
22 |
23 | filename = sys.argv[1]
24 | return filename
25 |
26 | #Function that parses the input csv file and
27 | #generates a HTML page.
28 | def parse(input_csv):
29 |
30 | totalCount = 0
31 | domainCount = 0
32 | dmarcPubCount = 0
33 | dmarcNoPubCount = 0
34 | domain_list = []
35 |
36 | f = open(input_csv, 'r')
37 |
38 |
39 |
40 | #Read input csv line by line
41 | try:
42 | reader = csv.reader(f)
43 | for row in reader:
44 | print row
45 | domain_list.append(row)
46 | totalCount += int(row[1])
47 | if row[2] != "None":
48 | dmarcPubCount += 1
49 | domainCount = len(domain_list)
50 | dmarcNoPubCount = domainCount - dmarcPubCount
51 |
52 | finally:
53 | f.close()
54 |
55 | #The corrsonding HTML page is written to dm-ph1.html
56 | output = open('dm-ph1.html', 'w')
57 |
58 |
59 | header = """
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 | DMARC Pre-check
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
"""
83 | emailCountHtml = """
84 |
85 |
""" + str(totalCount) + """
86 |
Emails
87 |
88 |
"""
89 | domainCountHtml = """
90 |
91 |
""" + str(domainCount) + """
92 |
Domains
93 |
94 |
"""
95 | dmarcPubHtml = """
96 |
97 |
""" + str(dmarcPubCount) +"""
98 |
DMARC record published
99 |
100 |
"""
101 | dmarcNoPubHtml = """
102 |
103 |
""" + str(dmarcNoPubCount) +"""
104 |
No DMARC record published
105 |
106 |
"""
107 | tailCountersHtml = """
108 |
109 |
110 |
111 | """
112 | startTableHtml = """
113 |
114 |
Domains which have published a DMARC record may provide you with reports on you mail systems's operation.
115 |
116 |
117 |
118 |
119 | Domain
120 | Email Volume
121 | DMARC record
122 |
123 |
124 | """
125 | pageEndHtml = """
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 | """
137 |
138 |
139 | #Write all parts of webpage to the HTML file
140 | output.write(header)
141 | output.write(emailCountHtml)
142 | output.write(domainCountHtml)
143 | output.write(dmarcPubHtml)
144 | output.write(dmarcNoPubHtml)
145 | output.write(tailCountersHtml)
146 | output.write(startTableHtml)
147 |
148 | #Genergate the table overview
149 | for domain in domain_list:
150 | output.write("""
151 | """ + str(domain[0]) + """
152 | """ + str(domain[1]) + """
153 | """ + str(domain[2]) + """
154 | """)
155 |
156 |
157 | output.write(pageEndHtml)
158 | output.close()
159 |
160 |
161 | parse(init())
162 |
--------------------------------------------------------------------------------
/tool1/parse.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 16-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that parses emails log and outputs #
7 | # a csv containing all outgoing #
8 | # email domains and their DMARC records #
9 | # #
10 | # To be set by user: #
11 | # #
12 | # domain variable #
13 | # #
14 | #################################################
15 |
16 |
17 |
18 | import sys
19 | import re
20 | from collections import Counter
21 | import dns.resolver
22 | import csv
23 |
24 | #Setup your domain here:
25 | domain = "dmarc-research.nl"
26 |
27 |
28 |
29 | #Function that creates a filehandle for the mail log
30 | def init():
31 |
32 | filename = sys.argv[1]
33 | try:
34 | mail_log = open(filename, 'r')
35 | except:
36 | print "File error"
37 | sys.exit()
38 |
39 | return mail_log
40 |
41 | #Funtion that parses the email log
42 | def parse(input_file, domain):
43 |
44 | tokens = input_file.readlines();
45 | pattern = re.compile(r'@.*?>')
46 | domains = []
47 |
48 |
49 | #Read mail log line by line
50 | for token in tokens:
51 | token = token.lower()
52 | fields = token.split(" ")
53 | result = re.findall(pattern, token)
54 | #We dont want emails that are sent to the domain, only outgoing emails.
55 | #Ugly but works for now. We need a propper domain selection tool
56 | if result and ("@" + domain +">," not in result):
57 | print result
58 | domains.append(result[0].strip('@').strip('>,'))
59 |
60 | counts = Counter(domains)
61 | return counts
62 |
63 |
64 | #Function to retrieve a DMARC record
65 | def dmarc_lookup(domain):
66 | print domain
67 |
68 | answer=""
69 |
70 | try:
71 | answer = dns.resolver.query("_dmarc." + domain + "." , "TXT")
72 | for data in answer:
73 | answer = data
74 | except:
75 | answer = "None"
76 |
77 | return answer
78 |
79 |
80 | #Function for writing the results to output.csv
81 | def write_csv(values):
82 |
83 | f = open('output.csv', 'wt')
84 |
85 | try:
86 | writer = csv.writer(f)
87 | for domain in values:
88 | dmarc = dmarc_lookup(domain) #Lookup DMARC record of domain
89 | writer.writerow( (domain, values[domain], dmarc))
90 |
91 | finally:
92 | f.close()
93 |
94 |
95 | domain_stats = parse(init(), domain)
96 | write_csv(domain_stats)
97 |
--------------------------------------------------------------------------------
/tool2/counters.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 16-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that generates statistics about #
7 | # authentication results. This includes #
8 | # an IP list and counters of SPF, DKIM, #
9 | # and DMARC authentication results. #
10 | # The statics are generated for both #
11 | # trusted and unknown sources. #
12 | # #
13 | # Database variables to be set by user: #
14 | # #
15 | # - dbAddress #
16 | # - dbName #
17 | # - dbUserName #
18 | # - dbPassword #
19 | # #
20 | # #
21 | #################################################
22 |
23 | import sys
24 | import MySQLdb
25 | import socket
26 | import struct
27 | from collections import Counter
28 |
29 |
30 | dbAddress = "127.0.0.1"
31 | dbName = "dmarc"
32 | dbUserName = "dmarc"
33 | dbPassword = "dmarcrp2"
34 |
35 |
36 | def calculateRatio(x1, x2):
37 |
38 | result = ""
39 |
40 | try:
41 | result = str(round((float(x1)/x2) * 100))
42 | except:
43 | result = "0"
44 |
45 | return result
46 |
47 |
48 | #Function that connects to the SQL database.
49 | def connectToDB(dbAddress, dbUserName, dbPassword, dbName):
50 |
51 | try:
52 | db = MySQLdb.connect(dbAddress,dbUserName, dbPassword, dbName)
53 | return db
54 | except:
55 | return None
56 |
57 |
58 | #Funtion that prepares the HTML ouput files and reads the arguments list to
59 | #obtain the list of trusted sources.
60 | def openFile():
61 |
62 | domains = []
63 | argumentLen = len(sys.argv)
64 | fileHandle = None
65 |
66 | trusted_list = open("trusted-list.txt", 'r')
67 |
68 |
69 | #Collect all trusted sources given by the the trusted list
70 | for line in trusted_list:
71 | domains.append(line[0:-1])
72 |
73 |
74 | if len(domains) > 0:
75 |
76 | #Collect all trusted sources given by the argument list
77 | #for x in range(1, argumentLen):
78 | # domains.append(sys.argv[x])
79 |
80 | #Prepare output files
81 | try:
82 | fileHandleTrusted = open('counterTrust.html', 'w')
83 | fileHandleForeign = open('counterForeign.html', 'w')
84 | return fileHandleTrusted, fileHandleForeign, domains
85 | except:
86 | print "file error"
87 | sys.exit()
88 | else:
89 | print "No IP address! Closing"
90 | sys.exit(0)
91 |
92 |
93 | #The main function that retrieves data from the MySQL database and generate
94 | #statics that are written to HTML files.
95 | def retrieveData(db, fileHandleTrusted, fileHanldeForeign, domains):
96 |
97 | ipBoxString = ""
98 | fileHandle = None
99 | trustedString = "("
100 |
101 | #Generate sql query to filter out only trusted domains
102 | for domain in domains:
103 | trustedString += str(struct.unpack("!I", socket.inet_aton(domain))[0]) + ","
104 |
105 | trustedString = trustedString[:-1]
106 | trustedString += ")"
107 |
108 |
109 | # prepare a cursor object using cursor() method
110 | cursor = db.cursor()
111 |
112 | #Generate statistics for both trusted and foreign hosts.
113 | for domain in ["trusted", "foreign"]:
114 |
115 | if domain == "trusted":
116 | cursor.execute("select * from report inner join rptrecord on report.serial=rptrecord.serial where ip IN " + trustedString)
117 | fileHandle = fileHandleTrusted
118 | else:
119 | cursor.execute("select * from report inner join rptrecord on report.serial=rptrecord.serial where ip NOT IN " + trustedString)
120 | fileHandle = fileHanldeForeign
121 |
122 | data = cursor.fetchall()
123 | resultCount = len(data)
124 |
125 | totalCount = 0
126 | dkimPass = 0
127 | dkimFail = 0
128 | spfPass = 0
129 | spfFail = 0
130 | dmarcCompliant = 0
131 | dmarcUnCompliant = 0
132 |
133 | ipaddr = []
134 |
135 |
136 | #Acumulate authentication results
137 | for entry in data:
138 |
139 | count = entry[17]
140 | totalCount += count
141 | dkimResult = entry[21]
142 | spfResult = entry[24]
143 |
144 | for y in range (0, count):
145 | ipaddr.append(socket.inet_ntoa(struct.pack('!L', entry[15])))
146 |
147 | #Populate the authentication counters from the result of the SQL query
148 | if (dkimResult == 'pass'):
149 | dkimPass += count
150 | else:
151 | dkimFail += count
152 |
153 | if spfResult == 'pass':
154 | spfPass += count
155 | else:
156 | spfFail += count
157 |
158 | #Configure strictness of DMARC check here.
159 | if ((dkimResult == 'pass') or (spfResult == 'pass')):
160 | dmarcCompliant += count
161 | else:
162 | dmarcUnCompliant += count
163 |
164 | print domain + ":\n"
165 | print "Totalcount:\t" + str(totalCount)
166 | print "dkim pass:\t" + str(dkimPass)
167 | print "dkim fail:\t" + str(dkimFail)
168 | print "spf pass:\t" + str(spfPass)
169 | print "spf fail:\t" + str(spfFail)
170 |
171 | # Count the number of message for each IP
172 | ipCount = Counter(ipaddr)
173 |
174 | # Generate string for Ip address box:
175 | for entry in ipCount:
176 | ipBoxString += entry + "\t|\t" + str(ipCount[entry]) + "\n------------------------------------\n"
177 |
178 |
179 | print ipBoxString
180 |
181 |
182 |
183 |
184 |
185 |
186 | result = """
187 |
188 |
189 |
190 |
191 | IP address:
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 | """ + str(dkimPass) + """ """ + calculateRatio(dkimPass, totalCount) + """ %
206 | DKIM pass
207 |
208 |
209 |
210 |
211 | """ + str(dkimFail) + """ """ + calculateRatio(dkimFail, totalCount) + """ %
212 | DKIM fail
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 | """ + str(spfPass) + """ """ + calculateRatio(spfPass, totalCount) + """ %
223 | SPF pass
224 |
225 |
226 |
227 |
228 | """ + str(spfFail) + """ """ + calculateRatio(spfFail, totalCount) + """ %
229 | SPF fail
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 | """ + str(totalCount) + """ tekst
240 | Total count
241 |
242 |
243 |
244 |
245 | """ + str(dmarcCompliant) + """ """ + calculateRatio(dmarcCompliant, totalCount) + """ %
246 | DMARC compliant
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 | """
255 |
256 | ipBoxString = ""
257 |
258 | # Write HTML results
259 | fileHandle.write(result)
260 | fileHandle.close()
261 |
262 |
263 |
264 | database = connectToDB(dbAddress,dbUserName, dbPassword, dbName)
265 | fileHandleTrusted,fileHandleForeign, domains = openFile()
266 | retrieveData(database, fileHandleTrusted, fileHandleForeign, domains)
267 |
268 |
--------------------------------------------------------------------------------
/tool2/dmarc-EmailTest-client.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | #################################################
4 | # Author: Yadvir Singh #
5 | # Date: 14-08-2016 #
6 | # Description: #
7 | # #
8 | # Client script that reads email from a #
9 | # reserved mailbox. The complete email #
10 | # (including headers) is sent to the server #
11 | # over a secure channel #
12 | # #
13 | # SSL connection based on: #
14 | # #
15 | # https://carlo-hamalainen.net/blog #
16 | # /2013/1/24/python-ssl-socket-echo- #
17 | # test-with-self-signed-certificate #
18 | # #
19 | # To be set by user: #
20 | # #
21 | # mailbox variable #
22 | # certificate variable #
23 | # #
24 | #################################################
25 |
26 |
27 | import socket, ssl
28 | import os
29 | import time
30 |
31 | mailbox = 'domain.txt'
32 | certificate = "cert"
33 |
34 | #Check if any mail is present in the reserved mailbox.
35 | #If so, sent the mail including all the headers over the secure channel.
36 | def poll():
37 |
38 | while True:
39 | mailBoxFileHandle = open(mailbox,'r+')
40 | message = mailBoxFileHandle.read()
41 |
42 | #Check if message is not empty
43 | if len(message) > 10:
44 | send(message)
45 | mailBoxFileHandle.close()
46 |
47 |
48 | # Empty the mail box for the the next test, remove this line if
49 | # you whish to keep the email.
50 | # (Hackish way of emtying the file contents)
51 | open(mailbox, 'w').close()
52 |
53 | time.sleep(5)
54 |
55 | #Function that builds a secure connection between the client and server.
56 | def send(data):
57 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
58 |
59 | # Require a certificate from the server. We used a self-signed certificate
60 | # so here ca_certs must be the server certificate itself.
61 | ssl_sock = ssl.wrap_socket(s,
62 | ca_certs=certificate,
63 | cert_reqs=ssl.CERT_REQUIRED)
64 |
65 |
66 | ssl_sock.connect(('localhost', 12345))
67 | ssl_sock.write(data)
68 | ssl_sock.close()
69 |
70 |
71 |
72 |
73 | poll()
74 |
75 |
--------------------------------------------------------------------------------
/tool2/dmarc-EmailTest-server.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 | #################################################
4 | # Author: Yadvir Singh #
5 | # Date: 14-08-2016 #
6 | # Description: #
7 | # #
8 | # Server script that listen for incomming #
9 | # connections from clients. The email passed #
10 | # by client is checked on SPF and DKIM #
11 | # allignment. The result is written to an #
12 | # HTML file #
13 | # #
14 | # SSL connection based on: #
15 | # #
16 | # https://carlo-hamalainen.net/blog #
17 | # /2013/1/24/python-ssl-socket-echo- #
18 | # test-with-self-signed-certificate #
19 | # #
20 | # To be set by user: #
21 | # #
22 | # key variable #
23 | # certificate variable #
24 | # #
25 | #################################################
26 |
27 | import socket, ssl
28 | import dkim
29 | import spf
30 | import sys
31 | import re
32 | from email.parser import Parser
33 | import email
34 |
35 |
36 | certificate = "cert"
37 | key = "key"
38 |
39 |
40 | #Function that checks the mail obtained from the client on SPF and DKIM allignment.
41 | def checkMail (message):
42 |
43 | outputFileHandle = None
44 |
45 | try:
46 | outputFileHandle = open('dmarcCheck.html', 'w')
47 | except:
48 | print "file error"
49 | sys.exit()
50 |
51 |
52 | htmlDMARCBox = ""
53 | htmlDKIMBox = ""
54 | htmlSPFBox = ""
55 | receivedHeader = ""
56 | fromHeader = ""
57 |
58 | #Read the message from the inbox
59 | headers = email.parser.Parser().parsestr(message)
60 |
61 |
62 | for field in headers.items():
63 | if field[0] == "Received" and "[" in field[1] and "]" in field[1]:
64 | receivedHeader = field[1]
65 |
66 | pattern = re.compile(r'\[.*]')
67 | result = re.search(pattern, receivedHeader).group(0)
68 |
69 | pattern = re.compile(r'from .*? ')
70 | result2 = re.search(pattern, receivedHeader).group(0)
71 |
72 | subject = headers['subject']
73 |
74 | #Variables needed for spf check
75 |
76 | #We need only the email address
77 | if "<" in headers['from'] and ">" in headers['from']:
78 | pattern = re.compile(r'\<.*>')
79 | fromHeader = re.search(pattern, headers['from']).group(0)
80 | fromHeader = fromHeader[1:-1]
81 | else:
82 | fromHeader = headers['from']
83 |
84 |
85 | ipaddr = result[1:-1]
86 | host = result2[5:-1]
87 |
88 |
89 | # Perfom SPF and DKIM checks
90 | spfResult = spf.check(i=ipaddr,s=fromHeader,h=host)
91 | dkimResult = dkim.verify(message ,None)
92 |
93 |
94 | #Create HTML conentent according to the results of the test
95 | if (spfResult[0] == 'pass'):
96 | htmlSPFBox = """
97 | SPF check passed ↳ """ + spfResult[2] + """
98 | """
99 | else:
100 | htmlSPFBox = """
101 | SPF check failed ↳ """ + spfResult[2] + """
102 | """
103 |
104 | if (dkimResult == True):
105 | htmlDKIMBox = """
106 | DKIM check passed
107 | """
108 | else:
109 | htmlDKIMBox = """
110 | DKIM check failed
111 | """
112 |
113 | if (spfResult[0] == 'pass' or dkimResult == True):
114 | htmlDMARCBox = """
115 | DMARC check passed
116 | """
117 | else:
118 | htmlDMARCBox = """
119 | DMARC check failed
120 | """
121 |
122 |
123 | html = """
124 |
125 |
DMARC test
126 |
127 |
128 |
129 | Result
130 |
131 |
132 |
133 |
134 |
135 | Subject: """ + subject + """
136 |
137 |
138 | """ + htmlDKIMBox + htmlSPFBox + """ """ + htmlDMARCBox + """
139 |
140 |
141 |
142 |
143 | """
144 | outputFileHandle.write(html)
145 | outputFileHandle.close()
146 |
147 | # Empty the mail box for the the next test, remove this line if
148 | # you whish to keep the email.
149 | # (Hackish way of emtying the file contents)
150 | #open(mailbox, 'w').close()
151 |
152 |
153 | def writeMailTofile(data):
154 | try:
155 | fileHandle = open('dmarcTestEmail', 'w')
156 | fileHandle.write(data)
157 | fileHandle.close()
158 | except:
159 | print "file error"
160 | sys.exit()
161 |
162 |
163 | def read_data(connstream, data):
164 | print "read_data:", data
165 |
166 | if data[-1] == "\n" and data[-2] == "\n":
167 | return False
168 |
169 |
170 | def deal_with_client(connstream):
171 | data = connstream.recv(8192)
172 | while data:
173 | print "in loop"
174 | if not read_data(connstream, data):
175 | break
176 | data = connstream.recv(8192)
177 | return data
178 |
179 |
180 | def startSocket():
181 | bindsocket = socket.socket()
182 | bindsocket.bind(('localhost', 12345))
183 | bindsocket.listen(1)
184 |
185 | while True:
186 | newsocket, fromaddr = bindsocket.accept()
187 | connstream = ssl.wrap_socket(newsocket,
188 | server_side=True,
189 | certfile=certificate,
190 | keyfile=key, ssl_version = ssl.PROTOCOL_TLSv1_2)
191 |
192 | try:
193 | data = deal_with_client(connstream)
194 | finally:
195 | connstream.shutdown(socket.SHUT_RDWR)
196 | connstream.close()
197 | newsocket.close()
198 | break
199 | writeMailTofile(data)
200 | checkMail(data)
201 |
202 |
203 | startSocket()
204 |
205 |
--------------------------------------------------------------------------------
/tool2/dns-record-tracker.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 17-07-2016 #
4 | # Description: #
5 | # #
6 | # Script retrieves the current SPF, DKIM and #
7 | # DMARC records. These records are saved in #
8 | # the database. If DNS records have been #
9 | # changed, a new entry is inserted into #
10 | # the database #
11 | # #
12 | # Database variables to be set by user: #
13 | # #
14 | # - dbAddress #
15 | # - dbName #
16 | # - dbUserName #
17 | # - dbPassword #
18 | # - domain #
19 | # - dkim_id (dkim identifier) #
20 | # #
21 | # #
22 | #################################################
23 |
24 |
25 |
26 | import dns.resolver
27 | import MySQLdb
28 | import time
29 |
30 |
31 | dbAddress = "127.0.0.1"
32 | dbName = "dmarc"
33 | dbUserName = "dmarc"
34 | dbPassword = "dmarcrp2"
35 | domain = "dmarc-research.nl"
36 | dkim_id = "mail"
37 |
38 |
39 | #Create the database connection
40 | def DBconnect(dbAddress, dbUserName, dbPassword, dbName):
41 |
42 | try:
43 |
44 | db = MySQLdb.connect(dbAddress, dbUserName, dbPassword, dbName)
45 | return db
46 |
47 | except:
48 | return None
49 |
50 |
51 | def dnsLookup(domain):
52 | record = ""
53 |
54 | try:
55 | answer = dns.resolver.query(domain , "TXT")
56 | for data in answer:
57 | record = str(data).replace("\"", "")
58 | return record
59 | except:
60 | return "None"
61 |
62 | #Function that does a DNS lookup of SPF, DKIM and DMARC and checks if
63 | #any of the 3 records have changed.
64 | def recordLookup(domain, dkim_id, db):
65 |
66 | dmarcRecord = ""
67 | dkimRecord = ""
68 | spfRecord = ""
69 |
70 | #Prepare a cursor object
71 | cursor = db.cursor()
72 |
73 |
74 | #Fetch all previous records.
75 | cursor.execute("select * from dns_records")
76 |
77 | data = cursor.fetchall();
78 | resultCount = len(data)
79 |
80 | #Fetch current records
81 | dmarcRecord = str(dnsLookup("_dmarc." + domain))
82 | dkimRecord = str(dnsLookup(dkim_id + "._domainkey." + domain))
83 | spfRecord = str(dnsLookup (domain))
84 | currentTime = str(time.strftime("%y-%m-%d"))
85 |
86 |
87 | #If no records are found in database, insert the first one.
88 | if resultCount == 0:
89 |
90 | addUpdate = ("insert into dns_records (dmarc ,dkim, spf, dateStamp) values (%s, %s, %s, %s)" )
91 | cursor.execute(addUpdate, (dmarcRecord, dkimRecord, spfRecord, currentTime))
92 | db.commit()
93 |
94 | else:
95 |
96 | print "Not the first"
97 |
98 | #Check if the record is changed, if so insert the current record.
99 | if (dmarcRecord != data[-1][1]) or (dkimRecord != data[-1][2]) or (spfRecord != data[-1][3]):
100 | print "difference spotted"
101 | addUpdate = ("insert into dns_records (dmarc ,dkim, spf, dateStamp) values (%s, %s, %s, %s)" )
102 | cursor.execute(addUpdate, (dmarcRecord, dkimRecord, spfRecord, currentTime))
103 | db.commit()
104 |
105 | else:
106 | print "No change detected"
107 |
108 |
109 |
110 |
111 | db = DBconnect(dbAddress, dbUserName, dbPassword, dbName)
112 | recordLookup(domain, dkim_id, db)
113 |
--------------------------------------------------------------------------------
/tool2/domain-status.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 16-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that retrieves the DMARC record #
7 | # and checks several parameters for their #
8 | # presences. Generates a HTML widget with #
9 | # the results #
10 | # #
11 | # To be set by user: #
12 | # #
13 | # variable domain #
14 | # #
15 | #################################################
16 |
17 |
18 |
19 | import dns.resolver
20 | import sys
21 |
22 | domain = "dmarc-research.nl"
23 |
24 |
25 | #Funtion that creates the filehandle for writing the results
26 | def openFile():
27 |
28 | try:
29 | fileHandle = open('domainstatus.html', 'w')
30 |
31 | except:
32 | print "file error"
33 | sys.exit()
34 |
35 | return fileHandle
36 |
37 |
38 | #Funtion for lookup of DMARC DNS record.
39 | def dnsLookup(domain):
40 | record = ""
41 |
42 | try:
43 | answer = dns.resolver.query('_dmarc.' + domain , "TXT")
44 | for data in answer:
45 | record = str(data).replace("\"", "")
46 | return record
47 | except:
48 | return "None"
49 |
50 |
51 | #Helper funtion to strip DNS results
52 | def getSubString(string, char):
53 |
54 | try:
55 | IndexStart = string.index(char)
56 | except:
57 | return None
58 | IndexEqual = string.index ('=', IndexStart) + 1
59 | IndexEnd = string.index(';',IndexStart)
60 |
61 | subString = string[IndexEqual:IndexEnd]
62 |
63 | return subString
64 |
65 |
66 | #Funtion that writes the results to an HTML file.
67 | def generateHtml(fileHandle, recordData):
68 |
69 | print recordData
70 |
71 | rowString = ""
72 |
73 | dmarcRecordDisection = []
74 |
75 | htmlStart = """
76 |
Status
77 |
78 |
79 |
80 | DMARC status
81 |
82 |
83 | """
84 |
85 | htmlEnd = """
86 |
87 |
88 |
89 | DMARC record
90 |
91 |
92 |
93 |
94 | """
95 |
96 |
97 | #Extract the parameters from the DMARC record
98 | policy = getSubString(recordData, "p=")
99 | subPolicy = getSubString(recordData, "sp=")
100 | rua = getSubString(recordData, "rua=")
101 | ruf = getSubString(recordData, "ruf=")
102 | pct = getSubString(recordData, "pct=")
103 |
104 |
105 | dmarcRecordDisection.append(("Policy",policy))
106 | dmarcRecordDisection.append(("Sub-policy",subPolicy))
107 | dmarcRecordDisection.append(("RUA",rua))
108 | dmarcRecordDisection.append(("RUF",ruf))
109 | dmarcRecordDisection.append(("PCT",pct))
110 |
111 |
112 |
113 | #Wrap results into HTML.
114 | for data in dmarcRecordDisection:
115 |
116 | if (data[1] == None) and ((data[0] == "RUA") or (data[0] == "RUF")):
117 | rowString += """No email specified for """ + data [0] +""" reports \n"""
118 |
119 | elif (data[1] == None):
120 | rowString += """No value found for """ + data [0] +""" \n"""
121 |
122 | else:
123 | rowString += """""" + data [0] +""" configured \n"""
124 |
125 |
126 |
127 | fileHandle.write(htmlStart)
128 | fileHandle.write(rowString)
129 | fileHandle.write(htmlEnd)
130 | fileHandle.close()
131 |
132 |
133 | fileHandle = openFile()
134 | record = dnsLookup(domain)
135 | generateHtml(fileHandle, record)
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
--------------------------------------------------------------------------------
/tool2/graph.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | #################################################
4 | # Author: Yadvir Singh #
5 | # Date: 16-07-2016 #
6 | # Description: #
7 | # #
8 | # Script that generates DMARC authentication #
9 | # graphs. Additionally, a time line with DNS #
10 | # Record changes is also created. #
11 | # #
12 | # Database variables to be set by user: #
13 | # #
14 | # - dbAddress #
15 | # - dbName #
16 | # - dbUserName #
17 | # - dbPassword #
18 | # #
19 | # #
20 | #################################################
21 |
22 |
23 |
24 | import sys
25 | import time
26 | from datetime import date, timedelta
27 | import MySQLdb
28 | import socket
29 | import struct
30 |
31 |
32 | dbAddress = "127.0.0.1"
33 | dbName = "dmarc"
34 | dbUserName = "dmarc"
35 | dbPassword = "dmarcrp2"
36 |
37 |
38 | #Function to generate date points. By default this function will generate
39 | #a set of dates that contains the 30 past days from date argument.
40 | #based on #http://stackoverflow.com/questions/1060279/iterating-through-a-range-of-dates-in-python
41 | def dateItterator(date):
42 |
43 | dates = []
44 | day = timedelta(days=1)
45 |
46 | date1 = date
47 | date2 = date - timedelta(days=30) # Adjust date range here
48 |
49 | while date2 < date1:
50 | dates.append(date2)
51 | date2 += day
52 |
53 | return dates
54 |
55 |
56 | #Funtion that prepares the HTML ouput files and reads the arguments list to
57 | #obtain the list of trusted sources.
58 | def openFile():
59 |
60 | domains = []
61 | argumentLen = len(sys.argv)
62 | fileHandle = None
63 |
64 | trusted_list = open("trusted-list.txt", 'r')
65 |
66 |
67 | #Collect all trusted sources given by the the trusted list
68 | for line in trusted_list:
69 | domains.append(line[0:-1])
70 |
71 |
72 | if len(domains) > 0:
73 |
74 | #Collect all trusted sources given by the argument list
75 | #for x in range(1, argumentLen):
76 | # domains.append(sys.argv[x])
77 |
78 | try:
79 | fileHandleTrusted = open('graphTrust.js', 'w')
80 | fileHandleForeign = open('graphForeign.js', 'w')
81 | return fileHandleTrusted, fileHandleForeign, domains
82 | except:
83 | print "file error"
84 | sys.exit()
85 |
86 | else:
87 | print "No IP address! Closing"
88 | sys.exit(0)
89 |
90 | #Function to obtain all reports of trusted and unknown sources from the
91 | #database.
92 | def sqlFilterQuery(domainList):
93 |
94 | trustedString = "("
95 |
96 | for domain in domains:
97 | trustedString += str(struct.unpack("!I", socket.inet_aton(domain))[0]) + ","
98 |
99 | trustedString = trustedString[:-1]
100 | trustedString += ")"
101 |
102 | queryTrusted = "select * from report inner join rptrecord on report.serial=rptrecord.serial where ip IN " + trustedString
103 | queryForeign = "select * from report inner join rptrecord on report.serial=rptrecord.serial where ip NOT IN " + trustedString
104 |
105 | return queryTrusted, queryForeign
106 |
107 | #Main function that collects information for generating the graphs
108 | #and the DNS history time line.
109 | def generateGraph(dates, query, dbAddress, dbUserName, dbPassword, dbName):
110 |
111 | dkimResult = ""
112 | spfResult = ""
113 | count = 0
114 | dmarcCompliant = 0
115 | dmarcUnCompliant = 0
116 |
117 | reportAggergate = []
118 | annotations = []
119 |
120 |
121 | # Open database connection
122 | db = MySQLdb.connect(dbAddress, dbUserName, dbPassword, dbName)
123 |
124 | # prepare a cursor object using cursor() method
125 | cursor = db.cursor()
126 |
127 | for date in dates:
128 |
129 | lowerbound = date - timedelta(days= 1)
130 | #print str(date), str(lowerbound)
131 |
132 | cursor.execute(query + " and maxdate <= '"+ str(date) +" 23:59:59' and mindate >= '" + str(lowerbound) + " 00:00:00'")
133 |
134 | data = cursor.fetchall()
135 | resultCount = len(data)
136 |
137 |
138 | # Loop through the resulting rows and populate the counters
139 | if not (len(data) == 0):
140 |
141 | for entry in data:
142 |
143 | dkimResult = entry[21]
144 | spfResult = entry[23]
145 | count = entry[17]
146 |
147 | #Configure strictness of DMARC check here.
148 | if ((dkimResult == 'pass') or (spfResult == 'pass')):
149 | dmarcCompliant += count
150 | else:
151 | dmarcUnCompliant += count
152 |
153 |
154 | reportAggergate.append((date, int(dmarcCompliant),int(dmarcUnCompliant)))
155 |
156 | dmarcCompliant = 0
157 | dmarcUnCompliant = 0
158 |
159 |
160 | #Retrieve the DNS record changes from the database.
161 | cursor.execute("select * from dns_records")
162 |
163 | data = cursor.fetchall()
164 | resultCount = len(data)
165 |
166 | for x in data:
167 | annotations.append(x)
168 |
169 | db.close()
170 |
171 | return reportAggergate, annotations
172 |
173 |
174 | #Function that transform the restults of generateGraph into javascript file
175 | def writeToFile (filehandle, data, annotations):
176 |
177 | prepend = "data: [ "
178 |
179 | datapointCompliant = ""
180 | datapointUnCompliant = ""
181 | annotationString = ""
182 |
183 | #Generate graph points
184 | for x in data:
185 | datapointCompliant += "{ x:" + str((x[0]-date(1970,1,1)).total_seconds()) + ", y:" + str(x[1]) +" },"
186 | datapointUnCompliant += ("{ x:" + str((x[0]-date(1970,1,1)).total_seconds()) + ", y:" + str(x[2]) +" },")
187 |
188 |
189 | graph = """
190 | var palette = new Rickshaw.Color.Palette();
191 |
192 | var graph = new Rickshaw.Graph( {
193 | element: document.querySelector("#chart"),
194 | width: 550,
195 | height: 250,
196 | series: [
197 | {
198 | name: "Dmarc UnCompliant",
199 | data: [""" + datapointUnCompliant + """],
200 | color: palette.color()
201 | },
202 | {
203 | name: "Dmarc Compliant",
204 | data: [""" + datapointCompliant + """],
205 | color: palette.color()
206 | },
207 |
208 | ]
209 | } );
210 |
211 | var x_axis = new Rickshaw.Graph.Axis.Time( { graph: graph } );
212 |
213 | var y_axis = new Rickshaw.Graph.Axis.Y( {
214 | graph: graph,
215 | orientation: 'left',
216 | tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
217 | element: document.getElementById('y_axis'),
218 | } );
219 |
220 | var legend = new Rickshaw.Graph.Legend( {
221 | element: document.querySelector('#legend'),
222 | graph: graph
223 | } );
224 |
225 | var annotator = new Rickshaw.Graph.Annotate({
226 | graph: graph,
227 | element: document.getElementById('timeline')
228 | });
229 |
230 | """
231 | unixTime = str((x[0]-date(1970,1,1)).total_seconds())
232 |
233 | #Add annotations to the time line with buttons
234 | for data in annotations:
235 | annotationString += ("annotator.add(" + unixTime + ", \" \" );\n")
236 |
237 |
238 | graphEnd = """
239 | annotator.update();
240 |
241 |
242 | graph.render();
243 |
244 | """
245 |
246 |
247 | #Write javascript files to appropriate files.
248 | filehandle.write(graph)
249 | filehandle.write(annotationString)
250 | filehandle.write(graphEnd)
251 | filehandle.close()
252 |
253 |
254 | fileHandleTrusted, fileHandleForeign, domains = openFile()
255 |
256 | trusted, foreign = sqlFilterQuery(domains)
257 | dates = dateItterator(date(2016, 6, 30))
258 |
259 | data, annotations = generateGraph(dates, trusted, dbAddress, dbUserName, dbPassword, dbName)
260 | writeToFile(fileHandleTrusted, data, annotations)
261 |
262 | data, annotations = generateGraph(dates, foreign, dbAddress, dbUserName, dbPassword, dbName)
263 | writeToFile(fileHandleForeign, data, annotations)
264 |
265 |
--------------------------------------------------------------------------------
/tool2/sendTestMail.py:
--------------------------------------------------------------------------------
1 | #################################################################################
2 | # Author: Yadvir Singh #
3 | # Date: 16-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that automatically sends an email #
7 | # to the reserved mailbox that is read by #
8 | # email-test.py #
9 | # #
10 | # Based on example from: https://docs.python.org/2/library/email-examples.html #
11 | # #
12 | # To be set by user: #
13 | # #
14 | # - smptAddress #
15 | # - mailAddress #
16 | #################################################################################
17 |
18 | import smtplib
19 | import time
20 | from email.mime.text import MIMEText
21 |
22 | smtpAddress = '145.100.104.165'
23 | mailAddress = 'test@dmarc-research.nl'
24 |
25 | #Email message content
26 | msg = MIMEText('Test email for DMARC')
27 |
28 | #Use the current time as an identifier in the subject line
29 | msg['Subject'] = str(time.time())
30 | msg['From'] = mailAddress
31 | msg['To'] = mailAddress
32 |
33 | # Send the message via our own SMTP server.
34 | s = smtplib.SMTP(smtpAddress)
35 | s.sendmail(mailAddress, [mailAddress], msg.as_string())
36 | s.quit()
37 |
38 |
--------------------------------------------------------------------------------
/tool2/spf-ip-extract.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 15-08-2016 #
4 | # Description: #
5 | # #
6 | # Script that extracts IP addresses from #
7 | # spf records. Networks addresses are also #
8 | # supported. All addresses are written to a #
9 | # file. #
10 | # #
11 | # Variables to be set by user: #
12 | # #
13 | # - domain #
14 | # #
15 | #################################################
16 |
17 |
18 |
19 |
20 | import re
21 | import dns.resolver
22 | import netaddr as ne
23 |
24 | domain = "dmarc-research.nl"
25 | ip_list = []
26 |
27 | def writeToFile(data):
28 |
29 | fileHandle = open('trusted-list.txt', 'w')
30 |
31 | for line in data:
32 | fileHandle.write(line + "\n")
33 |
34 | fileHandle.close()
35 |
36 | def dnsLookup(domain):
37 | record = ""
38 |
39 | try:
40 | answer = dns.resolver.query(domain , "TXT")
41 | return str(answer[0])
42 | except:
43 | return "None"
44 |
45 |
46 | #Start
47 | def init():
48 | # Pattern based on https://gist.github.com/0x9900/4471462
49 |
50 | pattern = re.compile(r'ip4[:=](.*?\s)', re.IGNORECASE)
51 | spfRecord = dnsLookup(domain)
52 |
53 | result = re.findall(pattern, spfRecord)
54 |
55 |
56 | #Parse IP addresses
57 | for x in result:
58 | ip = x[0:-1]
59 |
60 | print ip
61 |
62 | if "/" in ip:
63 | print "Dealing with complete network"
64 |
65 | ip_network = ne.IPNetwork(ip)
66 |
67 | for address in list(ip_network):
68 | ip_list.append(str(address))
69 |
70 | else:
71 | ip_list.append(ip)
72 |
73 | writeToFile(ip_list)
74 |
75 |
76 | init()
77 |
78 |
79 |
--------------------------------------------------------------------------------
/tool2/trusted-list.txt:
--------------------------------------------------------------------------------
1 | 192.168.0.1
2 |
--------------------------------------------------------------------------------
/visualizations/bubble-chart-AS.py:
--------------------------------------------------------------------------------
1 |
2 | #################################################
3 | # Author: Yadvir Singh #
4 | # Date: 17-07-2016 #
5 | # Description: #
6 | # #
7 | # Script that generates a bubble chart to #
8 | # review where emails come from, in which #
9 | # quantities and the ratio of successful #
10 | # DMARC authentication results #
11 | # #
12 | # Database variables to be set by user: #
13 | # #
14 | # - dbAddress #
15 | # - dbName #
16 | # - dbUserName #
17 | # - dbPassword #
18 | # #
19 | # Other variables to be set by the user: #
20 | # - asnDatabase that contains IP <-> AN #
21 | # mapping #
22 | # #
23 | # #
24 | #################################################
25 |
26 |
27 |
28 | from __future__ import division # needed for float division in python < 3
29 | import MySQLdb
30 | import matplotlib.pyplot as plt
31 | import collections
32 | import numpy as np
33 | import random
34 | import pyasn
35 | import struct
36 | import netaddr
37 | import urllib
38 | import json
39 | import sys
40 |
41 |
42 | dbAddress = "127.0.0.1"
43 | dbName = "dmarc"
44 | dbUserName = "dmarc"
45 | dbPassword = "dmarcrp2"
46 | asnDatabase = 'asn-mapping.dat'
47 |
48 | ASvolume = {}
49 | ASipMapping = {}
50 | asndb = pyasn.pyasn(asnDatabase)
51 |
52 | lookup = False
53 | output = open('asn-list.txt', 'w')
54 |
55 |
56 | #Convert MySQL binary blob data to IPv6 string
57 | def convertIPv6(ipv6):
58 |
59 | ipv6String = ""
60 | octetCount = 0
61 |
62 | for a in ipv6:
63 | hexValue = hex(int(struct.unpack('!B', a)[0]))[2:]
64 |
65 | if len(hexValue) < 2:
66 | hexValue = "0" + hexValue
67 |
68 | ipv6String = ipv6String + hexValue
69 | octetCount += 1
70 |
71 | if octetCount == 2:
72 | ipv6String += ":"
73 | octetCount = 0
74 |
75 | return ipv6String[:-1]
76 |
77 | #Store IP addresses found from each AS and their authentication result.
78 | def insertIP(AS ,ipv4, ipv6, dkimResult, spfResult):
79 |
80 | insertString = None
81 |
82 | if ipv6 == None:
83 | insertString = ipv4
84 | else:
85 | insertString = ipv6
86 |
87 | if dkimResult is not "pass" and spfResult is not "pass":
88 | ASipMapping[AS].append("F " + insertString)
89 |
90 | else:
91 | ASipMapping[AS].append("P " + insertString)
92 |
93 | #Write IP address authentication results to a file.
94 | def writeIP(filehandle, array):
95 |
96 | for address in array:
97 | filehandle.write(address + "\n")
98 |
99 |
100 | #Get name linked to the AS
101 | def getASNdata(asn):
102 | url = "http://rdap.arin.net/bootstrap/autnum/" + str(asn)
103 | response = urllib.urlopen(url)
104 | data = json.loads(response.read())
105 | return data['name']
106 |
107 |
108 | #Check if the user wants to lookup ASN names
109 | if len(sys.argv) > 1:
110 | argument = sys.argv[1]
111 | if argument == '--asn-lookup':
112 | lookup = True
113 | print "ASN lookup enabled"
114 |
115 | try:
116 | db = MySQLdb.connect(dbAddress, dbUserName, dbPassword, dbName)
117 | except:
118 | print "Error while accessing the database"
119 |
120 | cursor = db.cursor()
121 |
122 | #Fetch all reports that are neede for the bubble chart
123 | cursor.execute("select * from report inner join rptrecord on report.serial=rptrecord.serial")
124 | queryResult = cursor.fetchall()
125 |
126 | #Comment in this for loop for creating real bubble chart
127 | #Parse result and count total volume and successful authentication results.
128 | for x in queryResult:
129 | ip = x[15]
130 | ipString = None
131 | ipv6 = x[16]
132 | ipv6String = None
133 | dkimResult = x[21]
134 | spfResult = x[23]
135 | count = x[17]
136 |
137 |
138 | #Check if the address is IPv4 or IPv6 and retrieve the ASN
139 | if ipv6 == None:
140 | ipString = str(netaddr.IPAddress(ip))
141 | ASnumber = asndb.lookup(ipString)[0]
142 | else:
143 | #The binary blob from the MySQL database needs to be converted to a
144 | #IPv6 string first.
145 | ipv6String = convertIPv6(ipv6)
146 | ASnumber = asndb.lookup(str(netaddr.IPAddress(ipv6String)))[0]
147 |
148 | try:
149 | volume = ASvolume[ASnumber]
150 | ASipList = ASvolume[ASnumber][3]
151 |
152 | print ASipList
153 |
154 |
155 | dmarcPassCount = volume[0]
156 | volumeCount = volume[1]
157 |
158 | if dkimResult == "pass" or spfResult == "pass":
159 | dmarcPassCount += count
160 | volumeCount += count
161 | else:
162 | volumeCount += count
163 |
164 |
165 | ASvolume[ASnumber] = (dmarcPassCount, volumeCount)
166 |
167 | insertIP(ASnumber, ip, ipv6, dkimResult, spfResult)
168 |
169 | except:
170 | #Apperently we are the first to insert something for this IP block
171 | if dkimResult == "pass" or spfResult == "pass":
172 | ASvolume[ASnumber] = (count, count,[])
173 | else:
174 | ASvolume[ASnumber] = (0, count, [])
175 |
176 | ASipMapping[ASnumber] = []
177 |
178 | insertIP(ASnumber, ipString, ipv6String, dkimResult, spfResult)
179 |
180 |
181 | #Comment in this for loop for creating real bubble chart
182 | #Calculate the ratio of each IP chunk
183 | for key, value in ASvolume.iteritems():
184 | dmarcPass = value[0]
185 | totalCount = value[1]
186 |
187 | ratio = dmarcPass / totalCount
188 | ASvolume[key] = (dmarcPass, totalCount , ratio)
189 |
190 |
191 |
192 |
193 | fig, ax = plt.subplots()
194 | xAxis = []
195 | yAxis = []
196 | volume = []
197 | ip = []
198 | ratioList = []
199 | chunkIP = None
200 | chunkRatio = None
201 | chunkVolume = None
202 |
203 |
204 | #Comment in this section for creating real bubble chart
205 | for key, value in ASvolume.iteritems():
206 |
207 | ASnumber = key
208 | chunkVolume = value[1]
209 | chunkRatio = value[2]
210 |
211 | ip.append(chunkIP)
212 | volume.append(chunkVolume)
213 | ratioList.append(chunkRatio)
214 |
215 | xAxis.append(int(ASnumber))
216 | yAxis.append(chunkRatio)
217 | if(chunkVolume < 10000):
218 | ax.annotate(str(ASnumber), (xAxis[-1]+(chunkVolume/3), yAxis[-1]), size=20)
219 | else:
220 | ax.annotate(str(ASnumber), (xAxis[-1]+(chunkVolume/10), yAxis[-1]), size=20)
221 |
222 |
223 | #For demo purposes only. Comment out and comment in for loop above if
224 | #you want to generate the bubble chart for a real domain
225 | #############Demo start
226 | '''or x in range (0,4):
227 |
228 | ASNrandom = random.randrange(0, 30000)
229 |
230 | while ASNrandom in ip:
231 | ASNrandom = random.randrange(0, 30000)
232 |
233 | ip.append(ASNrandom)
234 | total = random.randrange(0, 1000)
235 |
236 |
237 | ASipMapping[ASNrandom] = []
238 |
239 |
240 | for x in range(0, total):
241 | ASipMapping[ASNrandom].append("demo address")
242 |
243 |
244 | ratio = random.random()
245 |
246 | ratioList.append(ratio)
247 |
248 | xAxis.append(ASNrandom)
249 | yAxis.append(ratio)
250 |
251 |
252 | ASvolume[ASNrandom] = (0, total , ratio)
253 |
254 | if(total < 10000):
255 | ax.annotate(str(ASNrandom), (xAxis[-1]+int(total/3), yAxis[-1]), size=20)
256 | else:
257 | ax.annotate(str(1103), (xAxis[-1]+int(total/10), yAxis[-1]), size=20)
258 |
259 | volume.append(total)
260 |
261 | '''
262 | #############Demo end
263 |
264 | ax.scatter(xAxis, yAxis, s=volume, marker='o', c=ratioList, cmap=plt.cm.RdYlGn, vmin=0, vmax=1)
265 | ax.set_ylim(bottom=-0.1)
266 | ax.set_xlim(left=0, right=max(xAxis)+5000)
267 | ax.set_xlabel('AS Number')
268 | ax.set_ylabel('Ratio')
269 |
270 | plt.show()
271 |
272 |
273 | #Generate asn list which holds volume and ratio per AS
274 | if lookup:
275 | for asn, value in ASvolume.iteritems():
276 | output.write("----- " + str(asn) + " -----\n")
277 | output.write("Name: \t\t" + getASNdata(asn) + "\n")
278 | output.write("Ratio: \t\t" + str(value[2]) + "\n")
279 | output.write("Volume: \t" + str(value[1]) + "\n")
280 | writeIP(output, ASipMapping[asn])
281 | output.write("\n\n")
282 | else:
283 | for asn, value in ASvolume.iteritems():
284 | output.write("----- " + str(asn) + " -----\n")
285 | output.write("Ratio: \t\t" + str(value[2]) + "\n")
286 | output.write("Volume: \t" + str(value[1]) + "\n")
287 | writeIP(output, ASipMapping[asn])
288 | output.write("\n\n")
289 |
290 | output.close()
291 |
292 |
293 |
294 |
--------------------------------------------------------------------------------
/visualizations/bubble-chart.py:
--------------------------------------------------------------------------------
1 |
2 | #################################################
3 | # Author: Yadvir Singh #
4 | # Date: 17-07-2016 #
5 | # Description: #
6 | # #
7 | # Script that generates a bubble chart to #
8 | # review where emails come from, in which #
9 | # quantities and the ratio of successful #
10 | # DMARC authentication results #
11 | # #
12 | # Database variables to be set by user: #
13 | # #
14 | # - dbAddress #
15 | # - dbName #
16 | # - dbUserName #
17 | # - dbPassword #
18 | # #
19 | # #
20 | #################################################
21 |
22 |
23 |
24 | from __future__ import division # needed for float division in python < 3
25 | import netaddr as ne
26 | import MySQLdb
27 | import matplotlib.pyplot as plt
28 | import collections
29 | import numpy as np
30 | import random
31 |
32 |
33 | dbAddress = "127.0.0.1"
34 | dbName = "dmarc"
35 | dbUserName = "dmarc"
36 | dbPassword = "dmarcrp2"
37 |
38 |
39 | ipVolume = {}
40 |
41 |
42 | #The complete IPv4 Space divided into 256 chunks. Used for categorization.
43 | ipBlocks = ['0.0.0.0/8', '1.0.0.0/8', '2.0.0.0/8', '3.0.0.0/8', '4.0.0.0/8', '5.0.0.0/8', '6.0.0.0/8', '7.0.0.0/8', '8.0.0.0/8', '9.0.0.0/8', '10.0.0.0/8', '11.0.0.0/8', '12.0.0.0/8', '13.0.0.0/8', '14.0.0.0/8', '15.0.0.0/8', '16.0.0.0/8', '17.0.0.0/8', '18.0.0.0/8', '19.0.0.0/8', '20.0.0.0/8', '21.0.0.0/8', '22.0.0.0/8', '23.0.0.0/8', '24.0.0.0/8', '25.0.0.0/8', '26.0.0.0/8', '27.0.0.0/8', '28.0.0.0/8', '29.0.0.0/8', '30.0.0.0/8', '31.0.0.0/8', '32.0.0.0/8', '33.0.0.0/8', '34.0.0.0/8', '35.0.0.0/8', '36.0.0.0/8', '37.0.0.0/8', '38.0.0.0/8', '39.0.0.0/8', '40.0.0.0/8', '41.0.0.0/8', '42.0.0.0/8', '43.0.0.0/8', '44.0.0.0/8', '45.0.0.0/8', '46.0.0.0/8', '47.0.0.0/8', '48.0.0.0/8', '49.0.0.0/8', '50.0.0.0/8', '51.0.0.0/8', '52.0.0.0/8', '53.0.0.0/8', '54.0.0.0/8', '55.0.0.0/8', '56.0.0.0/8', '57.0.0.0/8', '58.0.0.0/8', '59.0.0.0/8', '60.0.0.0/8', '61.0.0.0/8', '62.0.0.0/8', '63.0.0.0/8', '64.0.0.0/8', '65.0.0.0/8', '66.0.0.0/8', '67.0.0.0/8', '68.0.0.0/8', '69.0.0.0/8', '70.0.0.0/8', '71.0.0.0/8', '72.0.0.0/8', '73.0.0.0/8', '74.0.0.0/8', '75.0.0.0/8', '76.0.0.0/8', '77.0.0.0/8', '78.0.0.0/8', '79.0.0.0/8', '80.0.0.0/8', '81.0.0.0/8', '82.0.0.0/8', '83.0.0.0/8', '84.0.0.0/8', '85.0.0.0/8', '86.0.0.0/8', '87.0.0.0/8', '88.0.0.0/8', '89.0.0.0/8', '90.0.0.0/8', '91.0.0.0/8', '92.0.0.0/8', '93.0.0.0/8', '94.0.0.0/8', '95.0.0.0/8', '96.0.0.0/8', '97.0.0.0/8', '98.0.0.0/8', '99.0.0.0/8', '100.0.0.0/8', '101.0.0.0/8', '102.0.0.0/8', '103.0.0.0/8', '104.0.0.0/8', '105.0.0.0/8', '106.0.0.0/8', '107.0.0.0/8', '108.0.0.0/8', '109.0.0.0/8', '110.0.0.0/8', '111.0.0.0/8', '112.0.0.0/8', '113.0.0.0/8', '114.0.0.0/8', '115.0.0.0/8', '116.0.0.0/8', '117.0.0.0/8', '118.0.0.0/8', '119.0.0.0/8', '120.0.0.0/8', '121.0.0.0/8', '122.0.0.0/8', '123.0.0.0/8', '124.0.0.0/8', '125.0.0.0/8', '126.0.0.0/8', '127.0.0.0/8', '128.0.0.0/8', '129.0.0.0/8', '130.0.0.0/8', '131.0.0.0/8', '132.0.0.0/8', '133.0.0.0/8', '134.0.0.0/8', '135.0.0.0/8', '136.0.0.0/8', '137.0.0.0/8', '138.0.0.0/8', '139.0.0.0/8', '140.0.0.0/8', '141.0.0.0/8', '142.0.0.0/8', '143.0.0.0/8', '144.0.0.0/8', '145.0.0.0/8', '146.0.0.0/8', '147.0.0.0/8', '148.0.0.0/8', '149.0.0.0/8', '150.0.0.0/8', '151.0.0.0/8', '152.0.0.0/8', '153.0.0.0/8', '154.0.0.0/8', '155.0.0.0/8', '156.0.0.0/8', '157.0.0.0/8', '158.0.0.0/8', '159.0.0.0/8', '160.0.0.0/8', '161.0.0.0/8', '162.0.0.0/8', '163.0.0.0/8', '164.0.0.0/8', '165.0.0.0/8', '166.0.0.0/8', '167.0.0.0/8', '168.0.0.0/8', '169.0.0.0/8', '170.0.0.0/8', '171.0.0.0/8', '172.0.0.0/8', '173.0.0.0/8', '174.0.0.0/8', '175.0.0.0/8', '176.0.0.0/8', '177.0.0.0/8', '178.0.0.0/8', '179.0.0.0/8', '180.0.0.0/8', '181.0.0.0/8', '182.0.0.0/8', '183.0.0.0/8', '184.0.0.0/8', '185.0.0.0/8', '186.0.0.0/8', '187.0.0.0/8', '188.0.0.0/8', '189.0.0.0/8', '190.0.0.0/8', '191.0.0.0/8', '192.0.0.0/8', '193.0.0.0/8', '194.0.0.0/8', '195.0.0.0/8', '196.0.0.0/8', '197.0.0.0/8', '198.0.0.0/8', '199.0.0.0/8', '200.0.0.0/8', '201.0.0.0/8', '202.0.0.0/8', '203.0.0.0/8', '204.0.0.0/8', '205.0.0.0/8', '206.0.0.0/8', '207.0.0.0/8', '208.0.0.0/8', '209.0.0.0/8', '210.0.0.0/8', '211.0.0.0/8', '212.0.0.0/8', '213.0.0.0/8', '214.0.0.0/8', '215.0.0.0/8', '216.0.0.0/8', '217.0.0.0/8', '218.0.0.0/8', '219.0.0.0/8', '220.0.0.0/8', '221.0.0.0/8', '222.0.0.0/8', '223.0.0.0/8', '224.0.0.0/8', '225.0.0.0/8', '226.0.0.0/8', '227.0.0.0/8', '228.0.0.0/8', '229.0.0.0/8', '230.0.0.0/8', '231.0.0.0/8', '232.0.0.0/8', '233.0.0.0/8', '234.0.0.0/8', '235.0.0.0/8', '236.0.0.0/8', '237.0.0.0/8', '238.0.0.0/8', '239.0.0.0/8', '240.0.0.0/8', '241.0.0.0/8', '242.0.0.0/8', '243.0.0.0/8', '244.0.0.0/8', '245.0.0.0/8', '246.0.0.0/8', '247.0.0.0/8', '248.0.0.0/8', '249.0.0.0/8', '250.0.0.0/8', '251.0.0.0/8', '252.0.0.0/8', '253.0.0.0/8', '254.0.0.0/8', '255.0.0.0/8']
44 |
45 |
46 | try:
47 | db = MySQLdb.connect(dbAddress, dbUserName, dbPassword, dbName)
48 | except:
49 | print "Error while accessing the database"
50 |
51 | cursor = db.cursor()
52 |
53 | #Fetch all reports that are neede for the bubble chart
54 | cursor.execute("select * from report inner join rptrecord on report.serial=rptrecord.serial")
55 | queryResult = cursor.fetchall()
56 |
57 |
58 | #Parse result and count total volume and successful authentication results.
59 | for x in queryResult:
60 | ip = x[15]
61 | dkimResult = x[21]
62 | spfResult = x[23]
63 | count = x[17]
64 |
65 |
66 | # Categorize the IP address from the report into one of the 256 IP chunks.
67 | ipSpace = str(ne.all_matching_cidrs(ip, ipBlocks)[0])
68 |
69 | try:
70 | volume = ipVolume[ipSpace]
71 | dmarcPassCount = volume[0]
72 | volumeCount = volume[1]
73 |
74 | if dkimResult == "pass" or spfResult == "pass":
75 | dmarcPassCount += count
76 | volumeCount += count
77 | else:
78 | volumeCount += count
79 |
80 | ipVolume[ipSpace] = (dmarcPassCount, volumeCount)
81 |
82 | except:
83 | #Apperently we are the first to insert something for this IP block
84 | if dkimResult == "pass" or spfResult == "pass":
85 | ipVolume[ipSpace] = (count, count)
86 | else:
87 | ipVolume[ipSpace] = (0, count)
88 |
89 |
90 |
91 | #Calculate the ratio of each IP chunk
92 | for key, value in ipVolume.iteritems():
93 | dmarcPass = value[0]
94 | totalCount = value[1]
95 |
96 | ratio = dmarcPass / totalCount
97 | ipVolume[key] = (dmarcPass, totalCount , ratio)
98 |
99 |
100 | fig, ax = plt.subplots()
101 |
102 |
103 | xAxis = []
104 | yAxis = []
105 | volume = []
106 | ip = []
107 | ratioList = []
108 | chunkIP = None
109 | chunkRatio = None
110 | chunkVolume = None
111 |
112 |
113 | #Comment in this section for creating real bubble chart
114 | """for key, value in ipVolume.iteritems():
115 |
116 | chunkIP = key.partition(".")[0]
117 | chunkVolume = value[1]
118 | chunkRatio = value[2]
119 |
120 |
121 | print int(chunkIP)
122 |
123 | ip.append(chunkIP)
124 | volume.append(chunkVolume)
125 | ratioList.append(chunkRatio)
126 |
127 | xAxis.append(int(chunkIP))
128 | yAxis.append(chunkRatio)
129 |
130 | ax.annotate(str(chunkIP), (xAxis[-1]+(chunkVolume/600), yAxis[-1]), size=20)
131 |
132 | """
133 |
134 | #For demo purposes only. Comment out and comment in for loop above if
135 | #you want to generate the bubble chart for a real domain
136 | for x in range (0,12):
137 |
138 | ipRandom = random.randrange(0, 256)
139 |
140 | while ipRandom in ip:
141 | ipRandom = random.randrange(0, 256)
142 |
143 | ip.append(ipRandom)
144 | total = random.randrange(0, 10000)
145 |
146 | ratio = random.random()
147 |
148 | ratioList.append(ratio)
149 |
150 | xAxis.append(ipRandom)
151 | yAxis.append(ratio)
152 |
153 | ax.annotate(str(ipRandom), (xAxis[-1]+(total/600), yAxis[-1]), size=20)
154 |
155 | volume.append(total)
156 |
157 | ax.scatter(xAxis, yAxis, s=volume, marker='o', c=ratioList, cmap=plt.cm.RdYlGn)
158 | ax.set_ylim(bottom=0)
159 | ax.set_xlabel('IP chunk')
160 | ax.set_ylabel('Ratio')
161 |
162 | plt.show()
163 |
164 |
--------------------------------------------------------------------------------
/visualizations/heatMap-in.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 17-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that generates a heat map of #
7 | # incoming reports by name. Each tile #
8 | # contains the domain, the total number of #
9 | # email, volume of DMARC passes and DMARC #
10 | # failures. Each tile is color coded with #
11 | # a ratio. #
12 | # This ratio is calculated as: total DMARC #
13 | # passes divided by total email volume. #
14 | # #
15 | # Database variables to be set by user: #
16 | # #
17 | # - dbAddress #
18 | # - dbName #
19 | # - dbUserName #
20 | # - dbPassword #
21 | # #
22 | # #
23 | #################################################
24 |
25 | from __future__ import division
26 | import matplotlib.pyplot as plt
27 | import MySQLdb
28 | import numpy as np
29 | from mpl_toolkits.mplot3d import axes3d, Axes3D
30 | import sys
31 | import random
32 | import string
33 |
34 |
35 | dbAddress = "127.0.0.1"
36 | dbName = "dmarc"
37 | dbUserName = "dmarc"
38 | dbPassword = "dmarcrp2"
39 |
40 |
41 | #Main function that retrieves data from the database and populates all counters.
42 | def getData(dbAddress, dbName, dbUserName, dbPassword):
43 |
44 | domainsOfIntrest = []
45 | domains = []
46 | domainRatioList = []
47 |
48 | try:
49 | db = MySQLdb.connect(dbAddress, dbUserName, dbPassword, dbName)
50 |
51 | except:
52 | print "Error while accessing the database"
53 |
54 |
55 | #Fetch all organization from which reports have been received
56 | cursor = db.cursor()
57 | cursor.execute("select distinct org from report")
58 | queryResult = cursor.fetchall()
59 |
60 | dmarcPass = 0
61 | dmarcFail = 0
62 | totalCount = 0
63 | ratio = 0
64 |
65 |
66 | # Fetch authentication results per organization
67 | for x in queryResult:
68 | domain = x[0]
69 | cursor.execute("select * from report inner join rptrecord on report.serial=rptrecord.serial where org = \"" + domain + "\"")
70 | queryResult = cursor.fetchall()
71 |
72 | for row in queryResult:
73 | dkimResult = row[21]
74 | spfResult = row[23]
75 | count = row[17]
76 |
77 | if dkimResult == "pass" or spfResult == "pass":
78 | dmarcPass += count
79 | else:
80 | dmarcFail += count
81 |
82 | totalCount += count
83 |
84 | ratio = dmarcPass/totalCount
85 | domainRatioList.append((domain, ratio, totalCount, dmarcPass, dmarcFail))
86 |
87 | dmarcPass = 0
88 | dmarcFail = 0
89 | totalCount = 0
90 | ratio = 0
91 |
92 | return domainRatioList
93 |
94 |
95 |
96 | #Function that inserts text values in each tile. Based on:
97 | #http://stackoverflow.com/questions/25071968/heatmap-with-text-in-each-cell-with-matplotlibs-pyplot
98 | def show_values(pc, domainList, fmt="%.2f", **kw):
99 | from itertools import izip
100 | pc.update_scalarmappable()
101 | ax = pc.get_axes()
102 | for p, color, value, ratio in izip(pc.get_paths(), pc.get_facecolors(), pc.get_array(), domainList):
103 | x, y = p.vertices[:-2, :].mean(0)
104 |
105 | #Choose a lite color if we the tile is dark and vice versa.
106 | if np.all(color[:3] > 0.5):
107 | color = (0.0, 0.0, 0.0)
108 | else:
109 | color = (1.0, 1.0, 1.0)
110 | ax.text(x, y+0.15, ratio[0], ha="center", va="center", color=color, size='large', **kw)
111 | ax.text(x, y-0.15, "Total " + str(ratio[2]) + "\n DMARC-P " + str(ratio[3]) + "\n DMARC-F " + str(ratio[4]), ha="center", va="center", color=color, size='x-small', **kw)
112 |
113 |
114 | #Funtion that generates the heatmap from a list of domains and their
115 | #authentication results. Based on:
116 | #http://stackoverflow.com/questions/14391959/heatmap-in-matplotlib-with-pcolor
117 | def generateMap(ratioList):
118 |
119 | #If there are not enough results, populate missing tiles manually. We need
120 | #more elegant solution for this.
121 | while len(ratioList) < 36:
122 | ratioList.append(("",1.0,0,0,0))
123 |
124 | values = []
125 |
126 |
127 | #Uncomment the following for a demo heatmap
128 | """
129 | demo = []
130 |
131 | for x in range (0,36):
132 |
133 | domain = ''.join(random.choice(string.lowercase) for i in range(8)) + ".com"
134 |
135 | total = random.randrange(0, 1000)
136 |
137 | dmarcPass = random.randrange(0,total)
138 |
139 | dmarcFail = total - dmarcPass
140 |
141 | ratio = dmarcPass/total
142 |
143 | demo.append((domain, ratio, total, dmarcPass, dmarcFail ))
144 |
145 | demo = sorted(demo, key=lambda x: x[1])
146 |
147 | ratioList = demo
148 |
149 | """
150 |
151 | ratioList = sorted(ratioList, key=lambda x: x[1])
152 |
153 | for data in ratioList:
154 | values.append(data[1])
155 |
156 |
157 | #Create the tile map
158 | values = np.array(values).reshape((6, 6))
159 |
160 | column_labels = list('ABCD')
161 | row_labels = list('WXYZ')
162 |
163 | data = values
164 | fig, ax = plt.subplots()
165 |
166 | heatmap = ax.pcolor(data, cmap=plt.cm.RdYlGn, vmin = 0.0, vmax = 1.0)
167 | heatmap.update_scalarmappable()
168 |
169 | col = heatmap.get_facecolors()
170 |
171 | show_values(heatmap, ratioList)
172 | plt.colorbar(heatmap)
173 | plt.show()
174 |
175 | ratioList = getData(dbAddress, dbName, dbUserName, dbPassword)
176 | generateMap(ratioList)
177 |
178 |
--------------------------------------------------------------------------------
/visualizations/heatMap-out.py:
--------------------------------------------------------------------------------
1 | #################################################
2 | # Author: Yadvir Singh #
3 | # Date: 17-07-2016 #
4 | # Description: #
5 | # #
6 | # Script that generates a heat map of #
7 | # outgoing reports by name. Each tile #
8 | # contains the domain, #
9 | # the total number of email, volume #
10 | # of DMARC passes and DMARC failures. #
11 | # Each tile is color coded with a ratio. #
12 | # This ratio is calulated as: total DMARC #
13 | # passes divided by total email volume. #
14 | # This script reads a MySQL table that #
15 | # is generated by OpenDMARC #
16 | # #
17 | # Database variables to be set by user: #
18 | # #
19 | # - dbAddress #
20 | # - dbName #
21 | # - dbUserName #
22 | # - dbPassword #
23 | # #
24 | # #
25 | #################################################
26 | from __future__ import division # needed for float division in python < 3
27 | import matplotlib.pyplot as plt
28 | import MySQLdb
29 | import numpy as np
30 | from mpl_toolkits.mplot3d import axes3d, Axes3D
31 | import sys
32 | import random
33 | import string
34 |
35 |
36 | dbAddress = "127.0.0.1"
37 | dbName = "opendmarc"
38 | dbUserName = "dmarc"
39 | dbPassword = "dmarcrp2"
40 |
41 | #Main function that retrieves data from the database and populates all counters.
42 | def getData(dbAddress, dbUserName, dbPassword, dbName):
43 |
44 | domainsOfIntrest = []
45 | domains = []
46 | domainRatioList = []
47 |
48 | #Prepare database connection
49 | try:
50 | db = MySQLdb.connect(dbAddress, dbUserName, dbPassword, dbName)
51 |
52 | except:
53 | print "Error while accessing the database"
54 |
55 |
56 | #Fetch all organization to which reports are sent
57 | cursor = db.cursor()
58 | cursor.execute("select distinct repuri from requests")
59 | queryResult = cursor.fetchall()
60 |
61 | dmarcPass = 0
62 | dmarcFail = 0
63 | totalCount = 0
64 | ratio = 0
65 |
66 | # Fetch authentication results per organization
67 | for x in queryResult:
68 |
69 | domain = x[0]
70 | domains.append(domain)
71 | cursor.execute("SELECT * FROM messages INNER JOIN requests ON messages.env_domain=requests.id where repuri = '" + x[0] + "'")
72 | queryResult = cursor.fetchall()
73 |
74 |
75 | for y in queryResult:
76 |
77 | dkimResult = y[11]
78 | spfResult = y[12]
79 |
80 | if dkimResult == 5 or spfResult == 5:
81 | dmarcPass += 1
82 | else:
83 | dmarcFails += 1
84 |
85 | total = dmarcPass + dmarcFail
86 |
87 | ratio = dmarcPass / total
88 |
89 | domainRatioList.append((domain, ratio, total, dmarcPass, dmarcFail))
90 |
91 | dmarcPass = 0
92 | dmarcFail = 0
93 | totalCount = 0
94 | ratio = 0
95 |
96 | return domainRatioList
97 |
98 |
99 | #Function that inserts text values in each tile. Based on:
100 | #http://stackoverflow.com/questions/25071968/heatmap-with-text-in-each-cell-with-matplotlibs-pyplot
101 | def show_values(pc, domainList, fmt="%.2f", **kw):
102 | from itertools import izip
103 | pc.update_scalarmappable()
104 | ax = pc.get_axes()
105 | for p, color, value, ratio in izip(pc.get_paths(), pc.get_facecolors(), pc.get_array(), domainList):
106 | x, y = p.vertices[:-2, :].mean(0)
107 |
108 | #Choose a lite color if we the tile is dark and vice versa.
109 | if np.all(color[:3] > 0.5):
110 | color = (0.0, 0.0, 0.0)
111 | else:
112 | color = (1.0, 1.0, 1.0)
113 | ax.text(x, y+0.15, ratio[0], ha="center", va="center", color=color, size='large', **kw)
114 | ax.text(x, y-0.15, "Total " + str(ratio[2]) + "\n DMARC-P " + str(ratio[3]) + "\n DMARC-F " + str(ratio[4]), ha="center", va="center", color=color, size='x-small', **kw)
115 |
116 |
117 | #Funtion that generates the heatmap from a list of domains and their
118 | #authentication results. Based on:
119 | #http://stackoverflow.com/questions/14391959/heatmap-in-matplotlib-with-pcolor
120 | def generateMap(ratioList):
121 |
122 | #If there are not enough results, populate missing tiles manually. We need
123 | #more elegant solution for this.
124 | while len(ratioList) < 36:
125 | ratioList.append(("",1.0,0,0,0))
126 |
127 | values = []
128 |
129 | ratioList = sorted(ratioList, key=lambda x: x[1])
130 |
131 | for data in ratioList:
132 | values.append(data[1])
133 |
134 |
135 | #Create the tile map
136 | values = np.array(values).reshape((6, 6))
137 |
138 | column_labels = list('ABCD')
139 | row_labels = list('WXYZ')
140 |
141 | data = values
142 | fig, ax = plt.subplots()
143 |
144 | heatmap = ax.pcolor(data, cmap=plt.cm.RdYlGn, vmin = 0.0, vmax = 1.0)
145 | heatmap.update_scalarmappable()
146 |
147 | col = heatmap.get_facecolors()
148 |
149 | show_values(heatmap, ratioList)
150 | plt.colorbar(heatmap)
151 | plt.show()
152 |
153 | ratioList = getData(dbAddress, dbUserName, dbPassword, dbName)
154 | generateMap(ratioList)
155 |
156 |
157 |
--------------------------------------------------------------------------------