forked from nabla-c0d3/sslyze
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsslyze_cli.py
executable file
·679 lines (551 loc) · 29.3 KB
/
sslyze_cli.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
import json
import os
import sys
from sslyze import __version__, PROJECT_URL
# Add ./lib to the path for importing nassl for non-frozen builds
if not hasattr(sys,"frozen"):
sys.path.insert(1, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
import re
import signal
from multiprocessing import freeze_support
from time import time
from xml.dom import minidom
from xml.etree.ElementTree import Element, tostring
from sslyze.plugins_process_pool import PluginsProcessPool
from sslyze.plugins_finder import PluginsFinder
from sslyze.server_connectivity import ClientAuthenticationServerConfigurationEnum
from optparse import OptionParser, OptionGroup
from nassl import SSL_FILETYPE_ASN1, SSL_FILETYPE_PEM
from sslyze.server_connectivity import ServerConnectivityInfo, ServerConnectivityError, ServersConnectivityTester
from sslyze.ssl_settings import TlsWrappedProtocolEnum, ClientAuthenticationCredentials, HttpConnectTunnelingSettings
# Global so we can terminate processes when catching SIGINT
plugins_process_pool = None
# Command line parsing utils
class CommandLineParsingError(Exception):
PARSING_ERROR_FORMAT = ' Command line error: {0}\n Use -h for help.'
def get_error_msg(self):
return self.PARSING_ERROR_FORMAT.format(self)
class CommandLineParser(object):
# Defines what --regular means
REGULAR_CMD = ['sslv2', 'sslv3', 'tlsv1', 'tlsv1_1', 'tlsv1_2', 'reneg', 'resum', 'certinfo_basic', 'http_get',
'hide_rejected_ciphers', 'compression', 'heartbleed', 'openssl_ccs', 'fallback']
SSLYZE_USAGE = 'usage: %prog [options] target1.com target2.com:443 target3.com:443{ip} etc...'
# StartTLS options
START_TLS_PROTOCOLS = ['smtp', 'xmpp', 'xmpp_server', 'pop3', 'ftp', 'imap', 'ldap', 'rdp', 'postgres', 'auto']
START_TLS_USAGE = 'STARTTLS should be one of: {}. The \'auto\' option will cause SSLyze to deduce the protocol ' \
'(ftp, imap, etc.) from the supplied port number, ' \
'for each target servers.'.format(' , '.join(START_TLS_PROTOCOLS))
# Mapping of StartTls protocols and ports; useful for starttls=auto
STARTTLS_PROTOCOL_DICT = {'smtp': TlsWrappedProtocolEnum.STARTTLS_SMTP,
587: TlsWrappedProtocolEnum.STARTTLS_SMTP,
25: TlsWrappedProtocolEnum.STARTTLS_SMTP,
'xmpp': TlsWrappedProtocolEnum.STARTTLS_XMPP,
5222 : TlsWrappedProtocolEnum.STARTTLS_XMPP,
'xmpp_server': TlsWrappedProtocolEnum.STARTTLS_XMPP_SERVER,
5269: TlsWrappedProtocolEnum.STARTTLS_XMPP_SERVER,
'pop3': TlsWrappedProtocolEnum.STARTTLS_POP3,
109: TlsWrappedProtocolEnum.STARTTLS_POP3,
110: TlsWrappedProtocolEnum.STARTTLS_POP3,
'imap': TlsWrappedProtocolEnum.STARTTLS_IMAP,
143: TlsWrappedProtocolEnum.STARTTLS_IMAP,
220: TlsWrappedProtocolEnum.STARTTLS_IMAP,
'ftp': TlsWrappedProtocolEnum.STARTTLS_FTP,
21: TlsWrappedProtocolEnum.STARTTLS_FTP,
'ldap': TlsWrappedProtocolEnum.STARTTLS_LDAP,
3268: TlsWrappedProtocolEnum.STARTTLS_LDAP,
389: TlsWrappedProtocolEnum.STARTTLS_LDAP,
'rdp': TlsWrappedProtocolEnum.STARTTLS_RDP,
3389: TlsWrappedProtocolEnum.STARTTLS_RDP,
'postgres': TlsWrappedProtocolEnum.STARTTLS_POSTGRES,
5432: TlsWrappedProtocolEnum.STARTTLS_POSTGRES}
# Default values
DEFAULT_RETRY_ATTEMPTS = 4
DEFAULT_TIMEOUT = 5
def __init__(self, available_plugins, sslyze_version):
"""Generates SSLyze's command line parser.
"""
self._parser = OptionParser(version=sslyze_version, usage=self.SSLYZE_USAGE)
# Add generic command line options to the parser
self._add_default_options()
# Add plugin-specific options to the parser
self._add_plugin_options(available_plugins)
# Add the --regular command line parameter as a shortcut if possible
regular_help = 'Regular HTTPS scan; shortcut for --{}'.format(' --'.join(self.REGULAR_CMD))
self._parser.add_option('--regular', action="store_true", dest=None, help=regular_help)
def parse_command_line(self):
"""Parses the command line used to launch SSLyze.
"""
(args_command_list, args_target_list) = self._parser.parse_args()
# Handle the --targets_in command line and fill args_target_list
if args_command_list.targets_in:
if args_target_list:
raise CommandLineParsingError("Cannot use --targets_list and specify targets within the command line.")
try: # Read targets from a file
with open(args_command_list.targets_in) as f:
for target in f.readlines():
if target.strip(): # Ignore empty lines
if not target.startswith('#'): # Ignore comment lines
args_target_list.append(target.strip())
except IOError:
raise CommandLineParsingError("Can't read targets from input file '{}.".format(
args_command_list.targets_in))
if not args_target_list:
raise CommandLineParsingError('No targets to scan.')
# Handle the --regular command line parameter as a shortcut
if self._parser.has_option('--regular'):
if getattr(args_command_list, 'regular'):
setattr(args_command_list, 'regular', False)
for cmd in self.REGULAR_CMD:
setattr(args_command_list, cmd, True)
# Sanity checks on the command line options
# Prevent --quiet and --xml_out -
if args_command_list.xml_file and args_command_list.xml_file == '-' and args_command_list.quiet:
raise CommandLineParsingError('Cannot use --quiet with --xml_out -.')
# Prevent --quiet and --json_out -
if args_command_list.json_file and args_command_list.json_file == '-' and args_command_list.quiet:
raise CommandLineParsingError('Cannot use --quiet with --json_out -.')
# Prevent --xml_out - and --json_out -
if args_command_list.json_file and args_command_list.json_file == '-'\
and args_command_list.xml_file and args_command_list.xml_file == '-'.quiet:
raise CommandLineParsingError('Cannot use --xml_out - with --json_out -.')
# Sanity checks on the client cert options
client_auth_creds = None
if bool(args_command_list.cert) ^ bool(args_command_list.key):
raise CommandLineParsingError('No private key or certificate file were given. See --cert and --key.')
elif args_command_list.cert:
# Private key formats
if args_command_list.keyform == 'DER':
key_type = SSL_FILETYPE_ASN1
elif args_command_list.keyform == 'PEM':
key_type = SSL_FILETYPE_PEM
else:
raise CommandLineParsingError('--keyform should be DER or PEM.')
# Let's try to open the cert and key files
try:
client_auth_creds = ClientAuthenticationCredentials(args_command_list.cert,
args_command_list.key,
key_type,
args_command_list.keypass)
except ValueError as e:
raise CommandLineParsingError('Invalid client authentication settings: {}.'.format(e[0]))
# HTTP CONNECT proxy
http_tunneling_settings = None
if args_command_list.https_tunnel:
try:
http_tunneling_settings = HttpConnectTunnelingSettings.from_url(args_command_list.https_tunnel)
except ValueError as e:
raise CommandLineParsingError('Invalid proxy URL for --https_tunnel: {}.'.format(e[0]))
# STARTTLS
tls_wrapped_protocol = TlsWrappedProtocolEnum.PLAIN_TLS
if args_command_list.starttls:
if args_command_list.starttls not in self.START_TLS_PROTOCOLS:
raise CommandLineParsingError(self.START_TLS_USAGE)
else:
# StartTLS was specified
if args_command_list.starttls in self.STARTTLS_PROTOCOL_DICT.keys():
# Protocol was given in the command line
tls_wrapped_protocol = self.STARTTLS_PROTOCOL_DICT[args_command_list.starttls]
# Number of connection retries
if args_command_list.nb_retries < 1:
raise CommandLineParsingError('Cannot have a number smaller than 1 for --nb_retries.')
# Create the server connectivity info for each specifed servers
# A limitation when using the command line is that only one client_auth_credentials and http_tunneling_settings
# can be specified, for all the servers to scan
good_server_list = []
bad_server_list = []
for server_string in args_target_list:
try:
good_server_list.append(ServerConnectivityInfo.from_command_line(
server_string=server_string,
tls_wrapped_protocol=tls_wrapped_protocol,
tls_server_name_indication=args_command_list.sni,
xmpp_to_hostname=args_command_list.xmpp_to,
client_auth_credentials=client_auth_creds,
http_tunneling_settings=http_tunneling_settings)
)
except ServerConnectivityError as e:
# Will happen for example if the DNS lookup failed or the server string is malformed
bad_server_list.append((server_string, e))
except ValueError as e:
# Will happen for example if xmpp_to is specified for a non-XMPP connection
raise CommandLineParsingError(e[0])
# Command line hacks
# Handle --starttls=auto now that we parsed the server strings
if args_command_list.starttls == 'auto':
for server_info in good_server_list:
# We use the port number to deduce the protocol
if server_info.port in self.STARTTLS_PROTOCOL_DICT.keys():
server_info.tls_wrapped_protocol = self.STARTTLS_PROTOCOL_DICT[server_info.port]
# Handle --http_get now that we parsed the server strings
# Doing it here is hacky as the option is defined within PluginOpenSSLCipherSuites
if args_command_list.http_get:
for server_info in good_server_list:
if server_info.port == 443:
server_info.tls_wrapped_protocol = TlsWrappedProtocolEnum.HTTPS
return good_server_list, bad_server_list, args_command_list
def _add_default_options(self):
"""
Adds default command line options to the parser.
"""
# Client certificate options
clientcert_group = OptionGroup(self._parser,
'Client certificate support', '')
clientcert_group.add_option(
'--cert',
help='Client certificate chain filename. The certificates must be in PEM format and must be sorted '
'starting with the subject\'s client certificate, followed by intermediate CA certificates if '
'applicable.',
dest='cert'
)
clientcert_group.add_option(
'--key',
help= 'Client private key filename.',
dest='key'
)
clientcert_group.add_option(
'--keyform',
help= 'Client private key format. DER or PEM (default).',
dest='keyform',
default='PEM'
)
clientcert_group.add_option(
'--pass',
help= 'Client private key passphrase.',
dest='keypass',
default=''
)
self._parser.add_option_group(clientcert_group)
# XML output
self._parser.add_option(
'--xml_out',
help='Writes the scan results as an XML document to the file XML_FILE. If XML_FILE is set to "-", the XML '
'output will instead be printed to stdout.',
dest='xml_file',
default=None
)
# JSON output
self._parser.add_option(
'--json_out',
help='Writes the scan results as a JSON document to the file JSON_FILE. If JSON_FILE is set to "-", the '
'JSON output will instead be printed to stdout.',
dest='json_file',
default=None
)
# Read targets from input file
self._parser.add_option(
'--targets_in',
help='Reads the list of targets to scan from the file TARGETS_IN. It should contain one host:port per '
'line.',
dest='targets_in',
default=None
)
# Timeout
self._parser.add_option(
'--timeout',
help='Sets the timeout value in seconds used for every socket connection made to the target server(s). '
'Default is {}s.'.format(str(self.DEFAULT_TIMEOUT)),
type='int',
dest='timeout',
default=self.DEFAULT_TIMEOUT
)
# Control connection retry attempts
self._parser.add_option(
'--nb_retries',
help='Sets the number retry attempts for all network connections initiated throughout the scan. Increase '
'this value if you are getting a lot of timeout/connection errors when scanning a specific server. '
'Decrease this value to increase the speed of the scans; results may however return connection errors.'
' Default is {} connection attempts.'.format(str(self.DEFAULT_RETRY_ATTEMPTS)),
type='int',
dest='nb_retries',
default=self.DEFAULT_RETRY_ATTEMPTS
)
# HTTP CONNECT Proxy
self._parser.add_option(
'--https_tunnel',
help='Tunnels all traffic to the target server(s) through an HTTP CONNECT proxy. HTTP_TUNNEL should be the '
'proxy\'s URL: \'http://USER:PW@HOST:PORT/\'. For proxies requiring authentication, only Basic '
'Authentication is supported.',
dest='https_tunnel',
default=None
)
# STARTTLS
self._parser.add_option(
'--starttls',
help='Performs StartTLS handshakes when connecting to the target server(s). ' + self.START_TLS_USAGE,
dest='starttls',
default=None
)
self._parser.add_option(
'--xmpp_to',
help='Optional setting for STARTTLS XMPP. XMPP_TO should be the hostname to be put in the \'to\' attribute '
'of the XMPP stream. Default is the server\'s hostname.',
dest='xmpp_to',
default=None
)
# Server Name Indication
self._parser.add_option(
'--sni',
help='Use Server Name Indication to specify the hostname to connect to. Will only affect TLS 1.0+ '
'connections.',
dest='sni',
default=None
)
# No text output
self._parser.add_option(
'--quiet',
action="store_true",
dest='quiet',
help='Do not output anything to stdout; useful when using --xml_out or --json_out.'
)
def _add_plugin_options(self, available_plugins):
"""Recovers the list of command line options implemented by the available plugins and adds them to the command
line parser.
"""
for plugin_class in available_plugins:
plugin_desc = plugin_class.get_interface()
# Add the current plugin's commands to the parser
group = OptionGroup(self._parser, plugin_desc.title, plugin_desc.description)
for cmd in plugin_desc.get_commands():
group.add_option(cmd)
# Add the current plugin's options to the parser
for option in plugin_desc.get_options():
group.add_option(option)
self._parser.add_option_group(group)
# Todo: Move formatting stuff to another file
SCAN_FORMAT = 'Scan Results For {0}:{1} - {2}:{1}'
def _format_title(title):
return ' {title}\n {underline}\n'.format(title=title.upper(), underline='-' * len(title))
TLS_PROTOCOL_XML_TEXT = {
TlsWrappedProtocolEnum.PLAIN_TLS: 'plainTls',
TlsWrappedProtocolEnum.HTTPS: 'https',
TlsWrappedProtocolEnum.STARTTLS_SMTP: 'startTlsSmtp',
TlsWrappedProtocolEnum.STARTTLS_XMPP: 'startTlsXmpp',
TlsWrappedProtocolEnum.STARTTLS_XMPP_SERVER: 'startTlsXmppServer',
TlsWrappedProtocolEnum.STARTTLS_POP3: 'startTlsPop3',
TlsWrappedProtocolEnum.STARTTLS_IMAP: 'startTlsImap',
TlsWrappedProtocolEnum.STARTTLS_FTP: 'startTlsFtp',
TlsWrappedProtocolEnum.STARTTLS_LDAP: 'startTlsLdap',
TlsWrappedProtocolEnum.STARTTLS_RDP: 'startTlsRdp',
TlsWrappedProtocolEnum.STARTTLS_POSTGRES: 'startTlsPostGres',
}
def _format_xml_target_result(server_info, result_list):
target_attrib = {'host': server_info.hostname,
'ip': server_info.ip_address,
'port': str(server_info.port),
'tlsWrappedProtocol': TLS_PROTOCOL_XML_TEXT[server_info.tls_wrapped_protocol]
}
if server_info.http_tunneling_settings:
# Add proxy settings
target_attrib['httpsTunnelHostname'] = server_info.http_tunneling_settings.hostname
target_attrib['httpsTunnelPort'] = server_info.http_tunneling_settings.port
target_xml = Element('target', attrib=target_attrib)
result_list.sort(key=lambda result: result) # Sort results
for plugin_result in result_list:
target_xml.append(plugin_result.as_xml())
return target_xml
def _format_json_result(server_info, result_list):
dict_final = {'server_info': server_info.__dict__}
dict_command_result = {}
for plugin_result in result_list:
dict_result = plugin_result.__dict__
# Remove the server_info node
dict_result.pop("server_info", None)
# Remove the plugin_command node
plugin_command = dict_result.pop("plugin_command", None)
dict_command_result[plugin_command] = dict_result
dict_final['commands_results'] = dict_command_result
return dict_final
def _format_txt_target_result(server_info, result_list):
target_result_str = u''
for plugin_result in result_list:
# Print the result of each separate command
target_result_str += '\n'
for line in plugin_result.as_text():
target_result_str += line + '\n'
scan_txt = SCAN_FORMAT.format(server_info.hostname, str(server_info.port), server_info.ip_address)
return _format_title(scan_txt) + target_result_str + '\n\n'
def sigint_handler(signum, frame):
print 'Scan interrupted... shutting down.'
if plugins_process_pool:
plugins_process_pool.emergency_shutdown()
sys.exit()
def main():
# For py2exe builds
freeze_support()
# Handle SIGINT to terminate processes
signal.signal(signal.SIGINT, sigint_handler)
start_time = time()
#--PLUGINS INITIALIZATION--
sslyze_plugins = PluginsFinder()
available_plugins = sslyze_plugins.get_plugins()
available_commands = sslyze_plugins.get_commands()
# Create the command line parser and the list of available options
sslyze_parser = CommandLineParser(available_plugins, __version__)
online_servers_list = []
invalid_servers_list = []
# Parse the command line
try:
good_server_list, bad_server_list, args_command_list = sslyze_parser.parse_command_line()
invalid_servers_list.extend(bad_server_list)
except CommandLineParsingError as e:
print e.get_error_msg()
return
should_print_text_results = not args_command_list.quiet and args_command_list.xml_file != '-' \
and args_command_list.json_file != '-'
if should_print_text_results:
print '\n\n\n' + _format_title('Available plugins')
for plugin in available_plugins:
print ' ' + plugin.__name__
print '\n\n'
#--PROCESSES INITIALIZATION--
if args_command_list.https_tunnel:
# Maximum one process to not kill the proxy
plugins_process_pool = PluginsProcessPool(sslyze_plugins, args_command_list.nb_retries,
args_command_list.timeout, max_processes_nb=1)
else:
plugins_process_pool = PluginsProcessPool(sslyze_plugins, args_command_list.nb_retries,
args_command_list.timeout)
#--TESTING SECTION--
# Figure out which hosts are up and fill the task queue with work to do
if should_print_text_results:
print _format_title('Checking host(s) availability')
connectivity_tester = ServersConnectivityTester(good_server_list)
connectivity_tester.start_connectivity_testing()
SERVER_OK_FORMAT = ' {host}:{port:<25} => {ip_address} {client_auth_msg}'
SERVER_INVALID_FORMAT = ' {server_string:<35} => WARNING: {error_msg}; discarding corresponding tasks.'
# Store and print servers we were able to connect to
for server_connectivity_info in connectivity_tester.get_reachable_servers():
online_servers_list.append(server_connectivity_info)
if should_print_text_results:
client_auth_msg = ''
client_auth_requirement = server_connectivity_info.client_auth_requirement
if client_auth_requirement == ClientAuthenticationServerConfigurationEnum.REQUIRED:
client_auth_msg = ' WARNING: Server REQUIRED client authentication, specific plugins will fail.'
elif client_auth_requirement == ClientAuthenticationServerConfigurationEnum.OPTIONAL:
client_auth_msg = ' WARNING: Server requested optional client authentication'
print SERVER_OK_FORMAT.format(host=server_connectivity_info.hostname, port=server_connectivity_info.port,
ip_address=server_connectivity_info.ip_address,
client_auth_msg=client_auth_msg)
# Send tasks to worker processes
for plugin_command in available_commands:
if getattr(args_command_list, plugin_command):
# Get this plugin's options if there's any
plugin_options_dict = {}
for option in available_commands[plugin_command].get_interface().get_options():
# Was this option set ?
if getattr(args_command_list,option.dest):
plugin_options_dict[option.dest] = getattr(args_command_list, option.dest)
plugins_process_pool.queue_plugin_task(server_connectivity_info, plugin_command, plugin_options_dict)
for tentative_server_info, exception in connectivity_tester.get_invalid_servers():
invalid_servers_list.append(('{}:{}'.format(tentative_server_info.hostname, tentative_server_info.port),
exception))
# Print servers we were NOT able to connect to
if should_print_text_results:
for server_string, exception in invalid_servers_list:
if isinstance(exception, ServerConnectivityError):
print SERVER_INVALID_FORMAT.format(server_string=server_string, error_msg=exception.error_msg)
else:
# Unexpected bug in SSLyze
raise exception
print '\n\n'
# Keep track of how many tasks have to be performed for each target
task_num = 0
for command in available_commands:
if getattr(args_command_list, command):
task_num += 1
# --REPORTING SECTION--
# XML output
xml_output_list = []
# Each host has a list of results
result_dict = {}
# We cannot use the server_info object directly as its address will change due to multiprocessing
RESULT_KEY_FORMAT = '{hostname}:{ip_address}:{port}'.format
for server_info in online_servers_list:
result_dict[RESULT_KEY_FORMAT(hostname=server_info.hostname, ip_address=server_info.ip_address,
port=server_info.port)] = []
# Process the results as they come
for plugin_result in plugins_process_pool.get_results():
server_info = plugin_result.server_info
result_dict[RESULT_KEY_FORMAT(hostname=server_info.hostname, ip_address=server_info.ip_address,
port=server_info.port)].append(plugin_result)
result_list = result_dict[RESULT_KEY_FORMAT(hostname=server_info.hostname, ip_address=server_info.ip_address,
port=server_info.port)]
if len(result_list) == task_num:
# Done with this server; print the results and update the xml doc
if args_command_list.xml_file:
xml_output_list.append(_format_xml_target_result(server_info, result_list))
if should_print_text_results:
print _format_txt_target_result(server_info, result_list)
# --TERMINATE--
exec_time = time()-start_time
# Output JSON to a file if needed
if args_command_list.json_file:
json_output = {'total_scan_time': str(exec_time),
'network_timeout': str(args_command_list.timeout),
'network_max_retries': str(args_command_list.nb_retries),
'invalid_targets': [],
'accepted_targets': []}
# Add the list of invalid targets
for server_string, exception in invalid_servers_list:
if isinstance(exception, ServerConnectivityError):
json_output['invalid_targets'].append({server_string: exception.error_msg})
else:
# Unexpected bug in SSLyze
raise exception
# Add the output of the plugins for each server
for host_str, plugin_result_list in result_dict.iteritems():
server_info = plugin_result_list[0].server_info
json_output['accepted_targets'].append(_format_json_result(server_info, plugin_result_list))
final_json_output = json.dumps(json_output, default=lambda o: o.__dict__, sort_keys=True, indent=4)
if args_command_list.json_file == '-':
# Print XML output to the console if needed
print final_json_output
else:
# Otherwise save the XML output to the console
with open(args_command_list.json_file, 'w') as json_file:
json_file.write(final_json_output)
# Output XML doc to a file if needed
if args_command_list.xml_file:
result_xml_attr = {'totalScanTime': str(exec_time),
'networkTimeout': str(args_command_list.timeout),
'networkMaxRetries': str(args_command_list.nb_retries)}
result_xml = Element('results', attrib = result_xml_attr)
# Sort results in alphabetical order to make the XML files (somewhat) diff-able
xml_output_list.sort(key=lambda xml_elem: xml_elem.attrib['host'])
for xml_element in xml_output_list:
result_xml.append(xml_element)
xml_final_doc = Element('document', title="SSLyze Scan Results", SSLyzeVersion=__version__,
SSLyzeWeb=PROJECT_URL)
# Add the list of invalid targets
invalid_targets_xml = Element('invalidTargets')
for server_string, exception in invalid_servers_list:
if isinstance(exception, ServerConnectivityError):
error_xml = Element('invalidTarget', error=exception.error_msg)
error_xml.text = server_string
invalid_targets_xml.append(error_xml)
else:
# Unexpected bug in SSLyze
raise exception
xml_final_doc.append(invalid_targets_xml)
# Add the output of the plugins
xml_final_doc.append(result_xml)
# Remove characters that are illegal for XML
# https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
xml_final_string = tostring(xml_final_doc, encoding='UTF-8')
illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
xml_sanitized_final_string = illegal_xml_chars_RE.sub('', xml_final_string)
# Hack: Prettify the XML file so it's (somewhat) diff-able
xml_final_pretty = minidom.parseString(xml_sanitized_final_string).toprettyxml(indent=" ", encoding="utf-8" )
if args_command_list.xml_file == '-':
# Print XML output to the console if needed
print xml_final_pretty
else:
# Otherwise save the XML output to the console
with open(args_command_list.xml_file, 'w') as xml_file:
xml_file.write(xml_final_pretty)
if should_print_text_results:
print _format_title('Scan Completed in {0:.2f} s'.format(exec_time))
if __name__ == "__main__":
main()