secator 0.10.1a12__py3-none-any.whl → 0.11.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 (40) hide show
  1. secator/celery.py +10 -5
  2. secator/celery_signals.py +2 -11
  3. secator/cli.py +153 -46
  4. secator/config.py +2 -2
  5. secator/configs/workflows/url_params_fuzz.yaml +23 -0
  6. secator/configs/workflows/wordpress.yaml +4 -1
  7. secator/decorators.py +10 -5
  8. secator/definitions.py +3 -0
  9. secator/installer.py +46 -32
  10. secator/output_types/__init__.py +2 -1
  11. secator/output_types/certificate.py +78 -0
  12. secator/output_types/user_account.py +1 -1
  13. secator/rich.py +1 -1
  14. secator/runners/_base.py +14 -6
  15. secator/runners/_helpers.py +4 -3
  16. secator/runners/command.py +81 -21
  17. secator/runners/scan.py +5 -3
  18. secator/runners/workflow.py +22 -4
  19. secator/tasks/_categories.py +12 -4
  20. secator/tasks/arjun.py +82 -0
  21. secator/tasks/ffuf.py +2 -1
  22. secator/tasks/fping.py +1 -0
  23. secator/tasks/gitleaks.py +76 -0
  24. secator/tasks/grype.py +1 -1
  25. secator/tasks/mapcidr.py +1 -1
  26. secator/tasks/naabu.py +7 -1
  27. secator/tasks/nmap.py +29 -29
  28. secator/tasks/subfinder.py +1 -1
  29. secator/tasks/testssl.py +274 -0
  30. secator/tasks/trivy.py +95 -0
  31. secator/tasks/wafw00f.py +83 -0
  32. secator/tasks/wpprobe.py +94 -0
  33. secator/template.py +49 -67
  34. secator/utils.py +16 -7
  35. secator/utils_test.py +26 -8
  36. {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/METADATA +1 -1
  37. {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/RECORD +40 -32
  38. {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/WHEEL +0 -0
  39. {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/entry_points.txt +0 -0
  40. {secator-0.10.1a12.dist-info → secator-0.11.1.dist-info}/licenses/LICENSE +0 -0
@@ -26,6 +26,7 @@ from secator.output_types.url import Url
26
26
  from secator.output_types.user_account import UserAccount
27
27
  from secator.output_types.vulnerability import Vulnerability
28
28
  from secator.output_types.record import Record
29
+ from secator.output_types.certificate import Certificate
29
30
  from secator.output_types.info import Info
30
31
  from secator.output_types.warning import Warning
31
32
  from secator.output_types.error import Error
@@ -39,6 +40,6 @@ STAT_TYPES = [
39
40
  Stat
40
41
  ]
41
42
  FINDING_TYPES = [
42
- Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability
43
+ Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability, Certificate
43
44
  ]
44
45
  OUTPUT_TYPES = FINDING_TYPES + EXECUTION_TYPES + STAT_TYPES
@@ -0,0 +1,78 @@
1
+ import time
2
+ from datetime import datetime, timedelta
3
+ from dataclasses import dataclass, field
4
+ from secator.output_types import OutputType
5
+ from secator.utils import rich_to_ansi
6
+ from secator.definitions import CERTIFICATE_STATUS_UNKNOWN
7
+
8
+
9
+ @dataclass
10
+ class Certificate(OutputType):
11
+ host: str
12
+ fingerprint_sha256: str = field(default='')
13
+ ip: str = field(default='', compare=False)
14
+ raw_value: str = field(default='', compare=False)
15
+ subject_cn: str = field(default='', compare=False)
16
+ subject_an: list[str] = field(default_factory=list, compare=False)
17
+ not_before: datetime = field(default=None, compare=False)
18
+ not_after: datetime = field(default=None, compare=False)
19
+ issuer_dn: str = field(default='', compare=False)
20
+ issuer_cn: str = field(default='', compare=False)
21
+ issuer: str = field(default='', compare=False)
22
+ self_signed: bool = field(default=True, compare=False)
23
+ trusted: bool = field(default=False, compare=False)
24
+ status: str = field(default=CERTIFICATE_STATUS_UNKNOWN, compare=False)
25
+ keysize: int = field(default=None, compare=False)
26
+ serial_number: str = field(default='', compare=False)
27
+ ciphers: list[str] = field(default_factory=list, compare=False)
28
+ # parent_certificate: 'Certificate' = None # noqa: F821
29
+ _source: str = field(default='', repr=True)
30
+ _type: str = field(default='certificate', repr=True)
31
+ _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
32
+ _uuid: str = field(default='', repr=True, compare=False)
33
+ _context: dict = field(default_factory=dict, repr=True, compare=False)
34
+ _tagged: bool = field(default=False, repr=True, compare=False)
35
+ _duplicate: bool = field(default=False, repr=True, compare=False)
36
+ _related: list = field(default_factory=list, compare=False)
37
+ _table_fields = ['ip', 'host']
38
+ _sort_by = ('ip',)
39
+
40
+ def __str__(self) -> str:
41
+ return self.subject_cn
42
+
43
+ def is_expired(self) -> bool:
44
+ if self.not_after:
45
+ return self.not_after < datetime.now()
46
+ return True
47
+
48
+ def is_expired_soon(self, months: int = 1) -> bool:
49
+ if self.not_after:
50
+ return self.not_after < datetime.now() + timedelta(days=months * 30)
51
+ return True
52
+
53
+ @staticmethod
54
+ def format_date(date):
55
+ if date:
56
+ return date.strftime("%m/%d/%Y")
57
+ return '?'
58
+
59
+ def __repr__(self) -> str:
60
+ s = f'📜 [bold white]{self.host}[/]'
61
+ s += f' [cyan]{self.status}[/]'
62
+ s += rf' [white]\[fingerprint={self.fingerprint_sha256[:10]}][/]'
63
+ if self.subject_cn:
64
+ s += rf' [white]\[cn={self.subject_cn}][/]'
65
+ if self.subject_an:
66
+ s += rf' [white]\[an={", ".join(self.subject_an)}][/]'
67
+ if self.issuer:
68
+ s += rf' [white]\[issuer={self.issuer}][/]'
69
+ elif self.issuer_cn:
70
+ s += rf' [white]\[issuer_cn={self.issuer_cn}][/]'
71
+ expiry_date = Certificate.format_date(self.not_after)
72
+ if self.is_expired():
73
+ s += f' [red]expired since {expiry_date}[/red]'
74
+ elif self.is_expired_soon(months=2):
75
+ s += f' [yellow]expires <2 months[/yellow], [yellow]valid until {expiry_date}[/yellow]'
76
+ else:
77
+ s += f' [green]not expired[/green], [yellow]valid until {expiry_date}[/yellow]'
78
+ return rich_to_ansi(s)
@@ -37,5 +37,5 @@ class UserAccount(OutputType):
37
37
  if self.url:
38
38
  s += rf' \[[white]{_s(self.url)}[/]]'
39
39
  if self.extra_data:
40
- s += r' \[[bold yellow]' + _s(', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]')
40
+ s += r' \[[bold yellow]' + _s(', '.join(f'{k}:{v}' for k, v in self.extra_data.items())) + '[/]]'
41
41
  return rich_to_ansi(s)
secator/rich.py CHANGED
@@ -4,7 +4,7 @@ import yaml
4
4
  from rich.console import Console
5
5
  from rich.table import Table
6
6
 
7
- console = Console(stderr=True)
7
+ console = Console(stderr=True, record=True)
8
8
  console_stdout = Console(record=True)
9
9
  # handler = RichHandler(rich_tracebacks=True) # TODO: add logging handler
10
10
 
secator/runners/_base.py CHANGED
@@ -102,6 +102,7 @@ class Runner:
102
102
  self.piped_input = self.run_opts.get('piped_input', False)
103
103
  self.piped_output = self.run_opts.get('piped_output', False)
104
104
  self.enable_duplicate_check = self.run_opts.get('enable_duplicate_check', True)
105
+ self.dry_run = self.run_opts.get('dry_run', False)
105
106
 
106
107
  # Runner print opts
107
108
  self.print_item = self.run_opts.get('print_item', False)
@@ -128,12 +129,8 @@ class Runner:
128
129
  [self.add_result(result, print=False, output=False) for result in results]
129
130
 
130
131
  # Determine inputs
131
- inputs = [inputs] if not isinstance(inputs, list) else inputs
132
- if not self.chunk and self.results:
133
- inputs, run_opts, errors = run_extractors(self.results, run_opts, inputs)
134
- for error in errors:
135
- self.add_result(error, print=True)
136
- self.inputs = list(set(inputs))
132
+ self.inputs = [inputs] if not isinstance(inputs, list) else inputs
133
+ self.filter_results(results)
137
134
 
138
135
  # Debug
139
136
  self.debug('Inputs', obj=self.inputs, sub='init')
@@ -310,6 +307,8 @@ class Runner:
310
307
  self.mark_completed()
311
308
 
312
309
  finally:
310
+ if self.dry_run:
311
+ return
313
312
  if self.sync:
314
313
  self.mark_completed()
315
314
  if self.enable_reports:
@@ -328,6 +327,15 @@ class Runner:
328
327
  self.add_result(error, print=True)
329
328
  yield error
330
329
 
330
+ def filter_results(self, results):
331
+ """Filter results based on the runner's config."""
332
+ if not self.chunk:
333
+ inputs, run_opts, errors = run_extractors(results, self.run_opts, self.inputs, self.dry_run)
334
+ for error in errors:
335
+ self.add_result(error, print=True)
336
+ self.inputs = list(set(inputs))
337
+ self.run_opts = run_opts
338
+
331
339
  def add_result(self, item, print=False, output=True):
332
340
  """Add item to runner results.
333
341
 
@@ -4,13 +4,14 @@ from secator.output_types import Error
4
4
  from secator.utils import deduplicate, debug
5
5
 
6
6
 
7
- def run_extractors(results, opts, inputs=[]):
7
+ def run_extractors(results, opts, inputs=[], dry_run=False):
8
8
  """Run extractors and merge extracted values with option dict.
9
9
 
10
10
  Args:
11
11
  results (list): List of results.
12
12
  opts (dict): Options.
13
13
  inputs (list): Original inputs.
14
+ dry_run (bool): Dry run.
14
15
 
15
16
  Returns:
16
17
  tuple: inputs, options, errors.
@@ -22,9 +23,9 @@ def run_extractors(results, opts, inputs=[]):
22
23
  values, err = extract_from_results(results, val)
23
24
  errors.extend(err)
24
25
  if key == 'targets':
25
- inputs = deduplicate(values)
26
+ inputs = ['<COMPUTED>'] if dry_run else deduplicate(values)
26
27
  else:
27
- opts[key] = deduplicate(values)
28
+ opts[key] = ['<COMPUTED>'] if dry_run else deduplicate(values)
28
29
  return inputs, opts, errors
29
30
 
30
31
 
@@ -71,6 +71,7 @@ class Command(Runner):
71
71
 
72
72
  # Flag to take a file as input
73
73
  file_flag = None
74
+ file_eof_newline = False
74
75
 
75
76
  # Flag to enable output JSON
76
77
  json_flag = None
@@ -164,6 +165,9 @@ class Command(Runner):
164
165
  # Process
165
166
  self.process = None
166
167
 
168
+ # Sudo
169
+ self.requires_sudo = False
170
+
167
171
  # Proxy config (global)
168
172
  self.proxy = self.run_opts.pop('proxy', False)
169
173
  self.configure_proxy()
@@ -177,6 +181,10 @@ class Command(Runner):
177
181
  # Run on_cmd hook
178
182
  self.run_hooks('on_cmd')
179
183
 
184
+ # Add sudo to command if it is required
185
+ if self.requires_sudo:
186
+ self.cmd = f'sudo {self.cmd}'
187
+
180
188
  # Build item loaders
181
189
  instance_func = getattr(self, 'item_loader', None)
182
190
  item_loaders = self.item_loaders.copy()
@@ -228,6 +236,22 @@ class Command(Runner):
228
236
  dict(self.opts, **self.meta_opts),
229
237
  opt_prefix=self.config.name)
230
238
 
239
+ @classmethod
240
+ def get_version_flag(cls):
241
+ if cls.version_flag == OPT_NOT_SUPPORTED:
242
+ return None
243
+ return cls.version_flag or f'{cls.opt_prefix}version'
244
+
245
+ @classmethod
246
+ def get_version_info(cls):
247
+ from secator.installer import get_version_info
248
+ return get_version_info(
249
+ cls.__name__,
250
+ cls.get_version_flag(),
251
+ cls.install_github_handle,
252
+ cls.install_cmd
253
+ )
254
+
231
255
  @classmethod
232
256
  def get_supported_opts(cls):
233
257
  def convert(d):
@@ -346,6 +370,11 @@ class Command(Runner):
346
370
  if self.has_children:
347
371
  return
348
372
 
373
+ # Abort if dry run
374
+ if self.dry_run:
375
+ self.print_command()
376
+ return
377
+
349
378
  # Print task description
350
379
  self.print_description()
351
380
 
@@ -464,15 +493,12 @@ class Command(Runner):
464
493
  if line is None:
465
494
  return
466
495
 
496
+ # Yield line if no items were yielded
497
+ yield line
498
+
467
499
  # Run item_loader to try parsing as dict
468
- item_count = 0
469
500
  for item in self.run_item_loaders(line):
470
501
  yield item
471
- item_count += 1
472
-
473
- # Yield line if no items were yielded
474
- if item_count == 0:
475
- yield line
476
502
 
477
503
  # Skip rest of iteration (no process mode)
478
504
  if self.no_process:
@@ -499,6 +525,7 @@ class Command(Runner):
499
525
  cmd_str += f' [dim gray11]({self.chunk}/{self.chunk_count})[/]'
500
526
  self._print(cmd_str, color='bold cyan', rich=True)
501
527
  self.debug('Command', obj={'cmd': self.cmd}, sub='init')
528
+ self.debug('Options', obj={'opts': self.cmd_options}, sub='init')
502
529
 
503
530
  def handle_file_not_found(self, exc):
504
531
  """Handle case where binary is not found.
@@ -687,11 +714,17 @@ class Command(Runner):
687
714
  opt_value_map (dict, str | Callable): A dict to map option values with their actual values.
688
715
  opt_prefix (str, default: '-'): Option prefix.
689
716
  command_name (str | None, default: None): Command name.
717
+
718
+ Returns:
719
+ dict: Processed options dict.
690
720
  """
691
- opts_str = ''
721
+ opts_dict = {}
692
722
  for opt_name, opt_conf in opts_conf.items():
693
723
  debug('before get_opt_value', obj={'name': opt_name, 'conf': opt_conf}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
694
724
 
725
+ # Save original opt name
726
+ original_opt_name = opt_name
727
+
695
728
  # Get opt value
696
729
  default_val = opt_conf.get('default')
697
730
  opt_val = Command._get_opt_value(
@@ -743,15 +776,10 @@ class Command(Runner):
743
776
 
744
777
  # Append opt name + opt value to option string.
745
778
  # 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)
779
+ opts_dict[original_opt_name] = {'name': opt_name, 'value': opt_val, 'conf': opt_conf}
780
+ debug('final', obj={'name': original_opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True) # noqa: E501
753
781
 
754
- return opts_str.strip()
782
+ return opts_dict
755
783
 
756
784
  @staticmethod
757
785
  def _validate_chunked_input(self, inputs):
@@ -806,27 +834,57 @@ class Command(Runner):
806
834
  if self.json_flag:
807
835
  self.cmd += f' {self.json_flag}'
808
836
 
837
+ # Opts str
838
+ opts_str = ''
839
+ opts = {}
840
+
809
841
  # Add options to cmd
810
- opts_str = Command._process_opts(
842
+ opts_dict = Command._process_opts(
811
843
  self.run_opts,
812
844
  self.opts,
813
845
  self.opt_key_map,
814
846
  self.opt_value_map,
815
847
  self.opt_prefix,
816
848
  command_name=self.config.name)
817
- if opts_str:
818
- self.cmd += f' {opts_str}'
819
849
 
820
850
  # Add meta options to cmd
821
- meta_opts_str = Command._process_opts(
851
+ meta_opts_dict = Command._process_opts(
822
852
  self.run_opts,
823
853
  self.meta_opts,
824
854
  self.opt_key_map,
825
855
  self.opt_value_map,
826
856
  self.opt_prefix,
827
857
  command_name=self.config.name)
828
- if meta_opts_str:
829
- self.cmd += f' {meta_opts_str}'
858
+
859
+ if opts_dict:
860
+ opts.update(opts_dict)
861
+ if meta_opts_dict:
862
+ opts.update(meta_opts_dict)
863
+
864
+ if opts:
865
+ for opt_conf in opts.values():
866
+ conf = opt_conf['conf']
867
+ internal = conf.get('internal', False)
868
+ if internal:
869
+ continue
870
+ if conf.get('requires_sudo', False):
871
+ self.requires_sudo = True
872
+ opts_str += ' ' + Command._build_opt_str(opt_conf)
873
+ self.cmd_options = opts
874
+ self.cmd += opts_str
875
+
876
+ @staticmethod
877
+ def _build_opt_str(opt):
878
+ """Build option string."""
879
+ conf = opt['conf']
880
+ opts_str = f'{opt["name"]}'
881
+ shlex_quote = conf.get('shlex', True)
882
+ value = opt['value']
883
+ if value is not True:
884
+ if shlex_quote:
885
+ value = shlex.quote(str(value))
886
+ opts_str += f' {value}'
887
+ return opts_str
830
888
 
831
889
  def _build_cmd_input(self):
832
890
  """Many commands take as input a string or a list. This function facilitate this based on whether we pass a
@@ -859,6 +917,8 @@ class Command(Runner):
859
917
  # Write the input to a file
860
918
  with open(fpath, 'w') as f:
861
919
  f.write('\n'.join(inputs))
920
+ if self.file_eof_newline:
921
+ f.write('\n')
862
922
 
863
923
  if self.file_flag == OPT_PIPE_INPUT:
864
924
  cmd = f'cat {fpath} | {cmd}'
secator/runners/scan.py CHANGED
@@ -34,8 +34,10 @@ class Scan(Runner):
34
34
  for name, workflow_opts in self.config.workflows.items():
35
35
  run_opts = self.run_opts.copy()
36
36
  run_opts['no_poll'] = True
37
+ run_opts['caller'] = 'Scan'
37
38
  opts = merge_opts(scan_opts, workflow_opts, run_opts)
38
- config = TemplateLoader(name=f'workflows/{name}')
39
+ name = name.split('/')[0]
40
+ config = TemplateLoader(name=f'workflow/{name}')
39
41
  workflow = Workflow(
40
42
  config,
41
43
  self.inputs,
@@ -44,13 +46,13 @@ class Scan(Runner):
44
46
  hooks=self._hooks,
45
47
  context=self.context.copy()
46
48
  )
47
- celery_workflow = workflow.build_celery_workflow()
49
+ celery_workflow = workflow.build_celery_workflow(chain_previous_results=True)
48
50
  for task_id, task_info in workflow.celery_ids_map.items():
49
51
  self.add_subtask(task_id, task_info['name'], task_info['descr'])
50
52
  sigs.append(celery_workflow)
51
53
 
52
54
  return chain(
53
- mark_runner_started.si(self).set(queue='results'),
55
+ mark_runner_started.si([], self).set(queue='results'),
54
56
  *sigs,
55
57
  mark_runner_completed.s(self).set(queue='results'),
56
58
  )
@@ -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,9 +18,14 @@ 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
30
  DEPTH: {'type': int, 'help': 'Scan depth', 'default': 2},
26
31
  FILTER_CODES: {'type': str, 'short': 'fc', 'help': 'Filter out responses with HTTP codes'},
@@ -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')
secator/tasks/arjun.py ADDED
@@ -0,0 +1,82 @@
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, HEADER, URL)
6
+ from secator.output_types import Info, Url, Warning, Error
7
+ from secator.runners import Command
8
+ from secator.tasks._categories import OPTS
9
+ from secator.utils import process_wordlist
10
+
11
+
12
+ @task()
13
+ class arjun(Command):
14
+ """HTTP Parameter Discovery Suite."""
15
+ cmd = 'arjun'
16
+ input_flag = '-u'
17
+ input_type = URL
18
+ version_flag = ' '
19
+ opts = {
20
+ 'chunk_size': {'type': int, 'help': 'Control query/chunk size'},
21
+ 'stable': {'is_flag': True, 'default': False, 'help': 'Use stable mode'},
22
+ 'include': {'type': str, 'help': 'Include persistent data (e.g: "api_key=xxxxx" or {"api_key": "xxxx"})'},
23
+ 'passive': {'is_flag': True, 'default': False, 'help': 'Passive mode'},
24
+ 'casing': {'type': str, 'help': 'Casing style for params e.g. like_this, likeThis, LIKE_THIS, like_this'}, # noqa: E501
25
+ WORDLIST: {'type': str, 'short': 'w', 'default': None, 'process': process_wordlist, 'help': 'Wordlist to use (default: arjun wordlist)'}, # noqa: E501
26
+ }
27
+ meta_opts = {
28
+ THREADS: OPTS[THREADS],
29
+ DELAY: OPTS[DELAY],
30
+ TIMEOUT: OPTS[TIMEOUT],
31
+ RATE_LIMIT: OPTS[RATE_LIMIT],
32
+ METHOD: OPTS[METHOD],
33
+ HEADER: OPTS[HEADER],
34
+ }
35
+ opt_key_map = {
36
+ THREADS: 't',
37
+ DELAY: 'd',
38
+ TIMEOUT: 'T',
39
+ RATE_LIMIT: '--rate-limit',
40
+ METHOD: 'm',
41
+ WORDLIST: 'w',
42
+ HEADER: '--headers',
43
+ 'chunk_size': 'c',
44
+ 'stable': '--stable',
45
+ 'passive': '--passive',
46
+ 'casing': '--casing',
47
+ }
48
+ output_types = [Url]
49
+ install_cmd = 'pipx install arjun && pipx upgrade arjun'
50
+ install_github_handle = 's0md3v/Arjun'
51
+
52
+ @staticmethod
53
+ def on_line(self, line):
54
+ if 'Processing chunks' in line:
55
+ return ''
56
+ return line
57
+
58
+ @staticmethod
59
+ def on_cmd(self):
60
+ self.output_path = self.get_opt_value(OUTPUT_PATH)
61
+ if not self.output_path:
62
+ self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
63
+ self.cmd += f' -oJ {self.output_path}'
64
+
65
+ @staticmethod
66
+ def on_cmd_done(self):
67
+ if not os.path.exists(self.output_path):
68
+ yield Error(message=f'Could not find JSON results in {self.output_path}')
69
+ return
70
+ yield Info(message=f'JSON results saved to {self.output_path}')
71
+ with open(self.output_path, 'r') as f:
72
+ results = yaml.safe_load(f.read())
73
+ if not results:
74
+ yield Warning(message='No results found !')
75
+ return
76
+ for url, values in results.items():
77
+ for param in values['params']:
78
+ yield Url(
79
+ url=url + '?' + param + '=' + 'FUZZ',
80
+ headers=values['headers'],
81
+ method=values['method'],
82
+ )
secator/tasks/ffuf.py CHANGED
@@ -18,7 +18,7 @@ FFUF_PROGRESS_REGEX = r':: Progress: \[(?P<count>\d+)/(?P<total>\d+)\] :: Job \[
18
18
  @task()
19
19
  class ffuf(HttpFuzzer):
20
20
  """Fast web fuzzer written in Go."""
21
- cmd = 'ffuf -noninteractive -recursion'
21
+ cmd = 'ffuf -noninteractive'
22
22
  input_flag = '-u'
23
23
  input_chunk_size = 1
24
24
  file_flag = None
@@ -30,6 +30,7 @@ class ffuf(HttpFuzzer):
30
30
  ]
31
31
  opts = {
32
32
  AUTO_CALIBRATION: {'is_flag': True, 'short': 'ac', 'help': 'Auto-calibration'},
33
+ 'recursion': {'is_flag': True, 'default': True, 'short': 'recursion', 'help': 'Recursion'},
33
34
  }
34
35
  opt_key_map = {
35
36
  HEADER: 'H',
secator/tasks/fping.py CHANGED
@@ -29,6 +29,7 @@ class fping(ReconIp):
29
29
  input_type = IP
30
30
  output_types = [Ip]
31
31
  install_pre = {'*': ['fping']}
32
+ ignore_return_code = True
32
33
 
33
34
  @staticmethod
34
35
  def item_loader(self, line):