secator 0.22.0__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.
Files changed (150) hide show
  1. secator/.gitignore +162 -0
  2. secator/__init__.py +0 -0
  3. secator/celery.py +453 -0
  4. secator/celery_signals.py +138 -0
  5. secator/celery_utils.py +320 -0
  6. secator/cli.py +2035 -0
  7. secator/cli_helper.py +395 -0
  8. secator/click.py +87 -0
  9. secator/config.py +670 -0
  10. secator/configs/__init__.py +0 -0
  11. secator/configs/profiles/__init__.py +0 -0
  12. secator/configs/profiles/aggressive.yaml +8 -0
  13. secator/configs/profiles/all_ports.yaml +7 -0
  14. secator/configs/profiles/full.yaml +31 -0
  15. secator/configs/profiles/http_headless.yaml +7 -0
  16. secator/configs/profiles/http_record.yaml +8 -0
  17. secator/configs/profiles/insane.yaml +8 -0
  18. secator/configs/profiles/paranoid.yaml +8 -0
  19. secator/configs/profiles/passive.yaml +11 -0
  20. secator/configs/profiles/polite.yaml +8 -0
  21. secator/configs/profiles/sneaky.yaml +8 -0
  22. secator/configs/profiles/tor.yaml +5 -0
  23. secator/configs/scans/__init__.py +0 -0
  24. secator/configs/scans/domain.yaml +31 -0
  25. secator/configs/scans/host.yaml +23 -0
  26. secator/configs/scans/network.yaml +30 -0
  27. secator/configs/scans/subdomain.yaml +27 -0
  28. secator/configs/scans/url.yaml +19 -0
  29. secator/configs/workflows/__init__.py +0 -0
  30. secator/configs/workflows/cidr_recon.yaml +48 -0
  31. secator/configs/workflows/code_scan.yaml +29 -0
  32. secator/configs/workflows/domain_recon.yaml +46 -0
  33. secator/configs/workflows/host_recon.yaml +95 -0
  34. secator/configs/workflows/subdomain_recon.yaml +120 -0
  35. secator/configs/workflows/url_bypass.yaml +15 -0
  36. secator/configs/workflows/url_crawl.yaml +98 -0
  37. secator/configs/workflows/url_dirsearch.yaml +62 -0
  38. secator/configs/workflows/url_fuzz.yaml +68 -0
  39. secator/configs/workflows/url_params_fuzz.yaml +66 -0
  40. secator/configs/workflows/url_secrets_hunt.yaml +23 -0
  41. secator/configs/workflows/url_vuln.yaml +91 -0
  42. secator/configs/workflows/user_hunt.yaml +29 -0
  43. secator/configs/workflows/wordpress.yaml +38 -0
  44. secator/cve.py +718 -0
  45. secator/decorators.py +7 -0
  46. secator/definitions.py +168 -0
  47. secator/exporters/__init__.py +14 -0
  48. secator/exporters/_base.py +3 -0
  49. secator/exporters/console.py +10 -0
  50. secator/exporters/csv.py +37 -0
  51. secator/exporters/gdrive.py +123 -0
  52. secator/exporters/json.py +16 -0
  53. secator/exporters/table.py +36 -0
  54. secator/exporters/txt.py +28 -0
  55. secator/hooks/__init__.py +0 -0
  56. secator/hooks/gcs.py +80 -0
  57. secator/hooks/mongodb.py +281 -0
  58. secator/installer.py +694 -0
  59. secator/loader.py +128 -0
  60. secator/output_types/__init__.py +49 -0
  61. secator/output_types/_base.py +108 -0
  62. secator/output_types/certificate.py +78 -0
  63. secator/output_types/domain.py +50 -0
  64. secator/output_types/error.py +42 -0
  65. secator/output_types/exploit.py +58 -0
  66. secator/output_types/info.py +24 -0
  67. secator/output_types/ip.py +47 -0
  68. secator/output_types/port.py +55 -0
  69. secator/output_types/progress.py +36 -0
  70. secator/output_types/record.py +36 -0
  71. secator/output_types/stat.py +41 -0
  72. secator/output_types/state.py +29 -0
  73. secator/output_types/subdomain.py +45 -0
  74. secator/output_types/tag.py +69 -0
  75. secator/output_types/target.py +38 -0
  76. secator/output_types/url.py +112 -0
  77. secator/output_types/user_account.py +41 -0
  78. secator/output_types/vulnerability.py +101 -0
  79. secator/output_types/warning.py +30 -0
  80. secator/report.py +140 -0
  81. secator/rich.py +130 -0
  82. secator/runners/__init__.py +14 -0
  83. secator/runners/_base.py +1240 -0
  84. secator/runners/_helpers.py +218 -0
  85. secator/runners/celery.py +18 -0
  86. secator/runners/command.py +1178 -0
  87. secator/runners/python.py +126 -0
  88. secator/runners/scan.py +87 -0
  89. secator/runners/task.py +81 -0
  90. secator/runners/workflow.py +168 -0
  91. secator/scans/__init__.py +29 -0
  92. secator/serializers/__init__.py +8 -0
  93. secator/serializers/dataclass.py +39 -0
  94. secator/serializers/json.py +45 -0
  95. secator/serializers/regex.py +25 -0
  96. secator/tasks/__init__.py +8 -0
  97. secator/tasks/_categories.py +487 -0
  98. secator/tasks/arjun.py +113 -0
  99. secator/tasks/arp.py +53 -0
  100. secator/tasks/arpscan.py +70 -0
  101. secator/tasks/bbot.py +372 -0
  102. secator/tasks/bup.py +118 -0
  103. secator/tasks/cariddi.py +193 -0
  104. secator/tasks/dalfox.py +87 -0
  105. secator/tasks/dirsearch.py +84 -0
  106. secator/tasks/dnsx.py +186 -0
  107. secator/tasks/feroxbuster.py +93 -0
  108. secator/tasks/ffuf.py +135 -0
  109. secator/tasks/fping.py +85 -0
  110. secator/tasks/gau.py +102 -0
  111. secator/tasks/getasn.py +60 -0
  112. secator/tasks/gf.py +36 -0
  113. secator/tasks/gitleaks.py +96 -0
  114. secator/tasks/gospider.py +84 -0
  115. secator/tasks/grype.py +109 -0
  116. secator/tasks/h8mail.py +75 -0
  117. secator/tasks/httpx.py +167 -0
  118. secator/tasks/jswhois.py +36 -0
  119. secator/tasks/katana.py +203 -0
  120. secator/tasks/maigret.py +87 -0
  121. secator/tasks/mapcidr.py +42 -0
  122. secator/tasks/msfconsole.py +179 -0
  123. secator/tasks/naabu.py +85 -0
  124. secator/tasks/nmap.py +487 -0
  125. secator/tasks/nuclei.py +151 -0
  126. secator/tasks/search_vulns.py +225 -0
  127. secator/tasks/searchsploit.py +109 -0
  128. secator/tasks/sshaudit.py +299 -0
  129. secator/tasks/subfinder.py +48 -0
  130. secator/tasks/testssl.py +283 -0
  131. secator/tasks/trivy.py +130 -0
  132. secator/tasks/trufflehog.py +240 -0
  133. secator/tasks/urlfinder.py +100 -0
  134. secator/tasks/wafw00f.py +106 -0
  135. secator/tasks/whois.py +34 -0
  136. secator/tasks/wpprobe.py +116 -0
  137. secator/tasks/wpscan.py +202 -0
  138. secator/tasks/x8.py +94 -0
  139. secator/tasks/xurlfind3r.py +83 -0
  140. secator/template.py +294 -0
  141. secator/thread.py +24 -0
  142. secator/tree.py +196 -0
  143. secator/utils.py +922 -0
  144. secator/utils_test.py +297 -0
  145. secator/workflows/__init__.py +29 -0
  146. secator-0.22.0.dist-info/METADATA +447 -0
  147. secator-0.22.0.dist-info/RECORD +150 -0
  148. secator-0.22.0.dist-info/WHEEL +4 -0
  149. secator-0.22.0.dist-info/entry_points.txt +2 -0
  150. secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/tasks/nmap.py ADDED
@@ -0,0 +1,487 @@
1
+ import logging
2
+ import os
3
+ import shlex
4
+ import re
5
+ import xmltodict
6
+
7
+ from secator.config import CONFIG
8
+ from secator.decorators import task
9
+ from secator.definitions import (CONFIDENCE, CIDR_RANGE, CVSS_SCORE, DELAY,
10
+ DESCRIPTION, EXTRA_DATA, FOLLOW_REDIRECT,
11
+ HEADER, HOST, ID, IP, PROTOCOL, MATCHED_AT, NAME,
12
+ OPT_NOT_SUPPORTED, OUTPUT_PATH, PORT, PORTS, PROVIDER,
13
+ PROXY, RATE_LIMIT, REFERENCE, REFERENCES, RETRIES, SCRIPT, SERVICE_NAME,
14
+ SEVERITY, STATE, TAGS, THREADS, TIMEOUT, TOP_PORTS, USER_AGENT)
15
+ from secator.output_types import Exploit, Port, Vulnerability, Info, Error, Ip
16
+ from secator.tasks._categories import ReconPort, VulnMulti
17
+ from secator.utils import debug, traceback_as_string
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @task()
23
+ class nmap(ReconPort):
24
+ """Network Mapper is a free and open source utility for network discovery and security auditing."""
25
+ cmd = 'nmap'
26
+ input_types = [HOST, IP, CIDR_RANGE]
27
+ output_types = [Port, Ip, Vulnerability, Exploit]
28
+ tags = ['port', 'scan']
29
+ input_chunk_size = 1
30
+ file_flag = '-iL'
31
+ opt_prefix = '--'
32
+ opts = {
33
+ # Script scanning
34
+ SCRIPT: {'type': str, 'default': None, 'help': 'NSE scripts'},
35
+ 'script_args': {'type': str, 'short': 'sargs', 'default': None, 'help': 'NSE script arguments (n1=v1,n2=v2,...)'},
36
+
37
+ # Host discovery
38
+ 'skip_host_discovery': {'is_flag': True, 'short': 'Pn', 'default': False, 'help': 'Skip host discovery (no ping)'},
39
+
40
+ # Service and version detection
41
+ 'version_detection': {'is_flag': True, 'short': 'sV', 'default': False, 'help': 'Enable version detection (slow)'},
42
+ 'detect_all': {'is_flag': True, 'short': 'A', 'default': False, 'help': 'Enable OS detection, version detection, script scanning, and traceroute on open ports'}, # noqa: E501
43
+ 'detect_os': {'is_flag': True, 'short': 'O', 'default': False, 'help': 'Enable OS detection', 'requires_sudo': True},
44
+
45
+ # Scan techniques
46
+ 'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth', 'requires_sudo': True}, # noqa: E501
47
+ 'tcp_connect': {'is_flag': True, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
48
+ 'udp_scan': {'is_flag': True, 'short': 'sU', 'default': False, 'help': 'UDP scan', 'requires_sudo': True},
49
+ 'tcp_null_scan': {'is_flag': True, 'short': 'sN', 'default': False, 'help': 'TCP Null scan', 'requires_sudo': True},
50
+ 'tcp_fin_scan': {'is_flag': True, 'short': 'sF', 'default': False, 'help': 'TCP FIN scan', 'requires_sudo': True},
51
+ 'tcp_xmas_scan': {'is_flag': True, 'short': 'sX', 'default': False, 'help': 'TCP Xmas scan', 'requires_sudo': True},
52
+ 'tcp_ack_scan': {'is_flag': True, 'short': 'sA', 'default': False, 'help': 'TCP ACK scan', 'requires_sudo': True},
53
+ 'tcp_window_scan': {'is_flag': True, 'short': 'sW', 'default': False, 'help': 'TCP Window scan', 'requires_sudo': True}, # noqa: E501
54
+ 'tcp_maimon_scan': {'is_flag': True, 'short': 'sM', 'default': False, 'help': 'TCP Maimon scan', 'requires_sudo': True}, # noqa: E501
55
+ 'sctp_init_scan': {'is_flag': True, 'short': 'sY', 'default': False, 'help': 'SCTP Init scan', 'requires_sudo': True},
56
+ 'sctp_cookie_echo_scan': {'is_flag': True, 'short': 'sZ', 'default': False, 'help': 'SCTP Cookie Echo scan', 'requires_sudo': True}, # noqa: E501
57
+ 'ping_scan': {'is_flag': True, 'short': 'sn', 'default': False, 'help': 'Ping scan (disable port scan)'},
58
+ 'ip_protocol_scan': {'type': str, 'short': 'sO', 'default': None, 'help': 'IP protocol scan', 'requires_sudo': True},
59
+ 'script_scan': {'is_flag': True, 'short': 'sC', 'default': False, 'help': 'Enable default scanning'},
60
+ 'zombie_host': {'type': str, 'short': 'sI', 'default': None, 'help': 'Use a zombie host for idle scan', 'requires_sudo': True}, # noqa: E501
61
+ 'ftp_relay_host': {'type': str, 'short': 'sB', 'default': None, 'help': 'FTP bounce scan relay host'},
62
+
63
+ # Firewall / IDS evasion and spoofing
64
+ 'spoof_source_port': {'type': int, 'short': 'g', 'default': None, 'help': 'Send packets from a specific port'},
65
+ 'spoof_source_ip': {'type': str, 'short': 'S', 'default': None, 'help': 'Spoof source IP address'},
66
+ 'spoof_source_mac': {'type': str, 'short': 'spoofmac', 'default': None, 'help': 'Spoof MAC address'},
67
+ 'fragment': {'is_flag': True, 'short': 'fragment', 'default': False, 'help': 'Fragment packets', 'requires_sudo': True}, # noqa: E501
68
+ 'mtu': {'type': int, 'short': 'mtu', 'default': None, 'help': 'Fragment packets with given MTU', 'requires_sudo': True}, # noqa: E501
69
+ 'ttl': {'type': int, 'short': 'ttl', 'default': None, 'help': 'Set TTL', 'requires_sudo': True},
70
+ 'badsum': {'is_flag': True, 'short': 'badsum', 'default': False, 'help': 'Create a bad checksum in the TCP header', 'requires_sudo': True}, # noqa: E501
71
+ 'ipv6': {'is_flag': True, 'short': 'ipv6', 'default': False, 'help': 'Enable IPv6 scanning'},
72
+
73
+ # Host discovery
74
+ 'traceroute': {'is_flag': True, 'short': 'traceroute', 'default': False, 'help': 'Traceroute', 'requires_sudo': True},
75
+ 'disable_arp_ping': {'is_flag': True, 'short': 'dap', 'default': False, 'help': 'Disable ARP ping'},
76
+
77
+ # Misc
78
+ 'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path', 'internal': True, 'display': False}, # noqa: E501
79
+ 'debug': {'is_flag': True, 'short': 'd', 'default': False, 'help': 'Enable debug mode'},
80
+ 'verbose': {'is_flag': True, 'short': 'v', 'default': False, 'help': 'Enable verbose mode'},
81
+ 'timing': {'type': int, 'short': 'T', 'default': None, 'help': 'Timing template (0: paranoid, 1: sneaky, 2: polite, 3: normal, 4: aggressive, 5: insane)'}, # noqa: E501
82
+ }
83
+ opt_key_map = {
84
+ HEADER: OPT_NOT_SUPPORTED,
85
+ DELAY: 'scan-delay',
86
+ FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
87
+ PROXY: None, # TODO: supports --proxies but not in TCP mode [https://github.com/nmap/nmap/issues/1098]
88
+ RATE_LIMIT: 'max-rate',
89
+ RETRIES: 'max-retries',
90
+ THREADS: OPT_NOT_SUPPORTED,
91
+ TIMEOUT: 'max-rtt-timeout',
92
+ USER_AGENT: OPT_NOT_SUPPORTED,
93
+ PORTS: '-p',
94
+ TOP_PORTS: 'top-ports',
95
+
96
+ # Nmap opts
97
+ 'skip_host_discovery': '-Pn',
98
+ 'version_detection': '-sV',
99
+ 'detect_all': '-A',
100
+ 'detect_os': '-O',
101
+ 'tcp_syn_stealth': '-sS',
102
+ 'tcp_connect': '-sT',
103
+ 'tcp_window_scan': '-sW',
104
+ 'tcp_maimon_scan': '-sM',
105
+ 'udp_scan': '-sU',
106
+ 'tcp_null_scan': '-sN',
107
+ 'tcp_fin_scan': '-sF',
108
+ 'tcp_xmas_scan': '-sX',
109
+ 'tcp_ack_scan': '-sA',
110
+ 'sctp_init_scan': '-sY',
111
+ 'sctp_cookie_echo_scan': '-sZ',
112
+ 'ping_scan': '-sn',
113
+ 'ip_protocol_scan': '-sO',
114
+ 'script_scan': '-sC',
115
+ 'zombie_host': '-sI',
116
+ 'ftp_relay_host': '-b',
117
+ 'spoof_source_port': '-g',
118
+ 'spoof_source_ip': '-S',
119
+ 'spoof_source_mac': '--spoof-mac',
120
+ 'fragment': '-f',
121
+ 'mtu': '--mtu',
122
+ 'ttl': '--ttl',
123
+ 'badsum': '--badsum',
124
+ 'ipv6': '-6',
125
+ 'traceroute': '--traceroute',
126
+ 'disable_arp_ping': '--disable-arp-ping',
127
+ 'output_path': '-oX',
128
+ }
129
+ opt_value_map = {
130
+ PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
131
+ }
132
+ install_cmd_pre = {
133
+ 'apt|pacman|brew': ['nmap'],
134
+ 'apk': ['nmap', 'nmap-scripts'],
135
+ }
136
+ install_cmd = (
137
+ 'sudo git clone --depth 1 --single-branch https://github.com/scipag/vulscan /opt/scipag_vulscan || true && '
138
+ 'sudo ln -s /opt/scipag_vulscan /usr/share/nmap/scripts/vulscan || true'
139
+ )
140
+ proxychains = True
141
+ proxychains_flavor = 'proxychains4'
142
+ proxy_socks5 = False
143
+ proxy_http = False
144
+ profile = 'io'
145
+
146
+ @staticmethod
147
+ def on_cmd(self):
148
+ output_path = self.get_opt_value(OUTPUT_PATH)
149
+ if not output_path:
150
+ output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.xml'
151
+ self.output_path = output_path
152
+ self.cmd += f' -oX {shlex.quote(self.output_path)}'
153
+ tcp_syn_stealth = self.cmd_options.get('tcp_syn_stealth')
154
+ tcp_connect = self.cmd_options.get('tcp_connect')
155
+ if tcp_connect and tcp_syn_stealth:
156
+ self._print(
157
+ 'Options -sT (SYN stealth scan) and -sS (CONNECT scan) are conflicting. Keeping only -sS.',
158
+ 'bold gold3')
159
+ self.cmd = self.cmd.replace('-sT ', '')
160
+
161
+ @staticmethod
162
+ def on_cmd_done(self):
163
+ if not os.path.exists(self.output_path):
164
+ yield Error(message=f'Could not find XML results in {self.output_path}')
165
+ return
166
+ yield Info(message=f'XML results saved to {self.output_path}')
167
+ yield from self.xml_to_json()
168
+
169
+ def xml_to_json(self):
170
+ results = []
171
+ with open(self.output_path, 'r') as f:
172
+ content = f.read()
173
+ try:
174
+ results = xmltodict.parse(content) # parse XML to dict
175
+ except Exception as exc:
176
+ yield Error(
177
+ message=f'Cannot parse XML output {self.output_path} to valid JSON.',
178
+ traceback=traceback_as_string(exc)
179
+ )
180
+ yield from nmapData(results)
181
+
182
+
183
+ class nmapData(dict):
184
+
185
+ def __iter__(self):
186
+ datas = []
187
+ ips = []
188
+ for host in self._get_hosts():
189
+ hostname = self._get_hostname(host)
190
+ ip = self._get_ip(host)
191
+ if ip and ip not in ips:
192
+ yield Ip(ip=ip, alive=True, host=hostname, extra_data={'protocol': 'tcp'})
193
+ ips.append(ip)
194
+ for port in self._get_ports(host):
195
+ # Get port number
196
+ port_number = port['@portid']
197
+ if not port_number or not port_number.isdigit():
198
+ continue
199
+ port_number = int(port_number)
200
+
201
+ # Get port state
202
+ state = port.get('state', {}).get('@state', '')
203
+
204
+ # Get extra data
205
+ extra_data = self._get_extra_data(port)
206
+ service_name = extra_data.get('service_name', '')
207
+ version_exact = extra_data.get('version_exact', False)
208
+ conf = extra_data.get('confidence')
209
+
210
+ # Grab CPEs
211
+ cpes = extra_data.get('cpe', [])
212
+
213
+ # Get script output
214
+ scripts = self._get_scripts(port)
215
+
216
+ # Get port protocol
217
+ protocol = port['@protocol'].lower()
218
+
219
+ # Yield port data
220
+ port = {
221
+ PORT: port_number,
222
+ HOST: hostname,
223
+ STATE: state,
224
+ SERVICE_NAME: service_name,
225
+ IP: ip,
226
+ PROTOCOL: protocol,
227
+ EXTRA_DATA: extra_data,
228
+ CONFIDENCE: conf
229
+ }
230
+ yield Port(**port)
231
+
232
+ # Parse each script output to get vulns
233
+ for script in scripts:
234
+ script_id = script['id']
235
+ output = script['output']
236
+ extra_data = {'script': script_id}
237
+ if service_name:
238
+ extra_data['service_name'] = service_name
239
+ funcmap = {
240
+ 'vulscan': self._parse_vulscan_output,
241
+ 'vulners': self._parse_vulners_output,
242
+ }
243
+ func = funcmap.get(script_id)
244
+ metadata = {
245
+ MATCHED_AT: f'{hostname}:{port_number}',
246
+ IP: ip,
247
+ EXTRA_DATA: extra_data,
248
+ }
249
+ if not func:
250
+ debug(f'Script output parser for "{script_id}" is not supported YET.', sub='cve.nmap')
251
+ continue
252
+ for data in func(output, cpes=cpes):
253
+ data.update(metadata)
254
+ confidence = 'low'
255
+ if 'cpe-match' in data[TAGS]:
256
+ confidence = 'high' if version_exact else 'medium'
257
+ data[CONFIDENCE] = confidence
258
+ if (CONFIG.runners.skip_cve_low_confidence and data[CONFIDENCE] == 'low'):
259
+ debug(f'{data[ID]}: ignored (low confidence).', sub='cve.nmap')
260
+ continue
261
+ if data in datas:
262
+ continue
263
+ yield data
264
+ datas.append(data)
265
+
266
+ #---------------------#
267
+ # XML FILE EXTRACTORS #
268
+ #---------------------#
269
+ def _get_hosts(self):
270
+ hosts = self.get('nmaprun', {}).get('host', {})
271
+ if isinstance(hosts, dict):
272
+ hosts = [hosts]
273
+ return hosts
274
+
275
+ def _get_ports(self, host_cfg):
276
+ ports = host_cfg.get('ports', {}).get('port', [])
277
+ if isinstance(ports, dict):
278
+ ports = [ports]
279
+ return ports
280
+
281
+ def _get_hostname(self, host_cfg):
282
+ hostnames = host_cfg.get('hostnames', {})
283
+ if hostnames:
284
+ hostnames = hostnames.get('hostname', [])
285
+ if isinstance(hostnames, dict):
286
+ hostnames = [hostnames]
287
+ if hostnames:
288
+ hostname = hostnames[0]['@name']
289
+ else:
290
+ hostname = self._get_address(host_cfg).get('@addr', None)
291
+ return hostname
292
+
293
+ def _get_address(self, host_cfg):
294
+ if isinstance(host_cfg.get('address', {}), list):
295
+ addresses = host_cfg.get('address', {})
296
+ for address in addresses:
297
+ if address.get('@addrtype') == "ipv4":
298
+ return address
299
+ return host_cfg.get('address', {})
300
+
301
+ def _get_ip(self, host_cfg):
302
+ return self._get_address(host_cfg).get('@addr', None)
303
+
304
+ def _get_extra_data(self, port_cfg):
305
+ extra_data = {
306
+ k.lstrip('@'): v
307
+ for k, v in port_cfg.get('service', {}).items()
308
+ }
309
+
310
+ # Strip product / version strings
311
+ if 'product' in extra_data:
312
+ extra_data['product'] = extra_data['product'].lower()
313
+
314
+ # Get version and post-process it
315
+ version = None
316
+ if 'version' in extra_data:
317
+ vsplit = extra_data['version'].split(' ')
318
+ version_exact = True
319
+ os = None
320
+ if len(vsplit) == 3:
321
+ version, os, extra_version = tuple(vsplit)
322
+ if os == 'or' and extra_version == 'later':
323
+ version_exact = False
324
+ os = None
325
+ version = f'{version}-{extra_version}'
326
+ elif len(vsplit) == 2:
327
+ version, os = tuple(vsplit)
328
+ elif len(vsplit) == 1:
329
+ version = vsplit[0]
330
+ else:
331
+ version = extra_data['version']
332
+ if os:
333
+ extra_data['os'] = os
334
+ if version:
335
+ extra_data['version'] = version
336
+ extra_data['version_exact'] = version_exact
337
+
338
+ # Grap service name
339
+ product = extra_data.get('product', None) or extra_data.get('name', None)
340
+ if product:
341
+ service_name = product
342
+ if version:
343
+ service_name += f'/{version}'
344
+ extra_data['service_name'] = service_name
345
+
346
+ # Grab CPEs
347
+ cpes = extra_data.get('cpe', [])
348
+ if not isinstance(cpes, list):
349
+ cpes = [cpes]
350
+ extra_data['cpe'] = cpes
351
+ debug(f'Found CPEs: {",".join(cpes)}', sub='cve.nmap')
352
+
353
+ # Grab confidence
354
+ conf = int(extra_data.get('conf', 0))
355
+ if conf > 7:
356
+ confidence = 'high'
357
+ elif conf > 4:
358
+ confidence = 'medium'
359
+ else:
360
+ confidence = 'low'
361
+ extra_data['confidence'] = confidence
362
+
363
+ # Build custom CPE
364
+ if product and version:
365
+ vsplit = version.split('-')
366
+ version_cpe = vsplit[0] if not version_exact else version
367
+ cpe = VulnMulti.create_cpe_string(product, version_cpe)
368
+ if cpe not in cpes:
369
+ cpes.append(cpe)
370
+ debug(f'Added new CPE from identified product and version: {cpe}', sub='cve.nmap')
371
+
372
+ return extra_data
373
+
374
+ def _get_scripts(self, port_cfg):
375
+ scripts = port_cfg.get('script', [])
376
+ if isinstance(scripts, dict):
377
+ scripts = [scripts]
378
+ scripts = [
379
+ {k.lstrip('@'): v for k, v in script.items()}
380
+ for script in scripts
381
+ ]
382
+ return scripts
383
+
384
+ #--------------#
385
+ # VULN PARSERS #
386
+ #--------------#
387
+ def _parse_vulscan_output(self, out, cpes=[]):
388
+ """Parse nmap vulscan script output.
389
+
390
+ Args:
391
+ out (str): Vulscan script output.
392
+
393
+ Returns:
394
+ list: List of Vulnerability dicts.
395
+ """
396
+ provider_name = ''
397
+ for line in out.splitlines():
398
+ if not line:
399
+ continue
400
+ line = line.strip()
401
+ if not line.startswith('[') and line != 'No findings': # provider line
402
+ provider_name, _ = tuple(line.split(' - '))
403
+ continue
404
+ reg = r'\[([ A-Za-z0-9_@./#&+-]*)\] (.*)'
405
+ matches = re.match(reg, line)
406
+ if not matches:
407
+ continue
408
+ vuln_id, vuln_title = matches.groups()
409
+ vuln = {
410
+ ID: vuln_id,
411
+ NAME: vuln_id,
412
+ DESCRIPTION: vuln_title,
413
+ PROVIDER: provider_name,
414
+ TAGS: [provider_name]
415
+ }
416
+ if provider_name == 'MITRE CVE':
417
+ data = VulnMulti.lookup_cve(vuln['id'], *cpes)
418
+ if data:
419
+ vuln.update(data)
420
+ yield vuln
421
+ else:
422
+ debug(f'Vulscan provider {provider_name} is not supported YET.', sub='cve.provider', verbose=True)
423
+ continue
424
+
425
+ def _parse_vulners_output(self, out, **kwargs):
426
+ cpes = kwargs.get('cpes', [])
427
+ provider_name = 'vulners'
428
+ for line in out.splitlines():
429
+ if not line:
430
+ continue
431
+ line = line.strip()
432
+ if line.startswith('cpe:'):
433
+ cpes.append(line.rstrip(':'))
434
+ continue
435
+ elems = tuple(line.split('\t'))
436
+
437
+ if len(elems) == 4: # exploit
438
+ exploit_id, cvss_score, reference_url, _ = elems
439
+ name = exploit_id
440
+ # edb_id = name.split(':')[-1] if 'EDB-ID' in name else None
441
+ exploit = {
442
+ ID: exploit_id,
443
+ NAME: name,
444
+ PROVIDER: provider_name,
445
+ REFERENCE: reference_url,
446
+ TAGS: [exploit_id, provider_name],
447
+ CVSS_SCORE: cvss_score,
448
+ CONFIDENCE: 'low',
449
+ '_type': 'exploit',
450
+ }
451
+ # TODO: lookup exploit in ExploitDB to find related CVEs
452
+ # if edb_id:
453
+ # print(edb_id)
454
+ # exploit_data = VulnMulti.lookup_exploitdb(edb_id)
455
+ vuln = VulnMulti.lookup_cve_from_vulners_exploit(exploit_id, *cpes)
456
+ if vuln:
457
+ yield vuln
458
+ exploit[TAGS].extend(vuln[TAGS])
459
+ exploit[CONFIDENCE] = vuln[CONFIDENCE]
460
+ yield exploit
461
+ continue
462
+
463
+ elif len(elems) == 3: # vuln
464
+ vuln = {}
465
+ vuln_id, vuln_cvss, reference_url = tuple(line.split('\t'))
466
+ vuln_cvss = float(vuln_cvss)
467
+ vuln_id = vuln_id.split(':')[-1]
468
+ vuln_type = vuln_id.split('-')[0]
469
+ vuln = {
470
+ ID: vuln_id,
471
+ NAME: vuln_id,
472
+ PROVIDER: provider_name,
473
+ CVSS_SCORE: vuln_cvss,
474
+ SEVERITY: VulnMulti.cvss_to_severity(vuln_cvss),
475
+ REFERENCES: [reference_url],
476
+ TAGS: [vuln_id, provider_name],
477
+ CONFIDENCE: 'low'
478
+ }
479
+ if vuln_type == 'CVE' or vuln_type == 'PRION:CVE':
480
+ data = VulnMulti.lookup_cve(vuln_id, *cpes)
481
+ if data:
482
+ vuln.update(data)
483
+ yield vuln
484
+ else:
485
+ debug(f'Vulners parser for "{vuln_type}" is not implemented YET.', sub='cve.nmap')
486
+ else:
487
+ debug(f'Unrecognized vulners output: {elems}', sub='cve.nmap')
@@ -0,0 +1,151 @@
1
+ from secator.decorators import task
2
+ from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
3
+ EXTRA_DATA, FOLLOW_REDIRECT, HEADER, ID, IP,
4
+ MATCHED_AT, NAME, OPT_NOT_SUPPORTED, PERCENT,
5
+ PROVIDER, PROXY, RATE_LIMIT, REFERENCES,
6
+ RETRIES, SEVERITY, TAGS, THREADS, TIMEOUT,
7
+ USER_AGENT, HOST, URL)
8
+ from secator.output_types import Progress, Tag, Vulnerability
9
+ from secator.serializers import JSONSerializer
10
+ from secator.tasks._categories import VulnMulti
11
+
12
+
13
+ def output_discriminator(self, item):
14
+ """Discriminate between Tag and Vulnerability based on severity."""
15
+ if 'percent' in item:
16
+ return Progress
17
+ severity = item.get('info', {}).get('severity', '').lower()
18
+ if severity == 'info':
19
+ return Tag
20
+ return Vulnerability
21
+
22
+
23
+ @task()
24
+ class nuclei(VulnMulti):
25
+ """Fast and customisable vulnerability scanner based on simple YAML based DSL."""
26
+ cmd = 'nuclei'
27
+ input_types = [HOST, IP, URL]
28
+ output_types = [Vulnerability, Tag, Progress]
29
+ tags = ['vuln', 'scan']
30
+ file_flag = '-l'
31
+ input_flag = '-u'
32
+ json_flag = '-jsonl'
33
+ input_chunk_size = 20
34
+ opts = {
35
+ 'bulk_size': {'type': int, 'short': 'bs', 'help': 'Maximum number of hosts to be analyzed in parallel per template'}, # noqa: E501
36
+ 'debug': {'type': str, 'help': 'Debug mode'},
37
+ 'exclude_severity': {'type': str, 'short': 'es', 'help': 'Exclude severity'},
38
+ 'exclude_tags': {'type': str, 'short': 'etags', 'help': 'Exclude tags'},
39
+ 'input_mode': {'type': str, 'short': 'im', 'help': 'Mode of input file (list, burp, jsonl, yaml, openapi, swagger)'},
40
+ 'hang_monitor': {'is_flag': True, 'short': 'hm', 'default': True, 'help': 'Enable nuclei hang monitoring'},
41
+ 'headless_bulk_size': {'type': int, 'short': 'hbs', 'help': 'Maximum number of headless hosts to be analzyed in parallel per template'}, # noqa: E501
42
+ 'new_templates': {'type': str, 'short': 'nt', 'help': 'Run only new templates added in latest nuclei-templates release'}, # noqa: E501
43
+ 'automatic_scan': {'is_flag': True, 'short': 'as', 'help': 'Automatic web scan using wappalyzer technology detection to tags mapping'}, # noqa: E501
44
+ 'omit_raw': {'is_flag': True, 'short': 'or', 'default': True, 'help': 'Omit requests/response pairs in the JSON, JSONL, and Markdown outputs (for findings only)'}, # noqa: E501
45
+ 'response_size_read': {'type': int, 'help': 'Max body size to read (bytes)'},
46
+ 'stats': {'is_flag': True, 'short': 'stats', 'default': True, 'help': 'Display statistics about the running scan'},
47
+ 'stats_json': {'is_flag': True, 'short': 'sj', 'default': True, 'help': 'Display statistics in JSONL(ines) format'},
48
+ 'stats_interval': {'type': str, 'short': 'si', 'help': 'Number of seconds to wait between showing a statistics update'}, # noqa: E501
49
+ 'tags': {'type': str, 'help': 'Tags'},
50
+ 'templates': {'type': str, 'short': 't', 'help': 'Templates'},
51
+ 'template_id': {'type': str, 'short': 'tid', 'help': 'Template id'},
52
+ }
53
+ opt_key_map = {
54
+ HEADER: 'header',
55
+ DELAY: OPT_NOT_SUPPORTED,
56
+ FOLLOW_REDIRECT: 'follow-redirects',
57
+ PROXY: 'proxy',
58
+ RATE_LIMIT: 'rate-limit',
59
+ RETRIES: 'retries',
60
+ THREADS: 'c',
61
+ TIMEOUT: 'timeout',
62
+ USER_AGENT: OPT_NOT_SUPPORTED,
63
+
64
+ # nuclei opts
65
+ 'exclude_tags': 'exclude-tags',
66
+ 'exclude_severity': 'exclude-severity',
67
+ 'templates': 't',
68
+ 'response_size_read': 'rsr'
69
+ }
70
+ opt_value_map = {
71
+ 'tags': lambda x: ','.join(x) if isinstance(x, list) else x,
72
+ 'templates': lambda x: ','.join(x) if isinstance(x, list) else x,
73
+ 'exclude_tags': lambda x: ','.join(x) if isinstance(x, list) else x,
74
+ }
75
+ item_loaders = [JSONSerializer()]
76
+ output_discriminator = output_discriminator
77
+ output_map = {
78
+ Vulnerability: {
79
+ ID: lambda x: nuclei.id_extractor(x),
80
+ NAME: lambda x: nuclei.name_extractor(x),
81
+ DESCRIPTION: lambda x: x['info'].get('description'),
82
+ SEVERITY: lambda x: x['info'][SEVERITY],
83
+ CONFIDENCE: lambda x: 'high',
84
+ CVSS_SCORE: lambda x: x['info'].get('classification', {}).get('cvss-score') or 0,
85
+ MATCHED_AT: 'matched-at',
86
+ IP: 'ip',
87
+ TAGS: lambda x: x['info']['tags'],
88
+ REFERENCES: lambda x: x['info'].get('reference', []),
89
+ EXTRA_DATA: lambda x: nuclei.extra_data_extractor(x),
90
+ PROVIDER: 'nuclei',
91
+ },
92
+ Tag: {
93
+ NAME: lambda x: nuclei.name_extractor(x),
94
+ 'match': 'matched-at',
95
+ 'value': lambda x: nuclei.value_extractor(x),
96
+ 'category': lambda x: 'info',
97
+ EXTRA_DATA: lambda x: nuclei.extra_data_extractor(x, with_tags=True),
98
+ '_source': 'nuclei',
99
+ },
100
+ Progress: {
101
+ PERCENT: lambda x: int(x['percent']),
102
+ EXTRA_DATA: lambda x: {k: v for k, v in x.items() if k not in ['percent']}
103
+ }
104
+ }
105
+ install_version = 'v3.4.2'
106
+ install_pre = {'*': ['git']}
107
+ install_cmd = 'go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@[install_version]'
108
+ github_handle = 'projectdiscovery/nuclei'
109
+ install_post = {
110
+ '*': 'nuclei -ut'
111
+ }
112
+ proxychains = False
113
+ proxy_socks5 = True # kind of, leaks data when running network / dns templates
114
+ proxy_http = True # same
115
+ profile = 'cpu'
116
+
117
+ @staticmethod
118
+ def id_extractor(item):
119
+ cve_ids = item['info'].get('classification', {}).get('cve-id') or []
120
+ if len(cve_ids) > 0:
121
+ return cve_ids[0]
122
+ return None
123
+
124
+ @staticmethod
125
+ def extra_data_extractor(item, with_tags=False):
126
+ data = {}
127
+ data['data'] = item.get('extracted-results', [])
128
+ data['type'] = item.get('type', '')
129
+ data['template_id'] = item['template-id']
130
+ data['template_url'] = item.get('template-url', '')
131
+ for k, v in item.get('meta', {}).items():
132
+ data['data'].append(f'{k}: {v}')
133
+ data['metadata'] = item.get('metadata', {})
134
+ if with_tags:
135
+ data['tags'] = item.get('info', {}).get('tags', [])
136
+ return data
137
+
138
+ @staticmethod
139
+ def value_extractor(item):
140
+ values = item.get('extracted-results', '')
141
+ if isinstance(values, list):
142
+ return '\n'.join(values)
143
+ return values
144
+
145
+ @staticmethod
146
+ def name_extractor(item):
147
+ name = item['template-id']
148
+ matcher_name = item.get('matcher-name', '')
149
+ if matcher_name:
150
+ name += f':{matcher_name}'
151
+ return name