secator 0.6.0__py3-none-any.whl → 0.7.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.

Potentially problematic release.


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

Files changed (84) hide show
  1. secator/celery.py +160 -185
  2. secator/celery_utils.py +268 -0
  3. secator/cli.py +327 -106
  4. secator/config.py +27 -11
  5. secator/configs/workflows/host_recon.yaml +5 -3
  6. secator/configs/workflows/port_scan.yaml +7 -3
  7. secator/configs/workflows/url_bypass.yaml +10 -0
  8. secator/configs/workflows/url_vuln.yaml +1 -1
  9. secator/decorators.py +169 -92
  10. secator/definitions.py +10 -3
  11. secator/exporters/__init__.py +7 -5
  12. secator/exporters/console.py +10 -0
  13. secator/exporters/csv.py +27 -19
  14. secator/exporters/gdrive.py +16 -11
  15. secator/exporters/json.py +3 -1
  16. secator/exporters/table.py +30 -2
  17. secator/exporters/txt.py +20 -16
  18. secator/hooks/gcs.py +53 -0
  19. secator/hooks/mongodb.py +53 -27
  20. secator/output_types/__init__.py +29 -11
  21. secator/output_types/_base.py +11 -1
  22. secator/output_types/error.py +36 -0
  23. secator/output_types/exploit.py +1 -1
  24. secator/output_types/info.py +24 -0
  25. secator/output_types/ip.py +7 -0
  26. secator/output_types/port.py +8 -1
  27. secator/output_types/progress.py +5 -0
  28. secator/output_types/record.py +3 -1
  29. secator/output_types/stat.py +33 -0
  30. secator/output_types/tag.py +6 -4
  31. secator/output_types/url.py +6 -3
  32. secator/output_types/vulnerability.py +3 -2
  33. secator/output_types/warning.py +24 -0
  34. secator/report.py +55 -23
  35. secator/rich.py +44 -39
  36. secator/runners/_base.py +622 -635
  37. secator/runners/_helpers.py +5 -91
  38. secator/runners/celery.py +18 -0
  39. secator/runners/command.py +364 -211
  40. secator/runners/scan.py +8 -24
  41. secator/runners/task.py +21 -55
  42. secator/runners/workflow.py +41 -40
  43. secator/scans/__init__.py +28 -0
  44. secator/serializers/dataclass.py +6 -0
  45. secator/serializers/json.py +10 -5
  46. secator/serializers/regex.py +12 -4
  47. secator/tasks/_categories.py +5 -2
  48. secator/tasks/bbot.py +293 -0
  49. secator/tasks/bup.py +98 -0
  50. secator/tasks/cariddi.py +38 -49
  51. secator/tasks/dalfox.py +3 -0
  52. secator/tasks/dirsearch.py +12 -23
  53. secator/tasks/dnsx.py +49 -30
  54. secator/tasks/dnsxbrute.py +2 -0
  55. secator/tasks/feroxbuster.py +8 -17
  56. secator/tasks/ffuf.py +3 -2
  57. secator/tasks/fping.py +3 -3
  58. secator/tasks/gau.py +5 -0
  59. secator/tasks/gf.py +2 -2
  60. secator/tasks/gospider.py +4 -0
  61. secator/tasks/grype.py +9 -9
  62. secator/tasks/h8mail.py +31 -41
  63. secator/tasks/httpx.py +58 -21
  64. secator/tasks/katana.py +18 -22
  65. secator/tasks/maigret.py +26 -24
  66. secator/tasks/mapcidr.py +2 -3
  67. secator/tasks/msfconsole.py +4 -16
  68. secator/tasks/naabu.py +3 -1
  69. secator/tasks/nmap.py +50 -35
  70. secator/tasks/nuclei.py +9 -2
  71. secator/tasks/searchsploit.py +17 -9
  72. secator/tasks/subfinder.py +5 -1
  73. secator/tasks/wpscan.py +79 -93
  74. secator/template.py +61 -45
  75. secator/thread.py +24 -0
  76. secator/utils.py +330 -80
  77. secator/utils_test.py +48 -23
  78. secator/workflows/__init__.py +28 -0
  79. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/METADATA +11 -5
  80. secator-0.7.0.dist-info/RECORD +115 -0
  81. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/WHEEL +1 -1
  82. secator-0.6.0.dist-info/RECORD +0 -101
  83. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/entry_points.txt +0 -0
  84. {secator-0.6.0.dist-info → secator-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,7 +21,6 @@ class msfconsole(VulnMulti):
21
21
  input_type = HOST
22
22
  input_chunk_size = 1
23
23
  output_types = []
24
- output_return_type = str
25
24
  opt_prefix = '--'
26
25
  opts = {
27
26
  'resource': {'type': str, 'help': 'Metasploit resource script.', 'short': 'r'},
@@ -40,19 +39,11 @@ class msfconsole(VulnMulti):
40
39
  THREADS: OPT_NOT_SUPPORTED,
41
40
  TIMEOUT: OPT_NOT_SUPPORTED,
42
41
  USER_AGENT: OPT_NOT_SUPPORTED,
43
- THREADS: OPT_NOT_SUPPORTED,
44
42
  }
45
43
  encoding = 'ansi'
46
44
  ignore_return_code = True
47
45
  # install_cmd = 'wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/msfinstall.sh | sh'
48
46
 
49
- @staticmethod
50
- def validate_input(self, input):
51
- """No list input supported for this command. Pass a single input instead."""
52
- if isinstance(input, list):
53
- return False
54
- return True
55
-
56
47
  @staticmethod
57
48
  def on_init(self):
58
49
  command = self.get_opt_value('execute_command')
@@ -61,14 +52,14 @@ class msfconsole(VulnMulti):
61
52
  env_vars = {}
62
53
  if environment:
63
54
  env_vars = dict(map(lambda x: x.split('='), environment.strip().split(',')))
64
- env_vars['RHOST'] = self.input
65
- env_vars['RHOSTS'] = self.input
55
+ env_vars['RHOST'] = self.inputs[0]
56
+ env_vars['RHOSTS'] = self.inputs[0]
66
57
 
67
58
  # Passing msfconsole command directly, simply add RHOST / RHOSTS from host input and run then exit
68
59
  if command:
69
60
  self.run_opts['msfconsole.execute_command'] = (
70
- f'setg RHOST {self.input}; '
71
- f'setg RHOSTS {self.input}; '
61
+ f'setg RHOST {self.inputs[0]}; '
62
+ f'setg RHOSTS {self.inputs[0]}; '
72
63
  f'{command.format(**env_vars)}; '
73
64
  f'exit;'
74
65
  )
@@ -101,9 +92,6 @@ class msfconsole(VulnMulti):
101
92
  else:
102
93
  raise ValueError('At least one of "inline_script" or "resource_script" must be passed.')
103
94
 
104
- # Clear host input
105
- self.input = ''
106
-
107
95
 
108
96
  # TODO: This is better as it goes through an RPC API to communicate with
109
97
  # metasploit rpc server, but it does not give any output.
secator/tasks/naabu.py CHANGED
@@ -3,6 +3,7 @@ from secator.definitions import (DELAY, HOST, OPT_NOT_SUPPORTED, PORT, PORTS,
3
3
  PROXY, RATE_LIMIT, RETRIES, STATE, THREADS,
4
4
  TIMEOUT, TOP_PORTS)
5
5
  from secator.output_types import Port
6
+ from secator.serializers import JSONSerializer
6
7
  from secator.tasks._categories import ReconPort
7
8
 
8
9
 
@@ -16,7 +17,7 @@ class naabu(ReconPort):
16
17
  opts = {
17
18
  PORTS: {'type': str, 'short': 'p', 'help': 'Ports'},
18
19
  TOP_PORTS: {'type': str, 'short': 'tp', 'help': 'Top ports'},
19
- 'scan_type': {'type': str, 'help': 'Scan type (SYN (s)/CONNECT(c))'},
20
+ 'scan_type': {'type': str, 'short': 'st', 'help': 'Scan type (SYN (s)/CONNECT(c))'},
20
21
  # 'health_check': {'is_flag': True, 'short': 'hc', 'help': 'Health check'}
21
22
  }
22
23
  opt_key_map = {
@@ -37,6 +38,7 @@ class naabu(ReconPort):
37
38
  RETRIES: lambda x: 1 if x == 0 else x,
38
39
  PROXY: lambda x: x.replace('socks5://', '')
39
40
  }
41
+ item_loaders = [JSONSerializer()]
40
42
  output_map = {
41
43
  Port: {
42
44
  PORT: lambda x: x['port'],
secator/tasks/nmap.py CHANGED
@@ -8,15 +8,14 @@ from secator.config import CONFIG
8
8
  from secator.decorators import task
9
9
  from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY,
10
10
  DESCRIPTION, EXTRA_DATA, FOLLOW_REDIRECT,
11
- HEADER, HOST, ID, IP, MATCHED_AT, NAME,
11
+ HEADER, HOST, ID, IP, PROTOCOL, MATCHED_AT, NAME,
12
12
  OPT_NOT_SUPPORTED, OUTPUT_PATH, PORT, PORTS, PROVIDER,
13
13
  PROXY, RATE_LIMIT, REFERENCE, REFERENCES,
14
14
  RETRIES, SCRIPT, SERVICE_NAME, SEVERITY, STATE, TAGS,
15
15
  THREADS, TIMEOUT, TOP_PORTS, USER_AGENT)
16
- from secator.output_types import Exploit, Port, Vulnerability
17
- from secator.rich import console
16
+ from secator.output_types import Exploit, Port, Vulnerability, Info, Error
18
17
  from secator.tasks._categories import VulnMulti
19
- from secator.utils import debug
18
+ from secator.utils import debug, traceback_as_string
20
19
 
21
20
  logger = logging.getLogger(__name__)
22
21
 
@@ -24,7 +23,7 @@ logger = logging.getLogger(__name__)
24
23
  @task()
25
24
  class nmap(VulnMulti):
26
25
  """Network Mapper is a free and open source utility for network discovery and security auditing."""
27
- cmd = 'nmap -sT -sV -Pn'
26
+ cmd = 'nmap'
28
27
  input_flag = None
29
28
  input_chunk_size = 1
30
29
  file_flag = '-iL'
@@ -34,8 +33,11 @@ class nmap(VulnMulti):
34
33
  PORTS: {'type': str, 'short': 'p', 'help': 'Ports to scan'},
35
34
  TOP_PORTS: {'type': int, 'short': 'tp', 'help': 'Top ports to scan [full, 100, 1000]'},
36
35
  SCRIPT: {'type': str, 'default': 'vulners', 'help': 'NSE scripts'},
37
- # 'tcp_connect': {'type': bool, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
36
+ 'skip_host_discovery': {'is_flag': True, 'short': 'Pn', 'default': False, 'help': 'Skip host discovery (no ping)'},
37
+ 'version_detection': {'is_flag': True, 'short': 'sV', 'default': False, 'help': 'Version detection'},
38
38
  'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth'},
39
+ 'tcp_connect': {'is_flag': True, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
40
+ 'udp_scan': {'is_flag': True, 'short': 'sU', 'default': False, 'help': 'UDP scan'},
39
41
  'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path'},
40
42
  }
41
43
  opt_key_map = {
@@ -51,8 +53,12 @@ class nmap(VulnMulti):
51
53
 
52
54
  # Nmap opts
53
55
  PORTS: '-p',
56
+ 'skip_host_discovery': '-Pn',
57
+ 'version_detection': '-sV',
58
+ 'tcp_connect': '-sT',
59
+ 'tcp_syn_stealth': '-sS',
60
+ 'udp_scan': '-sU',
54
61
  'output_path': '-oX',
55
- 'tcp_syn_stealth': '-sS'
56
62
  }
57
63
  opt_value_map = {
58
64
  PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
@@ -75,21 +81,23 @@ class nmap(VulnMulti):
75
81
  self.output_path = output_path
76
82
  self.cmd += f' -oX {self.output_path}'
77
83
  tcp_syn_stealth = self.get_opt_value('tcp_syn_stealth')
78
- if tcp_syn_stealth:
84
+ tcp_connect = self.get_opt_value('tcp_connect')
85
+ udp_scan = self.get_opt_value('udp_scan')
86
+ if tcp_syn_stealth or udp_scan:
79
87
  self.cmd = f'sudo {self.cmd}'
80
- self.cmd = self.cmd.replace('-sT', '')
88
+ if tcp_connect and tcp_syn_stealth:
89
+ self._print(
90
+ 'Options -sT (SYN stealth scan) and -sS (CONNECT scan) are conflicting. Keeping only -sT.',
91
+ 'bold gold3')
92
+ self.cmd = self.cmd.replace('-sT ', '')
81
93
 
82
- def yielder(self):
83
- yield from super().yielder()
84
- if self.return_code != 0:
94
+ @staticmethod
95
+ def on_cmd_done(self):
96
+ if not os.path.exists(self.output_path):
97
+ yield Error(message=f'Could not find XML results in {self.output_path}')
85
98
  return
86
- self.results = []
87
- note = f'nmap XML results saved to {self.output_path}'
88
- if self.print_line:
89
- self._print(note)
90
- if os.path.exists(self.output_path):
91
- nmap_data = self.xml_to_json()
92
- yield from nmap_data
99
+ yield Info(message=f'XML results saved to {self.output_path}')
100
+ yield from self.xml_to_json()
93
101
 
94
102
  def xml_to_json(self):
95
103
  results = []
@@ -97,12 +105,12 @@ class nmap(VulnMulti):
97
105
  content = f.read()
98
106
  try:
99
107
  results = xmltodict.parse(content) # parse XML to dict
100
- except Exception as e:
101
- logger.exception(e)
102
- logger.error(
103
- f'Cannot parse nmap XML output {self.output_path} to valid JSON.')
104
- results['_host'] = self.input
105
- return nmapData(results)
108
+ except Exception as exc:
109
+ yield Error(
110
+ message=f'Cannot parse XML output {self.output_path} to valid JSON.',
111
+ traceback=traceback_as_string(exc)
112
+ )
113
+ yield from nmapData(results)
106
114
 
107
115
 
108
116
  class nmapData(dict):
@@ -123,14 +131,9 @@ class nmapData(dict):
123
131
 
124
132
  # Get extra data
125
133
  extra_data = self._get_extra_data(port)
126
- service_name = extra_data['service_name']
134
+ service_name = extra_data.get('service_name', '')
127
135
  version_exact = extra_data.get('version_exact', False)
128
136
  conf = extra_data.get('confidence')
129
- if not version_exact:
130
- console.print(
131
- f'[bold orange1]nmap could not identify an exact version for {service_name} '
132
- f'(detection confidence is {conf}): do not blindy trust the results ![/]'
133
- )
134
137
 
135
138
  # Grab CPEs
136
139
  cpes = extra_data.get('cpe', [])
@@ -138,6 +141,9 @@ class nmapData(dict):
138
141
  # Get script output
139
142
  scripts = self._get_scripts(port)
140
143
 
144
+ # Get port protocol
145
+ protocol = port['@protocol'].lower()
146
+
141
147
  # Yield port data
142
148
  port = {
143
149
  PORT: port_number,
@@ -145,7 +151,9 @@ class nmapData(dict):
145
151
  STATE: state,
146
152
  SERVICE_NAME: service_name,
147
153
  IP: ip,
148
- EXTRA_DATA: extra_data
154
+ PROTOCOL: protocol,
155
+ EXTRA_DATA: extra_data,
156
+ CONFIDENCE: conf
149
157
  }
150
158
  yield port
151
159
 
@@ -197,7 +205,6 @@ class nmapData(dict):
197
205
 
198
206
  def _get_hostname(self, host_cfg):
199
207
  hostnames = host_cfg.get('hostnames', {})
200
- hostname = self['_host']
201
208
  if hostnames:
202
209
  hostnames = hostnames.get('hostname', [])
203
210
  if isinstance(hostnames, dict):
@@ -205,11 +212,19 @@ class nmapData(dict):
205
212
  if hostnames:
206
213
  hostname = hostnames[0]['@name']
207
214
  else:
208
- hostname = host_cfg.get('address', {}).get('@addr', None)
215
+ hostname = self._get_address(host_cfg).get('@addr', None)
209
216
  return hostname
210
217
 
218
+ def _get_address(self, host_cfg):
219
+ if isinstance(host_cfg.get('address', {}), list):
220
+ addresses = host_cfg.get('address', {})
221
+ for address in addresses:
222
+ if address.get('@addrtype') == "ipv4":
223
+ return address
224
+ return host_cfg.get('address', {})
225
+
211
226
  def _get_ip(self, host_cfg):
212
- return host_cfg.get('address', {}).get('@addr', None)
227
+ return self._get_address(host_cfg).get('@addr', None)
213
228
 
214
229
  def _get_extra_data(self, port_cfg):
215
230
  extra_data = {
secator/tasks/nuclei.py CHANGED
@@ -4,15 +4,16 @@ from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
4
4
  MATCHED_AT, NAME, OPT_NOT_SUPPORTED, PERCENT,
5
5
  PROVIDER, PROXY, RATE_LIMIT, REFERENCES,
6
6
  RETRIES, SEVERITY, TAGS, THREADS, TIMEOUT,
7
- USER_AGENT, DEFAULT_NUCLEI_FLAGS)
7
+ USER_AGENT)
8
8
  from secator.output_types import Progress, Vulnerability
9
+ from secator.serializers import JSONSerializer
9
10
  from secator.tasks._categories import VulnMulti
10
11
 
11
12
 
12
13
  @task()
13
14
  class nuclei(VulnMulti):
14
15
  """Fast and customisable vulnerability scanner based on simple YAML based DSL."""
15
- cmd = f'nuclei {DEFAULT_NUCLEI_FLAGS}'
16
+ cmd = 'nuclei'
16
17
  file_flag = '-l'
17
18
  input_flag = '-u'
18
19
  json_flag = '-jsonl'
@@ -23,6 +24,11 @@ class nuclei(VulnMulti):
23
24
  'exclude_severity': {'type': str, 'short': 'es', 'help': 'Exclude severity'},
24
25
  'template_id': {'type': str, 'short': 'tid', 'help': 'Template id'},
25
26
  'debug': {'type': str, 'help': 'Debug mode'},
27
+ 'stats': {'is_flag': True, 'short': 'stats', 'default': True, 'help': 'Display statistics about the running scan'},
28
+ 'stats_json': {'is_flag': True, 'short': 'sj', 'default': True, 'help': 'Display statistics in JSONL(ines) format'},
29
+ 'stats_interval': {'type': str, 'short': 'si', 'default': 20, 'help': 'Number of seconds to wait between showing a statistics update'}, # noqa: E501
30
+ 'hang_monitor': {'is_flag': True, 'short': 'hm', 'default': True, 'help': 'Enable nuclei hang monitoring'},
31
+ '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
26
32
  }
27
33
  opt_key_map = {
28
34
  HEADER: 'header',
@@ -45,6 +51,7 @@ class nuclei(VulnMulti):
45
51
  'templates': lambda x: ','.join(x) if isinstance(x, list) else x,
46
52
  'exclude_tags': lambda x: ','.join(x) if isinstance(x, list) else x,
47
53
  }
54
+ item_loaders = [JSONSerializer()]
48
55
  output_types = [Vulnerability, Progress]
49
56
  output_map = {
50
57
  Vulnerability: {
@@ -5,6 +5,7 @@ from secator.definitions import (CVES, EXTRA_DATA, ID, MATCHED_AT, NAME,
5
5
  PROVIDER, REFERENCE, TAGS, OPT_NOT_SUPPORTED)
6
6
  from secator.output_types import Exploit
7
7
  from secator.runners import Command
8
+ from secator.serializers import JSONSerializer
8
9
 
9
10
 
10
11
  SEARCHSPLOIT_TITLE_REGEX = re.compile(r'^((?:[a-zA-Z\-_!\.()]+\d?\s?)+)\.?\s*(.*)$')
@@ -21,6 +22,7 @@ class searchsploit(Command):
21
22
  'strict': {'short': 's', 'is_flag': True, 'default': False, 'help': 'Strict match'}
22
23
  }
23
24
  opt_key_map = {}
25
+ item_loaders = [JSONSerializer()]
24
26
  output_types = [Exploit]
25
27
  output_map = {
26
28
  Exploit: {
@@ -56,14 +58,15 @@ class searchsploit(Command):
56
58
 
57
59
  @staticmethod
58
60
  def before_init(self):
59
- _in = self.input
61
+ if len(self.inputs) == 0:
62
+ return
63
+ _in = self.inputs[0]
60
64
  self.matched_at = None
61
65
  if '~' in _in:
62
66
  split = _in.split('~')
63
67
  self.matched_at = split[0]
64
- self.input = split[1]
65
- if isinstance(self.input, str):
66
- self.input = self.input.replace('httpd', '').replace('/', ' ')
68
+ self.inputs[0] = split[1]
69
+ self.inputs[0] = self.inputs[0].replace('httpd', '').replace('/', ' ')
67
70
 
68
71
  @staticmethod
69
72
  def on_item_pre_convert(self, item):
@@ -80,12 +83,17 @@ class searchsploit(Command):
80
83
  group = match.groups()
81
84
  product = '-'.join(group[0].strip().split(' '))
82
85
  if len(group[1]) > 1:
83
- versions, title = tuple(group[1].split(' - '))
84
- item.name = title
85
- product_info = [f'{product.lower()} {v.strip()}' for v in versions.split('/')]
86
- item.tags = product_info + item.tags
86
+ try:
87
+ versions, title = tuple(group[1].split(' - '))
88
+ item.name = title
89
+ product_info = [f'{product.lower()} {v.strip()}' for v in versions.split('/')]
90
+ item.tags = product_info + item.tags
91
+ except ValueError:
92
+ item.name = item.name.split(' - ')[-1]
93
+ item.tags = [product.lower()]
94
+ pass
87
95
  # else:
88
96
  # self._print(f'[bold red]{item.name} ({item.reference}) did not quite match SEARCHSPLOIT_TITLE_REGEX. Please report this issue.[/]') # noqa: E501
89
- input_tag = '-'.join(self.input.replace('\'', '').split(' '))
97
+ input_tag = '-'.join(self.inputs[0].replace('\'', '').split(' '))
90
98
  item.tags = [input_tag] + item.tags
91
99
  return item
@@ -2,6 +2,7 @@ from secator.decorators import task
2
2
  from secator.definitions import (DELAY, DOMAIN, OPT_NOT_SUPPORTED, PROXY,
3
3
  RATE_LIMIT, RETRIES, THREADS, TIMEOUT)
4
4
  from secator.output_types import Subdomain
5
+ from secator.serializers import JSONSerializer
5
6
  from secator.tasks._categories import ReconDns
6
7
 
7
8
 
@@ -23,6 +24,7 @@ class subfinder(ReconDns):
23
24
  opt_value_map = {
24
25
  PROXY: lambda x: x.replace('http://', '').replace('https://', '') if x else None
25
26
  }
27
+ item_loaders = [JSONSerializer()]
26
28
  output_map = {
27
29
  Subdomain: {
28
30
  DOMAIN: 'input',
@@ -38,4 +40,6 @@ class subfinder(ReconDns):
38
40
 
39
41
  @staticmethod
40
42
  def validate_item(self, item):
41
- return item['input'] != 'localhost'
43
+ if isinstance(item, dict):
44
+ return item['input'] != 'localhost'
45
+ return True
secator/tasks/wpscan.py CHANGED
@@ -8,7 +8,7 @@ from secator.definitions import (CONFIDENCE, CVSS_SCORE, DELAY, DESCRIPTION,
8
8
  PROXY, RATE_LIMIT, REFERENCES, RETRIES,
9
9
  SEVERITY, TAGS, THREADS, TIMEOUT,
10
10
  URL, USER_AGENT)
11
- from secator.output_types import Tag, Vulnerability
11
+ from secator.output_types import Tag, Vulnerability, Info, Error
12
12
  from secator.tasks._categories import VulnHttp
13
13
 
14
14
 
@@ -73,105 +73,91 @@ class wpscan(VulnHttp):
73
73
  ignore_return_code = True
74
74
  profile = 'io'
75
75
 
76
- def yielder(self):
77
- prev = self.print_item_count
78
- self.print_item_count = False
79
- yield from super().yielder()
80
- if self.return_code != 0:
81
- return
82
- self.results = []
83
- if not self.output_json:
84
- return
76
+ @staticmethod
77
+ def on_init(self):
78
+ output_path = self.get_opt_value(OUTPUT_PATH)
79
+ if not output_path:
80
+ output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
81
+ self.output_path = output_path
82
+ self.cmd += f' -o {self.output_path}'
85
83
 
86
- note = f'wpscan JSON results saved to {self.output_path}'
87
- if self.print_line:
88
- self._print(note)
84
+ @staticmethod
85
+ def on_cmd_done(self):
86
+ if not os.path.exists(self.output_path):
87
+ yield Error(message=f'Could not find JSON results in {self.output_path}')
88
+ return
89
89
 
90
- if os.path.exists(self.output_path):
91
- with open(self.output_path, 'r') as f:
92
- data = json.load(f)
90
+ yield Info(message=f'JSON results saved to {self.output_path}')
91
+ with open(self.output_path, 'r') as f:
92
+ data = json.load(f)
93
93
 
94
- if self.orig:
95
- yield data
96
- return
94
+ # Get URL
95
+ target = data.get('target_url', self.inputs[0])
97
96
 
98
- # Get URL
99
- target = data.get('target_url', self.targets)
97
+ # Wordpress version
98
+ version = data.get('version', {})
99
+ if version:
100
+ wp_version = version['number']
101
+ wp_version_status = version['status']
102
+ if wp_version_status == 'outdated':
103
+ vuln = version
104
+ vuln.update({
105
+ 'url': target,
106
+ 'to_s': 'Wordpress outdated version',
107
+ 'type': wp_version,
108
+ 'references': {},
109
+ })
110
+ yield vuln
100
111
 
101
- # Wordpress version
102
- version = data.get('version', {})
112
+ # Main theme
113
+ main_theme = data.get('main_theme', {})
114
+ if main_theme:
115
+ version = main_theme.get('version', {})
116
+ slug = main_theme['slug']
117
+ location = main_theme['location']
103
118
  if version:
104
- wp_version = version['number']
105
- wp_version_status = version['status']
106
- if wp_version_status == 'outdated':
107
- vuln = version
108
- vuln.update({
109
- 'url': target,
110
- 'to_s': 'Wordpress outdated version',
111
- 'type': wp_version,
112
- 'references': {},
113
- })
114
- yield vuln
115
-
116
- # Main theme
117
- main_theme = data.get('main_theme', {})
118
- if main_theme:
119
- version = main_theme.get('version', {})
120
- slug = main_theme['slug']
121
- location = main_theme['location']
122
- if version:
123
- number = version['number']
124
- latest_version = main_theme.get('latest_version')
125
- yield Tag(
126
- name=f'Wordpress theme - {slug} {number}',
127
- match=target,
128
- extra_data={
129
- 'url': location,
130
- 'latest_version': latest_version
131
- }
119
+ number = version['number']
120
+ latest_version = main_theme.get('latest_version')
121
+ yield Tag(
122
+ name=f'Wordpress theme - {slug} {number}',
123
+ match=target,
124
+ extra_data={
125
+ 'url': location,
126
+ 'latest_version': latest_version
127
+ }
128
+ )
129
+ if (latest_version and number < latest_version):
130
+ yield Vulnerability(
131
+ matched_at=target,
132
+ name=f'Wordpress theme - {slug} {number} outdated',
133
+ severity='info'
132
134
  )
133
- if (latest_version and number < latest_version):
134
- yield Vulnerability(
135
- matched_at=target,
136
- name=f'Wordpress theme - {slug} {number} outdated',
137
- severity='info'
138
- )
139
135
 
140
- # Interesting findings
141
- interesting_findings = data.get('interesting_findings', [])
142
- for item in interesting_findings:
143
- yield item
136
+ # Interesting findings
137
+ interesting_findings = data.get('interesting_findings', [])
138
+ for item in interesting_findings:
139
+ yield item
144
140
 
145
- # Plugins
146
- plugins = data.get('plugins', {})
147
- for _, data in plugins.items():
148
- version = data.get('version', {})
149
- slug = data['slug']
150
- location = data['location']
151
- if version:
152
- number = version['number']
153
- latest_version = data.get('latest_version')
154
- yield Tag(
155
- name=f'Wordpress plugin - {slug} {number}',
156
- match=target,
157
- extra_data={
158
- 'url': location,
159
- 'latest_version': latest_version
160
- }
141
+ # Plugins
142
+ plugins = data.get('plugins', {})
143
+ for _, data in plugins.items():
144
+ version = data.get('version', {})
145
+ slug = data['slug']
146
+ location = data['location']
147
+ if version:
148
+ number = version['number']
149
+ latest_version = data.get('latest_version')
150
+ yield Tag(
151
+ name=f'Wordpress plugin - {slug} {number}',
152
+ match=target,
153
+ extra_data={
154
+ 'url': location,
155
+ 'latest_version': latest_version
156
+ }
157
+ )
158
+ if (latest_version and number < latest_version):
159
+ yield Vulnerability(
160
+ matched_at=target,
161
+ name=f'Wordpress plugin - {slug} {number} outdated',
162
+ severity='info'
161
163
  )
162
- if (latest_version and number < latest_version):
163
- yield Vulnerability(
164
- matched_at=target,
165
- name=f'Wordpress plugin - {slug} {number} outdated',
166
- severity='info'
167
- )
168
-
169
- self.print_item_count = prev
170
-
171
- @staticmethod
172
- def on_init(self):
173
- output_path = self.get_opt_value(OUTPUT_PATH)
174
- if not output_path:
175
- output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
176
- self.output_path = output_path
177
- self.cmd += f' -o {self.output_path}'