secator 0.10.1a12__py3-none-any.whl → 0.15.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 (73) hide show
  1. secator/celery.py +10 -5
  2. secator/celery_signals.py +2 -11
  3. secator/cli.py +309 -69
  4. secator/config.py +3 -2
  5. secator/configs/profiles/aggressive.yaml +6 -5
  6. secator/configs/profiles/default.yaml +6 -7
  7. secator/configs/profiles/insane.yaml +8 -0
  8. secator/configs/profiles/paranoid.yaml +8 -0
  9. secator/configs/profiles/polite.yaml +8 -0
  10. secator/configs/profiles/sneaky.yaml +8 -0
  11. secator/configs/profiles/tor.yaml +5 -0
  12. secator/configs/workflows/host_recon.yaml +11 -2
  13. secator/configs/workflows/url_dirsearch.yaml +5 -0
  14. secator/configs/workflows/url_params_fuzz.yaml +25 -0
  15. secator/configs/workflows/wordpress.yaml +4 -1
  16. secator/decorators.py +64 -34
  17. secator/definitions.py +8 -4
  18. secator/installer.py +84 -49
  19. secator/output_types/__init__.py +2 -1
  20. secator/output_types/certificate.py +78 -0
  21. secator/output_types/stat.py +3 -0
  22. secator/output_types/user_account.py +1 -1
  23. secator/report.py +2 -2
  24. secator/rich.py +1 -1
  25. secator/runners/_base.py +50 -11
  26. secator/runners/_helpers.py +15 -3
  27. secator/runners/command.py +85 -21
  28. secator/runners/scan.py +6 -3
  29. secator/runners/task.py +1 -0
  30. secator/runners/workflow.py +22 -4
  31. secator/tasks/_categories.py +25 -17
  32. secator/tasks/arjun.py +92 -0
  33. secator/tasks/bbot.py +33 -4
  34. secator/tasks/bup.py +4 -2
  35. secator/tasks/cariddi.py +17 -4
  36. secator/tasks/dalfox.py +4 -2
  37. secator/tasks/dirsearch.py +4 -2
  38. secator/tasks/dnsx.py +5 -2
  39. secator/tasks/dnsxbrute.py +4 -1
  40. secator/tasks/feroxbuster.py +5 -2
  41. secator/tasks/ffuf.py +7 -3
  42. secator/tasks/fping.py +4 -1
  43. secator/tasks/gau.py +5 -2
  44. secator/tasks/gf.py +4 -2
  45. secator/tasks/gitleaks.py +79 -0
  46. secator/tasks/gospider.py +5 -2
  47. secator/tasks/grype.py +5 -2
  48. secator/tasks/h8mail.py +4 -2
  49. secator/tasks/httpx.py +6 -3
  50. secator/tasks/katana.py +6 -3
  51. secator/tasks/maigret.py +4 -2
  52. secator/tasks/mapcidr.py +5 -3
  53. secator/tasks/msfconsole.py +8 -6
  54. secator/tasks/naabu.py +16 -5
  55. secator/tasks/nmap.py +31 -29
  56. secator/tasks/nuclei.py +18 -10
  57. secator/tasks/searchsploit.py +8 -3
  58. secator/tasks/subfinder.py +6 -3
  59. secator/tasks/testssl.py +276 -0
  60. secator/tasks/trivy.py +98 -0
  61. secator/tasks/wafw00f.py +85 -0
  62. secator/tasks/wpprobe.py +96 -0
  63. secator/tasks/wpscan.py +8 -4
  64. secator/template.py +61 -67
  65. secator/utils.py +31 -18
  66. secator/utils_test.py +34 -10
  67. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/METADATA +11 -3
  68. secator-0.15.1.dist-info/RECORD +128 -0
  69. secator/configs/profiles/stealth.yaml +0 -7
  70. secator-0.10.1a12.dist-info/RECORD +0 -116
  71. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/WHEEL +0 -0
  72. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/entry_points.txt +0 -0
  73. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -30,6 +30,8 @@ class Command(Runner):
30
30
  # Base cmd
31
31
  cmd = None
32
32
 
33
+ # Tags
34
+ tags = []
33
35
  # Meta options
34
36
  meta_opts = {}
35
37
 
@@ -71,6 +73,7 @@ class Command(Runner):
71
73
 
72
74
  # Flag to take a file as input
73
75
  file_flag = None
76
+ file_eof_newline = False
74
77
 
75
78
  # Flag to enable output JSON
76
79
  json_flag = None
@@ -83,6 +86,7 @@ class Command(Runner):
83
86
  install_post = None
84
87
  install_cmd = None
85
88
  install_github_handle = None
89
+ install_version = None
86
90
 
87
91
  # Serializer
88
92
  item_loader = None
@@ -164,6 +168,9 @@ class Command(Runner):
164
168
  # Process
165
169
  self.process = None
166
170
 
171
+ # Sudo
172
+ self.requires_sudo = False
173
+
167
174
  # Proxy config (global)
168
175
  self.proxy = self.run_opts.pop('proxy', False)
169
176
  self.configure_proxy()
@@ -177,6 +184,10 @@ class Command(Runner):
177
184
  # Run on_cmd hook
178
185
  self.run_hooks('on_cmd')
179
186
 
187
+ # Add sudo to command if it is required
188
+ if self.requires_sudo:
189
+ self.cmd = f'sudo {self.cmd}'
190
+
180
191
  # Build item loaders
181
192
  instance_func = getattr(self, 'item_loader', None)
182
193
  item_loaders = self.item_loaders.copy()
@@ -228,6 +239,22 @@ class Command(Runner):
228
239
  dict(self.opts, **self.meta_opts),
229
240
  opt_prefix=self.config.name)
230
241
 
242
+ @classmethod
243
+ def get_version_flag(cls):
244
+ if cls.version_flag == OPT_NOT_SUPPORTED:
245
+ return None
246
+ return cls.version_flag or f'{cls.opt_prefix}version'
247
+
248
+ @classmethod
249
+ def get_version_info(cls):
250
+ from secator.installer import get_version_info
251
+ return get_version_info(
252
+ cls.cmd.split(' ')[0],
253
+ cls.get_version_flag(),
254
+ cls.install_github_handle,
255
+ cls.install_cmd
256
+ )
257
+
231
258
  @classmethod
232
259
  def get_supported_opts(cls):
233
260
  def convert(d):
@@ -346,6 +373,12 @@ class Command(Runner):
346
373
  if self.has_children:
347
374
  return
348
375
 
376
+ # Abort if dry run
377
+ if self.dry_run:
378
+ self._print('')
379
+ self.print_command()
380
+ return
381
+
349
382
  # Print task description
350
383
  self.print_description()
351
384
 
@@ -464,15 +497,12 @@ class Command(Runner):
464
497
  if line is None:
465
498
  return
466
499
 
500
+ # Yield line if no items were yielded
501
+ yield line
502
+
467
503
  # Run item_loader to try parsing as dict
468
- item_count = 0
469
504
  for item in self.run_item_loaders(line):
470
505
  yield item
471
- item_count += 1
472
-
473
- # Yield line if no items were yielded
474
- if item_count == 0:
475
- yield line
476
506
 
477
507
  # Skip rest of iteration (no process mode)
478
508
  if self.no_process:
@@ -499,6 +529,7 @@ class Command(Runner):
499
529
  cmd_str += f' [dim gray11]({self.chunk}/{self.chunk_count})[/]'
500
530
  self._print(cmd_str, color='bold cyan', rich=True)
501
531
  self.debug('Command', obj={'cmd': self.cmd}, sub='init')
532
+ self.debug('Options', obj={'opts': self.cmd_options}, sub='init')
502
533
 
503
534
  def handle_file_not_found(self, exc):
504
535
  """Handle case where binary is not found.
@@ -687,11 +718,17 @@ class Command(Runner):
687
718
  opt_value_map (dict, str | Callable): A dict to map option values with their actual values.
688
719
  opt_prefix (str, default: '-'): Option prefix.
689
720
  command_name (str | None, default: None): Command name.
721
+
722
+ Returns:
723
+ dict: Processed options dict.
690
724
  """
691
- opts_str = ''
725
+ opts_dict = {}
692
726
  for opt_name, opt_conf in opts_conf.items():
693
727
  debug('before get_opt_value', obj={'name': opt_name, 'conf': opt_conf}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
694
728
 
729
+ # Save original opt name
730
+ original_opt_name = opt_name
731
+
695
732
  # Get opt value
696
733
  default_val = opt_conf.get('default')
697
734
  opt_val = Command._get_opt_value(
@@ -743,15 +780,10 @@ class Command(Runner):
743
780
 
744
781
  # Append opt name + opt value to option string.
745
782
  # Note: does not append opt value if value is True (flag)
746
- opts_str += f' {opt_name}'
747
- shlex_quote = opt_conf.get('shlex', True)
748
- if opt_val is not True:
749
- if shlex_quote:
750
- opt_val = shlex.quote(str(opt_val))
751
- opts_str += f' {opt_val}'
752
- debug('final', obj={'name': opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True)
783
+ opts_dict[original_opt_name] = {'name': opt_name, 'value': opt_val, 'conf': opt_conf}
784
+ debug('final', obj={'name': original_opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True) # noqa: E501
753
785
 
754
- return opts_str.strip()
786
+ return opts_dict
755
787
 
756
788
  @staticmethod
757
789
  def _validate_chunked_input(self, inputs):
@@ -806,27 +838,57 @@ class Command(Runner):
806
838
  if self.json_flag:
807
839
  self.cmd += f' {self.json_flag}'
808
840
 
841
+ # Opts str
842
+ opts_str = ''
843
+ opts = {}
844
+
809
845
  # Add options to cmd
810
- opts_str = Command._process_opts(
846
+ opts_dict = Command._process_opts(
811
847
  self.run_opts,
812
848
  self.opts,
813
849
  self.opt_key_map,
814
850
  self.opt_value_map,
815
851
  self.opt_prefix,
816
852
  command_name=self.config.name)
817
- if opts_str:
818
- self.cmd += f' {opts_str}'
819
853
 
820
854
  # Add meta options to cmd
821
- meta_opts_str = Command._process_opts(
855
+ meta_opts_dict = Command._process_opts(
822
856
  self.run_opts,
823
857
  self.meta_opts,
824
858
  self.opt_key_map,
825
859
  self.opt_value_map,
826
860
  self.opt_prefix,
827
861
  command_name=self.config.name)
828
- if meta_opts_str:
829
- self.cmd += f' {meta_opts_str}'
862
+
863
+ if opts_dict:
864
+ opts.update(opts_dict)
865
+ if meta_opts_dict:
866
+ opts.update(meta_opts_dict)
867
+
868
+ if opts:
869
+ for opt_conf in opts.values():
870
+ conf = opt_conf['conf']
871
+ internal = conf.get('internal', False)
872
+ if internal:
873
+ continue
874
+ if conf.get('requires_sudo', False):
875
+ self.requires_sudo = True
876
+ opts_str += ' ' + Command._build_opt_str(opt_conf)
877
+ self.cmd_options = opts
878
+ self.cmd += opts_str
879
+
880
+ @staticmethod
881
+ def _build_opt_str(opt):
882
+ """Build option string."""
883
+ conf = opt['conf']
884
+ opts_str = f'{opt["name"]}'
885
+ shlex_quote = conf.get('shlex', True)
886
+ value = opt['value']
887
+ if value is not True:
888
+ if shlex_quote:
889
+ value = shlex.quote(str(value))
890
+ opts_str += f' {value}'
891
+ return opts_str
830
892
 
831
893
  def _build_cmd_input(self):
832
894
  """Many commands take as input a string or a list. This function facilitate this based on whether we pass a
@@ -859,6 +921,8 @@ class Command(Runner):
859
921
  # Write the input to a file
860
922
  with open(fpath, 'w') as f:
861
923
  f.write('\n'.join(inputs))
924
+ if self.file_eof_newline:
925
+ f.write('\n')
862
926
 
863
927
  if self.file_flag == OPT_PIPE_INPUT:
864
928
  cmd = f'cat {fpath} | {cmd}'
secator/runners/scan.py CHANGED
@@ -33,9 +33,12 @@ class Scan(Runner):
33
33
  sigs = []
34
34
  for name, workflow_opts in self.config.workflows.items():
35
35
  run_opts = self.run_opts.copy()
36
+ run_opts.pop('profiles', None)
36
37
  run_opts['no_poll'] = True
38
+ run_opts['caller'] = 'Scan'
37
39
  opts = merge_opts(scan_opts, workflow_opts, run_opts)
38
- config = TemplateLoader(name=f'workflows/{name}')
40
+ name = name.split('/')[0]
41
+ config = TemplateLoader(name=f'workflow/{name}')
39
42
  workflow = Workflow(
40
43
  config,
41
44
  self.inputs,
@@ -44,13 +47,13 @@ class Scan(Runner):
44
47
  hooks=self._hooks,
45
48
  context=self.context.copy()
46
49
  )
47
- celery_workflow = workflow.build_celery_workflow()
50
+ celery_workflow = workflow.build_celery_workflow(chain_previous_results=True)
48
51
  for task_id, task_info in workflow.celery_ids_map.items():
49
52
  self.add_subtask(task_id, task_info['name'], task_info['descr'])
50
53
  sigs.append(celery_workflow)
51
54
 
52
55
  return chain(
53
- mark_runner_started.si(self).set(queue='results'),
56
+ mark_runner_started.si([], self).set(queue='results'),
54
57
  *sigs,
55
58
  mark_runner_completed.s(self).set(queue='results'),
56
59
  )
secator/runners/task.py CHANGED
@@ -32,6 +32,7 @@ class Task(Runner):
32
32
  # Run opts
33
33
  opts = self.run_opts.copy()
34
34
  opts.pop('output', None)
35
+ opts.pop('profiles', None)
35
36
  opts.pop('no_poll', False)
36
37
 
37
38
  # Set output types
@@ -20,9 +20,12 @@ class Workflow(Runner):
20
20
  from secator.celery import run_workflow
21
21
  return run_workflow.s(args=args, kwargs=kwargs)
22
22
 
23
- def build_celery_workflow(self):
23
+ def build_celery_workflow(self, chain_previous_results=False):
24
24
  """Build Celery workflow for workflow execution.
25
25
 
26
+ Args:
27
+ chain_previous_results (bool): Chain previous results.
28
+
26
29
  Returns:
27
30
  celery.Signature: Celery task signature.
28
31
  """
@@ -49,21 +52,31 @@ class Workflow(Runner):
49
52
  opts['skip_if_no_inputs'] = True
50
53
  opts['caller'] = 'Workflow'
51
54
 
55
+ forwarded_opts = {}
56
+ if chain_previous_results:
57
+ forwarded_opts = {k: v for k, v in self.run_opts.items() if k.endswith('_')}
58
+
52
59
  # Build task signatures
53
60
  sigs = self.get_tasks(
54
61
  self.config.tasks.toDict(),
55
62
  self.inputs,
56
63
  self.config.options,
57
- opts)
64
+ opts,
65
+ forwarded_opts=forwarded_opts
66
+ )
67
+
68
+ start_sig = mark_runner_started.si([], self, enable_hooks=True).set(queue='results')
69
+ if chain_previous_results:
70
+ start_sig = mark_runner_started.s(self, enable_hooks=True).set(queue='results')
58
71
 
59
72
  # Build workflow chain with lifecycle management
60
73
  return chain(
61
- mark_runner_started.si(self, enable_hooks=True).set(queue='results'),
74
+ start_sig,
62
75
  *sigs,
63
76
  mark_runner_completed.s(self, enable_hooks=True).set(queue='results'),
64
77
  )
65
78
 
66
- def get_tasks(self, config, inputs, workflow_opts, run_opts):
79
+ def get_tasks(self, config, inputs, workflow_opts, run_opts, forwarded_opts={}):
67
80
  """Get tasks recursively as Celery chains / chords.
68
81
 
69
82
  Args:
@@ -71,6 +84,7 @@ class Workflow(Runner):
71
84
  inputs (list): Inputs.
72
85
  workflow_opts (dict): Workflow options.
73
86
  run_opts (dict): Run options.
87
+ forwarded_opts (dict): Opts forwarded from parent runner (e.g: scan).
74
88
  sync (bool): Synchronous mode (chain of tasks, no chords).
75
89
 
76
90
  Returns:
@@ -78,6 +92,7 @@ class Workflow(Runner):
78
92
  """
79
93
  from celery import chain, group
80
94
  sigs = []
95
+ ix = 0
81
96
  for task_name, task_opts in config.items():
82
97
  # Task opts can be None
83
98
  task_opts = task_opts or {}
@@ -105,6 +120,8 @@ class Workflow(Runner):
105
120
 
106
121
  # Merge task options (order of priority with overrides)
107
122
  opts = merge_opts(workflow_opts, task_opts, run_opts)
123
+ if ix == 0 and forwarded_opts:
124
+ opts.update(forwarded_opts)
108
125
  opts['name'] = task_name
109
126
 
110
127
  # Create task signature
@@ -113,5 +130,6 @@ class Workflow(Runner):
113
130
  sig = task.s(inputs, **opts).set(queue=task.profile, task_id=task_id)
114
131
  self.add_subtask(task_id, task_name, task_opts.get('description', ''))
115
132
  self.output_types.extend(task.output_types)
133
+ ix += 1
116
134
  sigs.append(sig)
117
135
  return sigs
@@ -18,11 +18,16 @@ from secator.config import CONFIG
18
18
  from secator.runners import Command
19
19
  from secator.utils import debug, process_wordlist
20
20
 
21
+ USER_AGENTS = {
22
+ 'chrome_134.0_win10': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', # noqa: E501
23
+ 'chrome_134.0_macos': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36', # noqa: E501
24
+ }
25
+
21
26
 
22
27
  OPTS = {
23
- HEADER: {'type': str, 'help': 'Custom header to add to each request in the form "KEY1:VALUE1; KEY2:VALUE2"'},
28
+ HEADER: {'type': str, 'help': 'Custom header to add to each request in the form "KEY1:VALUE1; KEY2:VALUE2"', 'default': 'User-Agent: ' + USER_AGENTS['chrome_134.0_win10']}, # noqa: E501
24
29
  DELAY: {'type': float, 'short': 'd', 'help': 'Delay to add between each requests'},
25
- DEPTH: {'type': int, 'help': 'Scan depth', 'default': 2},
30
+ DEPTH: {'type': int, 'help': 'Scan depth'},
26
31
  FILTER_CODES: {'type': str, 'short': 'fc', 'help': 'Filter out responses with HTTP codes'},
27
32
  FILTER_REGEX: {'type': str, 'short': 'fr', 'help': 'Filter out responses with regular expression'},
28
33
  FILTER_SIZE: {'type': str, 'short': 'fs', 'help': 'Filter out responses with size'},
@@ -36,7 +41,7 @@ OPTS = {
36
41
  PROXY: {'type': str, 'help': 'HTTP(s) / SOCKS5 proxy'},
37
42
  RATE_LIMIT: {'type': int, 'short': 'rl', 'help': 'Rate limit, i.e max number of requests per second'},
38
43
  RETRIES: {'type': int, 'help': 'Retries'},
39
- THREADS: {'type': int, 'help': 'Number of threads to run', 'default': 50},
44
+ THREADS: {'type': int, 'help': 'Number of threads to run'},
40
45
  TIMEOUT: {'type': int, 'help': 'Request timeout'},
41
46
  USER_AGENT: {'type': str, 'short': 'ua', 'help': 'User agent, e.g "Mozilla Firefox 1.0"'},
42
47
  WORDLIST: {'type': str, 'short': 'w', 'default': 'http', 'process': process_wordlist, 'help': 'Wordlist to use'}
@@ -68,19 +73,19 @@ OPTS_VULN = [
68
73
 
69
74
  class Http(Command):
70
75
  meta_opts = {k: OPTS[k] for k in OPTS_HTTP_CRAWLERS}
71
- input_type = URL
76
+ input_types = [URL]
72
77
  output_types = [Url]
73
78
 
74
79
 
75
80
  class HttpCrawler(Command):
76
81
  meta_opts = {k: OPTS[k] for k in OPTS_HTTP_CRAWLERS}
77
- input_type = URL
82
+ input_types = [URL]
78
83
  output_types = [Url]
79
84
 
80
85
 
81
86
  class HttpFuzzer(Command):
82
87
  meta_opts = {k: OPTS[k] for k in OPTS_HTTP_FUZZERS}
83
- input_type = URL
88
+ input_types = [URL]
84
89
  output_types = [Url]
85
90
 
86
91
 
@@ -94,22 +99,22 @@ class Recon(Command):
94
99
 
95
100
 
96
101
  class ReconDns(Recon):
97
- input_type = HOST
102
+ input_types = [HOST]
98
103
  output_types = [Subdomain]
99
104
 
100
105
 
101
106
  class ReconUser(Recon):
102
- input_type = USERNAME
107
+ input_types = [USERNAME]
103
108
  output_types = [UserAccount]
104
109
 
105
110
 
106
111
  class ReconIp(Recon):
107
- input_type = CIDR_RANGE
112
+ input_types = [CIDR_RANGE]
108
113
  output_types = [Ip]
109
114
 
110
115
 
111
116
  class ReconPort(Recon):
112
- input_type = IP
117
+ input_types = [IP]
113
118
  output_types = [Port]
114
119
 
115
120
 
@@ -388,11 +393,11 @@ class Vuln(Command):
388
393
 
389
394
  @cache
390
395
  @staticmethod
391
- def lookup_ghsa(ghsa_id):
396
+ def lookup_cve_from_ghsa(ghsa_id):
392
397
  """Search for a GHSA on Github and and return associated CVE vulnerability data.
393
398
 
394
399
  Args:
395
- ghsa (str): CVE ID in the form GHSA-*
400
+ ghsa (str): GHSA ID in the form GHSA-*
396
401
 
397
402
  Returns:
398
403
  dict: vulnerability data.
@@ -405,7 +410,10 @@ class Vuln(Command):
405
410
  return None
406
411
  soup = BeautifulSoup(resp.text, 'lxml')
407
412
  sidebar_items = soup.find_all('div', {'class': 'discussion-sidebar-item'})
408
- cve_id = sidebar_items[2].find('div').text.strip()
413
+ cve_id = sidebar_items[3].find('div').text.strip()
414
+ if not cve_id.startswith('CVE'):
415
+ debug(f'{ghsa_id}: No CVE_ID extracted from https://github.com/advisories/{ghsa_id}', sub='cve')
416
+ return None
409
417
  vuln = Vuln.lookup_cve(cve_id)
410
418
  if vuln:
411
419
  vuln[TAGS].append('ghsa')
@@ -426,15 +434,15 @@ class Vuln(Command):
426
434
 
427
435
 
428
436
  class VulnHttp(Vuln):
429
- input_type = HOST
437
+ input_types = [HOST]
430
438
 
431
439
 
432
440
  class VulnCode(Vuln):
433
- input_type = PATH
441
+ input_types = [PATH]
434
442
 
435
443
 
436
444
  class VulnMulti(Vuln):
437
- input_type = HOST
445
+ input_types = [HOST]
438
446
  output_types = [Vulnerability]
439
447
 
440
448
 
@@ -443,7 +451,7 @@ class VulnMulti(Vuln):
443
451
  #--------------#
444
452
 
445
453
  class Tagger(Command):
446
- input_type = URL
454
+ input_types = [URL]
447
455
  output_types = [Tag]
448
456
 
449
457
  #----------------#
secator/tasks/arjun.py ADDED
@@ -0,0 +1,92 @@
1
+ import os
2
+ import yaml
3
+
4
+ from secator.decorators import task
5
+ from secator.definitions import (OUTPUT_PATH, RATE_LIMIT, THREADS, DELAY, TIMEOUT, METHOD, WORDLIST,
6
+ HEADER, URL, FOLLOW_REDIRECT)
7
+ from secator.output_types import Info, Url, Warning, Error
8
+ from secator.runners import Command
9
+ from secator.tasks._categories import OPTS
10
+ from secator.utils import process_wordlist
11
+
12
+
13
+ @task()
14
+ class arjun(Command):
15
+ """HTTP Parameter Discovery Suite."""
16
+ cmd = 'arjun'
17
+ tags = ['url', 'fuzz', 'params']
18
+ input_flag = '-u'
19
+ input_types = [URL]
20
+ version_flag = ' '
21
+ opts = {
22
+ 'chunk_size': {'type': int, 'help': 'Control query/chunk size'},
23
+ 'stable': {'is_flag': True, 'default': False, 'help': 'Use stable mode'},
24
+ 'include': {'type': str, 'help': 'Include persistent data (e.g: "api_key=xxxxx" or {"api_key": "xxxx"})'},
25
+ 'passive': {'is_flag': True, 'default': False, 'help': 'Passive mode'},
26
+ 'casing': {'type': str, 'help': 'Casing style for params e.g. like_this, likeThis, LIKE_THIS, like_this'}, # noqa: E501
27
+ WORDLIST: {'type': str, 'short': 'w', 'default': None, 'process': process_wordlist, 'help': 'Wordlist to use (default: arjun wordlist)'}, # noqa: E501
28
+ }
29
+ meta_opts = {
30
+ THREADS: OPTS[THREADS],
31
+ DELAY: OPTS[DELAY],
32
+ TIMEOUT: OPTS[TIMEOUT],
33
+ RATE_LIMIT: OPTS[RATE_LIMIT],
34
+ METHOD: OPTS[METHOD],
35
+ HEADER: OPTS[HEADER],
36
+ FOLLOW_REDIRECT: OPTS[FOLLOW_REDIRECT],
37
+ }
38
+ opt_key_map = {
39
+ THREADS: 't',
40
+ DELAY: 'd',
41
+ TIMEOUT: 'T',
42
+ RATE_LIMIT: '--rate-limit',
43
+ METHOD: 'm',
44
+ WORDLIST: 'w',
45
+ HEADER: '--headers',
46
+ 'chunk_size': 'c',
47
+ 'stable': '--stable',
48
+ 'passive': '--passive',
49
+ 'casing': '--casing',
50
+ 'follow_redirect': '--follow-redirect',
51
+ }
52
+ output_types = [Url]
53
+ install_version = '2.2.7'
54
+ install_cmd = 'pipx install arjun==[install_version] --force'
55
+ install_github_handle = 's0md3v/Arjun'
56
+
57
+ @staticmethod
58
+ def on_line(self, line):
59
+ if 'Processing chunks' in line:
60
+ return ''
61
+ return line
62
+
63
+ @staticmethod
64
+ def on_cmd(self):
65
+ follow_redirect = self.get_opt_value(FOLLOW_REDIRECT)
66
+ self.cmd = self.cmd.replace(' --follow-redirect', '')
67
+ if not follow_redirect:
68
+ self.cmd += ' --disable-redirects'
69
+
70
+ self.output_path = self.get_opt_value(OUTPUT_PATH)
71
+ if not self.output_path:
72
+ self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
73
+ self.cmd += f' -oJ {self.output_path}'
74
+
75
+ @staticmethod
76
+ def on_cmd_done(self):
77
+ if not os.path.exists(self.output_path):
78
+ yield Error(message=f'Could not find JSON results in {self.output_path}')
79
+ return
80
+ yield Info(message=f'JSON results saved to {self.output_path}')
81
+ with open(self.output_path, 'r') as f:
82
+ results = yaml.safe_load(f.read())
83
+ if not results:
84
+ yield Warning(message='No results found !')
85
+ return
86
+ for url, values in results.items():
87
+ for param in values['params']:
88
+ yield Url(
89
+ url=url + '?' + param + '=' + 'FUZZ',
90
+ headers=values['headers'],
91
+ method=values['method'],
92
+ )
secator/tasks/bbot.py CHANGED
@@ -2,6 +2,7 @@ import shutil
2
2
 
3
3
  from secator.config import CONFIG
4
4
  from secator.decorators import task
5
+ from secator.definitions import FILENAME, HOST, IP, ORG_NAME, PORT, URL, USERNAME
5
6
  from secator.runners import Command
6
7
  from secator.serializers import RegexSerializer
7
8
  from secator.output_types import Vulnerability, Port, Url, Record, Ip, Tag, Info, Error
@@ -121,6 +122,29 @@ BBOT_PRESETS = [
121
122
  'web-screenshots',
122
123
  'web-thorough'
123
124
  ]
125
+ BBOT_FLAGS = [
126
+ 'active',
127
+ 'affiliates',
128
+ 'aggressive',
129
+ 'baddns',
130
+ 'cloud-enum,'
131
+ 'code-enum,deadly',
132
+ 'email-enum',
133
+ 'iis-shortnames',
134
+ 'passive',
135
+ 'portscan',
136
+ 'report',
137
+ 'safe',
138
+ 'service-enum',
139
+ 'slow',
140
+ 'social-enum',
141
+ 'subdomain-enum',
142
+ 'subdomain-hijack',
143
+ 'web-basic',
144
+ 'web-paramminer',
145
+ 'web-screenshots',
146
+ 'web-thorough'
147
+ ]
124
148
  BBOT_MODULES_STR = ' '.join(BBOT_MODULES)
125
149
  BBOT_MAP_TYPES = {
126
150
  'IP_ADDRESS': Ip,
@@ -154,17 +178,21 @@ def output_discriminator(self, item):
154
178
  class bbot(Command):
155
179
  """Multipurpose scanner."""
156
180
  cmd = 'bbot -y --allow-deadly --force'
181
+ tags = ['vuln', 'scan']
157
182
  json_flag = '--json'
158
183
  input_flag = '-t'
184
+ input_types = [HOST, IP, URL, PORT, ORG_NAME, USERNAME, FILENAME]
159
185
  file_flag = None
160
186
  version_flag = '--help'
161
187
  opts = {
162
- 'modules': {'type': str, 'short': 'm', 'default': '', 'help': ','.join(BBOT_MODULES)},
163
- 'presets': {'type': str, 'short': 'ps', 'default': 'kitchen-sink', 'help': ','.join(BBOT_PRESETS), 'shlex': False},
188
+ 'modules': {'type': str, 'short': 'm', 'help': ','.join(BBOT_MODULES)},
189
+ 'presets': {'type': str, 'short': 'ps', 'help': ','.join(BBOT_PRESETS), 'shlex': False},
190
+ 'flags': {'type': str, 'short': 'fl', 'help': ','.join(BBOT_FLAGS)}
164
191
  }
165
192
  opt_key_map = {
166
193
  'modules': 'm',
167
- 'presets': 'p'
194
+ 'presets': 'p',
195
+ 'flags': 'f'
168
196
  }
169
197
  opt_value_map = {
170
198
  'presets': lambda x: ' '.join(x.split(','))
@@ -222,7 +250,8 @@ class bbot(Command):
222
250
  'apk': ['python3-dev', 'linux-headers', 'musl-dev', 'gcc', 'git', 'openssl', 'unzip', 'tar', 'chromium'],
223
251
  '*': ['gcc', 'git', 'openssl', 'unzip', 'tar', 'chromium']
224
252
  }
225
- install_cmd = 'pipx install bbot && pipx upgrade bbot'
253
+ install_version = '2.4.2'
254
+ install_cmd = 'pipx install bbot==[install_version] --force'
226
255
  install_post = {
227
256
  '*': f'rm -fr {CONFIG.dirs.share}/pipx/venvs/bbot/lib/python3.12/site-packages/ansible_collections/*'
228
257
  }
secator/tasks/bup.py CHANGED
@@ -16,8 +16,9 @@ from secator.tasks._categories import Http
16
16
  class bup(Http):
17
17
  """40X bypasser."""
18
18
  cmd = 'bup'
19
+ tags = ['url', 'bypass']
19
20
  input_flag = '-u'
20
- input_type = URL
21
+ input_types = [URL]
21
22
  json_flag = '--jsonl'
22
23
  opt_prefix = '--'
23
24
  opts = {
@@ -63,7 +64,8 @@ class bup(Http):
63
64
  'stored_response_path': 'response_html_filename',
64
65
  }
65
66
  }
66
- install_cmd = 'pipx install bypass-url-parser && pipx upgrade bypass-url-parser'
67
+ install_version = '0.4.4'
68
+ install_cmd = 'pipx install bypass-url-parser==[install_version] --force'
67
69
 
68
70
  @staticmethod
69
71
  def on_init(self):