secator 0.15.1__py3-none-any.whl → 0.16.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of secator might be problematic. Click here for more details.

Files changed (106) hide show
  1. secator/celery.py +40 -24
  2. secator/celery_signals.py +71 -68
  3. secator/celery_utils.py +43 -27
  4. secator/cli.py +520 -280
  5. secator/cli_helper.py +394 -0
  6. secator/click.py +87 -0
  7. secator/config.py +67 -39
  8. secator/configs/profiles/http_headless.yaml +6 -0
  9. secator/configs/profiles/http_record.yaml +6 -0
  10. secator/configs/profiles/tor.yaml +1 -1
  11. secator/configs/scans/domain.yaml +4 -2
  12. secator/configs/scans/host.yaml +1 -1
  13. secator/configs/scans/network.yaml +1 -4
  14. secator/configs/scans/subdomain.yaml +13 -1
  15. secator/configs/scans/url.yaml +1 -2
  16. secator/configs/workflows/cidr_recon.yaml +6 -4
  17. secator/configs/workflows/code_scan.yaml +1 -1
  18. secator/configs/workflows/host_recon.yaml +29 -3
  19. secator/configs/workflows/subdomain_recon.yaml +67 -16
  20. secator/configs/workflows/url_crawl.yaml +44 -15
  21. secator/configs/workflows/url_dirsearch.yaml +4 -4
  22. secator/configs/workflows/url_fuzz.yaml +25 -17
  23. secator/configs/workflows/url_params_fuzz.yaml +7 -0
  24. secator/configs/workflows/url_vuln.yaml +33 -8
  25. secator/configs/workflows/user_hunt.yaml +4 -2
  26. secator/configs/workflows/wordpress.yaml +5 -3
  27. secator/cve.py +718 -0
  28. secator/decorators.py +0 -454
  29. secator/definitions.py +49 -30
  30. secator/exporters/_base.py +2 -2
  31. secator/exporters/console.py +2 -2
  32. secator/exporters/table.py +4 -3
  33. secator/exporters/txt.py +1 -1
  34. secator/hooks/mongodb.py +2 -4
  35. secator/installer.py +77 -49
  36. secator/loader.py +116 -0
  37. secator/output_types/_base.py +3 -0
  38. secator/output_types/certificate.py +63 -63
  39. secator/output_types/error.py +4 -5
  40. secator/output_types/info.py +2 -2
  41. secator/output_types/ip.py +3 -1
  42. secator/output_types/progress.py +5 -9
  43. secator/output_types/state.py +17 -17
  44. secator/output_types/tag.py +3 -0
  45. secator/output_types/target.py +10 -2
  46. secator/output_types/url.py +19 -7
  47. secator/output_types/vulnerability.py +11 -7
  48. secator/output_types/warning.py +2 -2
  49. secator/report.py +27 -15
  50. secator/rich.py +18 -10
  51. secator/runners/_base.py +446 -233
  52. secator/runners/_helpers.py +133 -24
  53. secator/runners/command.py +182 -102
  54. secator/runners/scan.py +33 -5
  55. secator/runners/task.py +13 -7
  56. secator/runners/workflow.py +105 -72
  57. secator/scans/__init__.py +2 -2
  58. secator/serializers/dataclass.py +20 -20
  59. secator/tasks/__init__.py +4 -4
  60. secator/tasks/_categories.py +39 -27
  61. secator/tasks/arjun.py +9 -5
  62. secator/tasks/bbot.py +53 -21
  63. secator/tasks/bup.py +19 -5
  64. secator/tasks/cariddi.py +24 -3
  65. secator/tasks/dalfox.py +26 -7
  66. secator/tasks/dirsearch.py +10 -4
  67. secator/tasks/dnsx.py +70 -25
  68. secator/tasks/feroxbuster.py +11 -3
  69. secator/tasks/ffuf.py +42 -6
  70. secator/tasks/fping.py +20 -8
  71. secator/tasks/gau.py +3 -1
  72. secator/tasks/gf.py +3 -3
  73. secator/tasks/gitleaks.py +2 -2
  74. secator/tasks/gospider.py +7 -1
  75. secator/tasks/grype.py +5 -4
  76. secator/tasks/h8mail.py +2 -1
  77. secator/tasks/httpx.py +18 -5
  78. secator/tasks/katana.py +35 -15
  79. secator/tasks/maigret.py +4 -4
  80. secator/tasks/mapcidr.py +3 -3
  81. secator/tasks/msfconsole.py +4 -4
  82. secator/tasks/naabu.py +2 -2
  83. secator/tasks/nmap.py +12 -14
  84. secator/tasks/nuclei.py +3 -3
  85. secator/tasks/searchsploit.py +4 -5
  86. secator/tasks/subfinder.py +2 -2
  87. secator/tasks/testssl.py +264 -263
  88. secator/tasks/trivy.py +5 -5
  89. secator/tasks/wafw00f.py +21 -3
  90. secator/tasks/wpprobe.py +90 -83
  91. secator/tasks/wpscan.py +6 -5
  92. secator/template.py +218 -104
  93. secator/thread.py +15 -15
  94. secator/tree.py +196 -0
  95. secator/utils.py +131 -123
  96. secator/utils_test.py +60 -19
  97. secator/workflows/__init__.py +2 -2
  98. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/METADATA +36 -36
  99. secator-0.16.1.dist-info/RECORD +132 -0
  100. secator/configs/profiles/default.yaml +0 -8
  101. secator/configs/workflows/url_nuclei.yaml +0 -11
  102. secator/tasks/dnsxbrute.py +0 -42
  103. secator-0.15.1.dist-info/RECORD +0 -128
  104. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/WHEEL +0 -0
  105. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/entry_points.txt +0 -0
  106. {secator-0.15.1.dist-info → secator-0.16.1.dist-info}/licenses/LICENSE +0 -0
secator/tasks/nuclei.py CHANGED
@@ -14,8 +14,9 @@ from secator.tasks._categories import VulnMulti
14
14
  class nuclei(VulnMulti):
15
15
  """Fast and customisable vulnerability scanner based on simple YAML based DSL."""
16
16
  cmd = 'nuclei'
17
- tags = ['vuln', 'scan']
18
17
  input_types = [HOST, IP, URL]
18
+ output_types = [Vulnerability, Progress]
19
+ tags = ['vuln', 'scan']
19
20
  file_flag = '-l'
20
21
  input_flag = '-u'
21
22
  json_flag = '-jsonl'
@@ -59,7 +60,6 @@ class nuclei(VulnMulti):
59
60
  'exclude_tags': lambda x: ','.join(x) if isinstance(x, list) else x,
60
61
  }
61
62
  item_loaders = [JSONSerializer()]
62
- output_types = [Vulnerability, Progress]
63
63
  output_map = {
64
64
  Vulnerability: {
65
65
  ID: lambda x: nuclei.id_extractor(x),
@@ -77,7 +77,7 @@ class nuclei(VulnMulti):
77
77
  },
78
78
  Progress: {
79
79
  PERCENT: lambda x: int(x['percent']),
80
- EXTRA_DATA: lambda x: {k: v for k, v in x.items() if k not in ['duration', 'errors', 'percent']}
80
+ EXTRA_DATA: lambda x: {k: v for k, v in x.items() if k not in ['percent']}
81
81
  }
82
82
  }
83
83
  install_pre = {
@@ -3,7 +3,7 @@ import re
3
3
  from secator.config import CONFIG
4
4
  from secator.decorators import task
5
5
  from secator.definitions import (CVES, EXTRA_DATA, ID, MATCHED_AT, NAME,
6
- PROVIDER, REFERENCE, TAGS, TECHNOLOGY, OPT_NOT_SUPPORTED)
6
+ PROVIDER, REFERENCE, TAGS, OPT_NOT_SUPPORTED, STRING)
7
7
  from secator.output_types import Exploit
8
8
  from secator.runners import Command
9
9
  from secator.serializers import JSONSerializer
@@ -16,9 +16,10 @@ SEARCHSPLOIT_TITLE_REGEX = re.compile(r'^((?:[a-zA-Z\-_!\.()]+\d?\s?)+)\.?\s*(.*
16
16
  class searchsploit(Command):
17
17
  """Exploit searcher based on ExploitDB."""
18
18
  cmd = 'searchsploit'
19
+ input_types = [STRING]
20
+ output_types = [Exploit]
19
21
  tags = ['exploit', 'recon']
20
- input_flag = None
21
- input_types = [TECHNOLOGY]
22
+ input_chunk_size = 1
22
23
  json_flag = '--json'
23
24
  version_flag = OPT_NOT_SUPPORTED
24
25
  opts = {
@@ -26,7 +27,6 @@ class searchsploit(Command):
26
27
  }
27
28
  opt_key_map = {}
28
29
  item_loaders = [JSONSerializer()]
29
- output_types = [Exploit]
30
30
  output_map = {
31
31
  Exploit: {
32
32
  NAME: 'Title',
@@ -51,7 +51,6 @@ class searchsploit(Command):
51
51
  proxychains = False
52
52
  proxy_socks5 = False
53
53
  proxy_http = False
54
- input_chunk_size = 1
55
54
  profile = 'io'
56
55
 
57
56
  @staticmethod
@@ -10,8 +10,9 @@ from secator.tasks._categories import ReconDns
10
10
  class subfinder(ReconDns):
11
11
  """Fast passive subdomain enumeration tool."""
12
12
  cmd = 'subfinder -cs'
13
- tags = ['dns', 'recon']
14
13
  input_types = [HOST]
14
+ output_types = [Subdomain]
15
+ tags = ['dns', 'recon']
15
16
  file_flag = '-dL'
16
17
  input_flag = '-d'
17
18
  json_flag = '-json'
@@ -32,7 +33,6 @@ class subfinder(ReconDns):
32
33
  DOMAIN: 'input',
33
34
  }
34
35
  }
35
- output_types = [Subdomain]
36
36
  install_version = 'v2.7.0'
37
37
  install_cmd = 'go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@[install_version]'
38
38
  install_github_handle = 'projectdiscovery/subfinder'
secator/tasks/testssl.py CHANGED
@@ -6,271 +6,272 @@ from secator.config import CONFIG
6
6
  from secator.decorators import task
7
7
  from secator.output_types import Vulnerability, Certificate, Error, Info, Ip, Tag
8
8
  from secator.definitions import (PROXY, HOST, USER_AGENT, HEADER, OUTPUT_PATH,
9
- CERTIFICATE_STATUS_UNKNOWN, CERTIFICATE_STATUS_TRUSTED, CERTIFICATE_STATUS_REVOKED,
10
- TIMEOUT)
9
+ CERTIFICATE_STATUS_UNKNOWN, CERTIFICATE_STATUS_TRUSTED, CERTIFICATE_STATUS_REVOKED,
10
+ TIMEOUT)
11
11
  from secator.tasks._categories import Command, OPTS
12
12
 
13
13
 
14
14
  @task()
15
15
  class testssl(Command):
16
- """SSL/TLS security scanner, including ciphers, protocols and cryptographic flaws."""
17
- cmd = 'testssl.sh'
18
- tags = ['dns', 'recon', 'tls']
19
- input_types = [HOST]
20
- input_flag = None
21
- file_flag = '-iL'
22
- file_eof_newline = True
23
- version_flag = ''
24
- opt_prefix = '--'
25
- opts = {
26
- 'verbose': {'is_flag': True, 'default': False, 'internal': True, 'display': True, 'help': 'Record all SSL/TLS info, not only critical info'}, # noqa: E501
27
- 'parallel': {'is_flag': True, 'default': False, 'help': 'Test multiple hosts in parallel'},
28
- 'warnings': {'type': str, 'default': None, 'help': 'Set to "batch" to stop on errors, and "off" to skip errors and continue'}, # noqa: E501
29
- 'ids_friendly': {'is_flag': True, 'default': False, 'help': 'Avoid IDS blocking by skipping a few vulnerability checks'}, # noqa: E501
30
- 'hints': {'is_flag': True, 'default': False, 'help': 'Additional hints to findings'},
31
- 'server_defaults': {'is_flag': True, 'default': False, 'help': 'Displays the server default picks and certificate info'}, # noqa: E501
32
- }
33
- meta_opts = {
34
- PROXY: OPTS[PROXY],
35
- USER_AGENT: OPTS[USER_AGENT],
36
- HEADER: OPTS[HEADER],
37
- TIMEOUT: OPTS[TIMEOUT],
38
- }
39
- opt_key_map = {
40
- PROXY: 'proxy',
41
- USER_AGENT: 'user-agent',
42
- HEADER: 'reqheader',
43
- TIMEOUT: 'connect-timeout',
44
- 'ipv6': '-6',
45
- }
46
- output_types = [Certificate, Vulnerability, Ip, Tag]
47
- proxy_http = True
48
- proxychains = False
49
- proxy_socks5 = False
50
- profile = 'io'
51
- install_pre = {
52
- 'apk': ['hexdump', 'coreutils', 'procps'],
53
- 'pacman': ['util-linux'],
54
- '*': ['bsdmainutils']
55
- }
56
- install_version = 'v3.2.0'
57
- install_cmd = (
58
- f'git clone --depth 1 --single-branch -b [install_version] https://github.com/drwetter/testssl.sh.git {CONFIG.dirs.share}/testssl.sh_[install_version] || true && ' # noqa: E501
59
- f'ln -sf {CONFIG.dirs.share}/testssl.sh_[install_version]/testssl.sh {CONFIG.dirs.bin}'
60
- )
61
-
62
- @staticmethod
63
- def on_cmd(self):
64
- output_path = self.get_opt_value(OUTPUT_PATH)
65
- if not output_path:
66
- output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
67
- self.output_path = output_path
68
- self.cmd += f' --jsonfile {self.output_path}'
69
-
70
- # Hack because target needs to be the last argument in testssl.sh
71
- if len(self.inputs) == 1:
72
- target = self.inputs[0]
73
- self.cmd = self.cmd.replace(f' {target}', '')
74
- self.cmd += f' {target}'
75
-
76
- @staticmethod
77
- def on_cmd_done(self):
78
- if not os.path.exists(self.output_path):
79
- yield Error(message=f'Could not find JSON results in {self.output_path}')
80
- return
81
- yield Info(message=f'JSON results saved to {self.output_path}')
82
-
83
- verbose = self.get_opt_value('verbose')
84
- with open(self.output_path, 'r') as f:
85
- data = json.load(f)
86
- bad_cyphers = {}
87
- retrieved_certificates = {}
88
- ignored_item_ids = ["scanTime", "overall_grade", "DNS_CAArecord"]
89
- ip_addresses = []
90
- host_to_ips = {}
91
-
92
- for item in data:
93
- host, ip = tuple(item['ip'].split('/'))
94
- id = item['id']
95
- # port = item['port']
96
- finding = item['finding']
97
- severity = item['severity'].lower()
98
- cwe = item.get('cwe')
99
- vuln_tags = ['ssl', 'tls']
100
- if cwe:
101
- vuln_tags.append(cwe)
102
-
103
- # Skip ignored items
104
- if id.startswith(tuple(ignored_item_ids)):
105
- continue
106
-
107
- # Add IP to address pool
108
- host_to_ips.setdefault(host, []).append(ip)
109
- if ip not in ip_addresses:
110
- ip_addresses.append(ip)
111
- yield Ip(
112
- host=host,
113
- ip=ip,
114
- alive=True
115
- )
116
-
117
- # Process errors
118
- if id.startswith("scanProblem"):
119
- yield Error(message=finding)
120
-
121
- # Process bad ciphers
122
- elif id.startswith('cipher-'):
123
- splited_item = item["finding"].split(" ")
124
- concerned_protocol = splited_item[0]
125
- bad_cypher = splited_item[-1]
126
- bad_cyphers.setdefault(ip, {}).setdefault(concerned_protocol, []).append(bad_cypher) # noqa: E501
127
-
128
- # Process certificates
129
- elif id.startswith('cert_') or id.startswith('cert '):
130
- retrieved_certificates.setdefault(ip, []).append(item)
131
-
132
- # Process intermediate certificates
133
- elif id.startswith('intermediate_cert_'):
134
- # TODO: implement this
135
- pass
136
-
137
- # If info or ok, create a tag only if 'verbose' option is set
138
- elif severity in ['info', 'ok']:
139
- if not verbose:
140
- continue
141
- yield Tag(
142
- name=f'SSL/TLS [{id}]',
143
- match=host,
144
- extra_data={
145
- 'type': id,
146
- 'finding': finding,
147
- }
148
- )
149
-
150
- # Create vulnerability
151
- else:
152
- if id in ['TLS1', 'TLS1_1']:
153
- human_name = f'SSL/TLS deprecated protocol offered: {id}'
154
- else:
155
- human_name = f'SSL/TLS {id}: {finding}'
156
- yield Vulnerability(
157
- name=human_name,
158
- matched_at=host,
159
- ip=ip,
160
- tags=vuln_tags,
161
- severity=severity,
162
- confidence='high',
163
- extra_data={
164
- 'id': id,
165
- 'finding': finding
166
- }
167
- )
168
-
169
- # Creating vulnerability for the deprecated ciphers
170
- for ip, protocols in bad_cyphers.items():
171
- for protocol, cyphers in protocols.items():
172
- yield Vulnerability(
173
- name=f'SSL/TLS vulnerability ciphers for {protocol} deprecated',
174
- matched_at=ip,
175
- ip=ip,
176
- confidence='high',
177
- severity='low',
178
- extra_data={
179
- 'cyphers': cyphers
180
- }
181
- )
182
-
183
- # Creating certificates for each founded target
184
- host_to_ips = {k: set(v) for k, v in host_to_ips.items()}
185
- for ip, certs in retrieved_certificates.items():
186
- host = [k for k, v in host_to_ips.items() if ip in v][0]
187
- cert_data = {
188
- 'host': host,
189
- 'ip': ip,
190
- 'fingerprint_sha256': None,
191
- 'subject_cn': None,
192
- 'subject_an': None,
193
- 'not_before': None,
194
- 'not_after': None,
195
- 'issuer_cn': None,
196
- 'self_signed': None,
197
- 'trusted': None,
198
- 'status': None,
199
- 'keysize': None,
200
- 'serial_number': None,
201
- }
202
- for cert in certs:
203
- host = [k for k, v in host_to_ips.items() if ip in v][0]
204
- id = cert['id']
205
- finding = cert['finding']
206
-
207
- if id.startswith('cert_crlDistributionPoints') and finding != '--':
208
- # TODO not implemented, need to find a certificate that is revoked by CRL
209
- cert_data['status'] = CERTIFICATE_STATUS_UNKNOWN
210
-
211
- if id.startswith('cert_ocspRevoked'):
212
- if finding.startswith('not revoked'):
213
- cert_data['status'] = CERTIFICATE_STATUS_TRUSTED
214
- else:
215
- cert_data['status'] = CERTIFICATE_STATUS_REVOKED
216
-
217
- if id.startswith('cert_fingerprintSHA256'):
218
- cert_data['fingerprint_sha256'] = finding
219
-
220
- if id.startswith('cert_commonName'):
221
- cert_data['subject_cn'] = finding
222
-
223
- if id.startswith('cert_subjectAltName'):
224
- cert_data['subject_an'] = finding.split(" ")
225
-
226
- if id.startswith('cert_notBefore'):
227
- cert_data['not_before'] = datetime.strptime(finding, "%Y-%m-%d %H:%M")
228
-
229
- if id.startswith('cert_notAfter'):
230
- cert_data['not_after'] = datetime.strptime(finding, "%Y-%m-%d %H:%M")
231
-
232
- if id.startswith('cert_caIssuers'):
233
- cert_data['issuer_cn'] = finding
234
-
235
- if id.startswith('cert_chain_of_trust'):
236
- cert_data['self_signed'] = 'self signed' in finding
237
-
238
- if id.startswith('cert_chain_of_trust'):
239
- cert_data['trusted'] = finding.startswith('passed')
240
-
241
- if id.startswith('cert_keySize'):
242
- cert_data['keysize'] = int(finding.split(" ")[1])
243
-
244
- if id.startswith('cert_serialNumber'):
245
- cert_data['serial_number'] = finding
246
-
247
- if id.startswith('cert ') and finding.startswith('-----BEGIN CERTIFICATE-----'):
248
- cert_data['raw_value'] = finding
249
-
250
- # For the following attributes commented, it's because at the time of writting it
251
- # I did not found the value inside the result of testssl
252
- cert = Certificate(
253
- **cert_data
254
- # issuer_dn='',
255
- # issuer='',
256
- # TODO: delete the ciphers attribute from certificate outputType
257
- # ciphers=None,
258
- # TODO: need to find a way to retrieve the parent certificate,
259
- # parent_certificate=None,
260
- )
261
- yield cert
262
- if cert.is_expired():
263
- yield Vulnerability(
264
- name='SSL certificate expired',
265
- provider='testssl',
266
- description='The SSL certificate is expired. This can easily lead to domain takeovers',
267
- matched_at=host,
268
- ip=ip,
269
- tags=['ssl', 'tls'],
270
- severity='medium',
271
- confidence='high',
272
- extra_data={
273
- 'id': id,
274
- 'expiration_date': Certificate.format_date(cert.not_after)
275
- }
276
- )
16
+ """SSL/TLS security scanner, including ciphers, protocols and cryptographic flaws."""
17
+ cmd = 'testssl.sh'
18
+ input_types = [HOST]
19
+ output_types = [Certificate, Vulnerability, Ip, Tag]
20
+ tags = ['dns', 'recon', 'tls']
21
+ input_flag = None
22
+ file_flag = '-iL'
23
+ file_eof_newline = True
24
+ version_flag = ''
25
+ opt_prefix = '--'
26
+ opts = {
27
+ 'verbose': {'is_flag': True, 'default': False, 'internal': True, 'display': True, 'help': 'Record all SSL/TLS info, not only critical info'}, # noqa: E501
28
+ 'parallel': {'is_flag': True, 'default': False, 'help': 'Test multiple hosts in parallel'},
29
+ 'warnings': {'type': str, 'default': None, 'help': 'Set to "batch" to stop on errors, and "off" to skip errors and continue'}, # noqa: E501
30
+ 'ids_friendly': {'is_flag': True, 'default': False, 'help': 'Avoid IDS blocking by skipping a few vulnerability checks'}, # noqa: E501
31
+ 'hints': {'is_flag': True, 'default': False, 'help': 'Additional hints to findings'},
32
+ 'server_defaults': {'is_flag': True, 'default': False, 'help': 'Displays the server default picks and certificate info'}, # noqa: E501
33
+ }
34
+ meta_opts = {
35
+ PROXY: OPTS[PROXY],
36
+ USER_AGENT: OPTS[USER_AGENT],
37
+ HEADER: OPTS[HEADER],
38
+ TIMEOUT: OPTS[TIMEOUT],
39
+ }
40
+ opt_key_map = {
41
+ PROXY: 'proxy',
42
+ USER_AGENT: 'user-agent',
43
+ HEADER: 'reqheader',
44
+ TIMEOUT: 'connect-timeout',
45
+ 'ipv6': '-6',
46
+ }
47
+ proxy_http = True
48
+ proxychains = False
49
+ proxy_socks5 = False
50
+ profile = 'io'
51
+ install_pre = {
52
+ 'apk': ['hexdump', 'coreutils', 'procps'],
53
+ 'pacman': ['util-linux'],
54
+ '*': ['bsdmainutils']
55
+ }
56
+ install_github_handle = 'testssl/testssl.sh'
57
+ install_version = 'v3.2.0'
58
+ install_cmd = (
59
+ f'git clone --depth 1 --single-branch -b [install_version] https://github.com/drwetter/testssl.sh.git {CONFIG.dirs.share}/testssl.sh_[install_version] || true && ' # noqa: E501
60
+ f'ln -sf {CONFIG.dirs.share}/testssl.sh_[install_version]/testssl.sh {CONFIG.dirs.bin}'
61
+ )
62
+
63
+ @staticmethod
64
+ def on_cmd(self):
65
+ output_path = self.get_opt_value(OUTPUT_PATH)
66
+ if not output_path:
67
+ output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
68
+ self.output_path = output_path
69
+ self.cmd += f' --jsonfile {self.output_path}'
70
+
71
+ # Hack because target needs to be the last argument in testssl.sh
72
+ if len(self.inputs) == 1:
73
+ target = self.inputs[0]
74
+ self.cmd = self.cmd.replace(f' {target}', '')
75
+ self.cmd += f' {target}'
76
+
77
+ @staticmethod
78
+ def on_cmd_done(self):
79
+ if not os.path.exists(self.output_path):
80
+ yield Error(message=f'Could not find JSON results in {self.output_path}')
81
+ return
82
+ yield Info(message=f'JSON results saved to {self.output_path}')
83
+
84
+ verbose = self.get_opt_value('verbose')
85
+ with open(self.output_path, 'r') as f:
86
+ data = json.load(f)
87
+ bad_cyphers = {}
88
+ retrieved_certificates = {}
89
+ ignored_item_ids = ["scanTime", "overall_grade", "DNS_CAArecord"]
90
+ ip_addresses = []
91
+ host_to_ips = {}
92
+
93
+ for item in data:
94
+ host, ip = tuple(item['ip'].split('/'))
95
+ id = item['id']
96
+ # port = item['port']
97
+ finding = item['finding']
98
+ severity = item['severity'].lower()
99
+ cwe = item.get('cwe')
100
+ vuln_tags = ['ssl', 'tls']
101
+ if cwe:
102
+ vuln_tags.append(cwe)
103
+
104
+ # Skip ignored items
105
+ if id.startswith(tuple(ignored_item_ids)):
106
+ continue
107
+
108
+ # Add IP to address pool
109
+ host_to_ips.setdefault(host, []).append(ip)
110
+ if ip not in ip_addresses:
111
+ ip_addresses.append(ip)
112
+ yield Ip(
113
+ host=host,
114
+ ip=ip,
115
+ alive=True
116
+ )
117
+
118
+ # Process errors
119
+ if id.startswith("scanProblem"):
120
+ yield Error(message=finding)
121
+
122
+ # Process bad ciphers
123
+ elif id.startswith('cipher-'):
124
+ splited_item = item["finding"].split(" ")
125
+ concerned_protocol = splited_item[0]
126
+ bad_cypher = splited_item[-1]
127
+ bad_cyphers.setdefault(ip, {}).setdefault(concerned_protocol, []).append(bad_cypher) # noqa: E501
128
+
129
+ # Process certificates
130
+ elif id.startswith('cert_') or id.startswith('cert '):
131
+ retrieved_certificates.setdefault(ip, []).append(item)
132
+
133
+ # Process intermediate certificates
134
+ elif id.startswith('intermediate_cert_'):
135
+ # TODO: implement this
136
+ pass
137
+
138
+ # If info or ok, create a tag only if 'verbose' option is set
139
+ elif severity in ['info', 'ok']:
140
+ if not verbose:
141
+ continue
142
+ yield Tag(
143
+ name=f'SSL/TLS [{id}]',
144
+ match=host,
145
+ extra_data={
146
+ 'type': id,
147
+ 'finding': finding,
148
+ }
149
+ )
150
+
151
+ # Create vulnerability
152
+ else:
153
+ if id in ['TLS1', 'TLS1_1']:
154
+ human_name = f'SSL/TLS deprecated protocol offered: {id}'
155
+ else:
156
+ human_name = f'SSL/TLS {id}: {finding}'
157
+ yield Vulnerability(
158
+ name=human_name,
159
+ matched_at=host,
160
+ ip=ip,
161
+ tags=vuln_tags,
162
+ severity=severity,
163
+ confidence='high',
164
+ extra_data={
165
+ 'id': id,
166
+ 'finding': finding
167
+ }
168
+ )
169
+
170
+ # Creating vulnerability for the deprecated ciphers
171
+ for ip, protocols in bad_cyphers.items():
172
+ for protocol, cyphers in protocols.items():
173
+ yield Vulnerability(
174
+ name=f'SSL/TLS vulnerability ciphers for {protocol} deprecated',
175
+ matched_at=ip,
176
+ ip=ip,
177
+ confidence='high',
178
+ severity='low',
179
+ extra_data={
180
+ 'cyphers': cyphers
181
+ }
182
+ )
183
+
184
+ # Creating certificates for each founded target
185
+ host_to_ips = {k: set(v) for k, v in host_to_ips.items()}
186
+ for ip, certs in retrieved_certificates.items():
187
+ host = [k for k, v in host_to_ips.items() if ip in v][0]
188
+ cert_data = {
189
+ 'host': host,
190
+ 'ip': ip,
191
+ 'fingerprint_sha256': None,
192
+ 'subject_cn': None,
193
+ 'subject_an': None,
194
+ 'not_before': None,
195
+ 'not_after': None,
196
+ 'issuer_cn': None,
197
+ 'self_signed': None,
198
+ 'trusted': None,
199
+ 'status': None,
200
+ 'keysize': None,
201
+ 'serial_number': None,
202
+ }
203
+ for cert in certs:
204
+ host = [k for k, v in host_to_ips.items() if ip in v][0]
205
+ id = cert['id']
206
+ finding = cert['finding']
207
+
208
+ if id.startswith('cert_crlDistributionPoints') and finding != '--':
209
+ # TODO not implemented, need to find a certificate that is revoked by CRL
210
+ cert_data['status'] = CERTIFICATE_STATUS_UNKNOWN
211
+
212
+ if id.startswith('cert_ocspRevoked'):
213
+ if finding.startswith('not revoked'):
214
+ cert_data['status'] = CERTIFICATE_STATUS_TRUSTED
215
+ else:
216
+ cert_data['status'] = CERTIFICATE_STATUS_REVOKED
217
+
218
+ if id.startswith('cert_fingerprintSHA256'):
219
+ cert_data['fingerprint_sha256'] = finding
220
+
221
+ if id.startswith('cert_commonName'):
222
+ cert_data['subject_cn'] = finding
223
+
224
+ if id.startswith('cert_subjectAltName'):
225
+ cert_data['subject_an'] = finding.split(" ")
226
+
227
+ if id.startswith('cert_notBefore'):
228
+ cert_data['not_before'] = datetime.strptime(finding, "%Y-%m-%d %H:%M")
229
+
230
+ if id.startswith('cert_notAfter'):
231
+ cert_data['not_after'] = datetime.strptime(finding, "%Y-%m-%d %H:%M")
232
+
233
+ if id.startswith('cert_caIssuers'):
234
+ cert_data['issuer_cn'] = finding
235
+
236
+ if id.startswith('cert_chain_of_trust'):
237
+ cert_data['self_signed'] = 'self signed' in finding
238
+
239
+ if id.startswith('cert_chain_of_trust'):
240
+ cert_data['trusted'] = finding.startswith('passed')
241
+
242
+ if id.startswith('cert_keySize'):
243
+ cert_data['keysize'] = int(finding.split(" ")[1])
244
+
245
+ if id.startswith('cert_serialNumber'):
246
+ cert_data['serial_number'] = finding
247
+
248
+ if id.startswith('cert ') and finding.startswith('-----BEGIN CERTIFICATE-----'):
249
+ cert_data['raw_value'] = finding
250
+
251
+ # For the following attributes commented, it's because at the time of writting it
252
+ # I did not found the value inside the result of testssl
253
+ cert = Certificate(
254
+ **cert_data
255
+ # issuer_dn='',
256
+ # issuer='',
257
+ # TODO: delete the ciphers attribute from certificate outputType
258
+ # ciphers=None,
259
+ # TODO: need to find a way to retrieve the parent certificate,
260
+ # parent_certificate=None,
261
+ )
262
+ yield cert
263
+ if cert.is_expired():
264
+ yield Vulnerability(
265
+ name='SSL certificate expired',
266
+ provider='testssl',
267
+ description='The SSL certificate is expired. This can easily lead to domain takeovers',
268
+ matched_at=host,
269
+ ip=ip,
270
+ tags=['ssl', 'tls'],
271
+ severity='medium',
272
+ confidence='high',
273
+ extra_data={
274
+ 'id': id,
275
+ 'expiration_date': Certificate.format_date(cert.not_after)
276
+ }
277
+ )