secator 0.15.0__py3-none-any.whl → 0.16.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 (106) hide show
  1. secator/celery.py +40 -24
  2. secator/celery_signals.py +71 -68
  3. secator/celery_utils.py +43 -27
  4. secator/cli.py +520 -280
  5. secator/cli_helper.py +394 -0
  6. secator/click.py +87 -0
  7. secator/config.py +67 -39
  8. secator/configs/profiles/http_headless.yaml +6 -0
  9. secator/configs/profiles/http_record.yaml +6 -0
  10. secator/configs/profiles/tor.yaml +1 -1
  11. secator/configs/scans/domain.yaml +4 -2
  12. secator/configs/scans/host.yaml +1 -1
  13. secator/configs/scans/network.yaml +1 -4
  14. secator/configs/scans/subdomain.yaml +13 -1
  15. secator/configs/scans/url.yaml +1 -2
  16. secator/configs/workflows/cidr_recon.yaml +6 -4
  17. secator/configs/workflows/code_scan.yaml +1 -1
  18. secator/configs/workflows/host_recon.yaml +29 -3
  19. secator/configs/workflows/subdomain_recon.yaml +67 -16
  20. secator/configs/workflows/url_crawl.yaml +44 -15
  21. secator/configs/workflows/url_dirsearch.yaml +4 -4
  22. secator/configs/workflows/url_fuzz.yaml +25 -17
  23. secator/configs/workflows/url_params_fuzz.yaml +7 -0
  24. secator/configs/workflows/url_vuln.yaml +33 -8
  25. secator/configs/workflows/user_hunt.yaml +2 -1
  26. secator/configs/workflows/wordpress.yaml +5 -3
  27. secator/cve.py +718 -0
  28. secator/decorators.py +0 -454
  29. secator/definitions.py +49 -30
  30. secator/exporters/_base.py +2 -2
  31. secator/exporters/console.py +2 -2
  32. secator/exporters/table.py +4 -3
  33. secator/exporters/txt.py +1 -1
  34. secator/hooks/mongodb.py +2 -4
  35. secator/installer.py +77 -49
  36. secator/loader.py +116 -0
  37. secator/output_types/_base.py +3 -0
  38. secator/output_types/certificate.py +63 -63
  39. secator/output_types/error.py +4 -5
  40. secator/output_types/info.py +2 -2
  41. secator/output_types/ip.py +3 -1
  42. secator/output_types/progress.py +5 -9
  43. secator/output_types/state.py +17 -17
  44. secator/output_types/tag.py +3 -0
  45. secator/output_types/target.py +10 -2
  46. secator/output_types/url.py +19 -7
  47. secator/output_types/vulnerability.py +11 -7
  48. secator/output_types/warning.py +2 -2
  49. secator/report.py +27 -15
  50. secator/rich.py +18 -10
  51. secator/runners/_base.py +447 -234
  52. secator/runners/_helpers.py +133 -24
  53. secator/runners/command.py +182 -102
  54. secator/runners/scan.py +33 -5
  55. secator/runners/task.py +13 -7
  56. secator/runners/workflow.py +105 -72
  57. secator/scans/__init__.py +2 -2
  58. secator/serializers/dataclass.py +20 -20
  59. secator/tasks/__init__.py +4 -4
  60. secator/tasks/_categories.py +39 -27
  61. secator/tasks/arjun.py +9 -5
  62. secator/tasks/bbot.py +53 -21
  63. secator/tasks/bup.py +19 -5
  64. secator/tasks/cariddi.py +24 -3
  65. secator/tasks/dalfox.py +26 -7
  66. secator/tasks/dirsearch.py +10 -4
  67. secator/tasks/dnsx.py +70 -25
  68. secator/tasks/feroxbuster.py +11 -3
  69. secator/tasks/ffuf.py +42 -6
  70. secator/tasks/fping.py +20 -8
  71. secator/tasks/gau.py +3 -1
  72. secator/tasks/gf.py +5 -4
  73. secator/tasks/gitleaks.py +2 -2
  74. secator/tasks/gospider.py +7 -1
  75. secator/tasks/grype.py +5 -4
  76. secator/tasks/h8mail.py +2 -1
  77. secator/tasks/httpx.py +18 -5
  78. secator/tasks/katana.py +35 -15
  79. secator/tasks/maigret.py +4 -4
  80. secator/tasks/mapcidr.py +3 -3
  81. secator/tasks/msfconsole.py +4 -4
  82. secator/tasks/naabu.py +5 -4
  83. secator/tasks/nmap.py +12 -14
  84. secator/tasks/nuclei.py +3 -3
  85. secator/tasks/searchsploit.py +6 -5
  86. secator/tasks/subfinder.py +2 -2
  87. secator/tasks/testssl.py +264 -263
  88. secator/tasks/trivy.py +5 -5
  89. secator/tasks/wafw00f.py +21 -3
  90. secator/tasks/wpprobe.py +90 -83
  91. secator/tasks/wpscan.py +6 -5
  92. secator/template.py +218 -104
  93. secator/thread.py +15 -15
  94. secator/tree.py +196 -0
  95. secator/utils.py +131 -123
  96. secator/utils_test.py +60 -19
  97. secator/workflows/__init__.py +2 -2
  98. {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/METADATA +37 -36
  99. secator-0.16.0.dist-info/RECORD +132 -0
  100. secator/configs/profiles/default.yaml +0 -8
  101. secator/configs/workflows/url_nuclei.yaml +0 -11
  102. secator/tasks/dnsxbrute.py +0 -42
  103. secator-0.15.0.dist-info/RECORD +0 -128
  104. {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
  105. {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
  106. {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/licenses/LICENSE +0 -0
secator/tree.py ADDED
@@ -0,0 +1,196 @@
1
+ from typing import List, Optional, Union
2
+ from secator.template import TemplateLoader
3
+ from dotmap import DotMap
4
+
5
+
6
+ DEFAULT_RENDER_OPTS = {
7
+ 'group': lambda x: f"[dim]group {x.name.split('/')[-1] if '/' in x.name else ''}[/]",
8
+ 'task': lambda x: f"[bold gold3]:wrench: {x.name}[/]",
9
+ 'workflow': lambda x: f"[bold dark_orange3]:gear: {x.name}[/]",
10
+ 'scan': lambda x: f"[bold red]:magnifying_glass_tilted_left: {x.name}[/]",
11
+ 'condition': lambda x: f"[dim cyan]# if {x}[/]" if x else ''
12
+ }
13
+
14
+
15
+ class TaskNode:
16
+ """Represents a node in the workflow/scan task tree."""
17
+ def __init__(self, name: str, type_: str, id: str, opts: Optional[dict] = None, default_opts: Optional[dict] = None, condition: Optional[str] = None, description: Optional[str] = None, parent=None, ancestor=None): # noqa: E501
18
+ self.name = name
19
+ self.type = type_
20
+ self.id = id
21
+ self.opts = opts or {}
22
+ self.default_opts = default_opts or {}
23
+ self.description = description
24
+ self.condition = condition
25
+ self.children: List[TaskNode] = []
26
+ self.parent = parent
27
+ self.ancestor = ancestor
28
+
29
+ def add_child(self, child: 'TaskNode') -> None:
30
+ """Add a child node to this node."""
31
+ self.children.append(child)
32
+
33
+ def remove(self):
34
+ """Remove this node from its parent."""
35
+ if self.parent:
36
+ self.parent.children.remove(self)
37
+
38
+ def __str__(self) -> str:
39
+ """String representation with condition if present."""
40
+ if self.condition:
41
+ return f"{self.name} # if {self.condition}"
42
+ return self.name
43
+
44
+
45
+ class RunnerTree:
46
+ """Represents a tree of workflow/scan tasks."""
47
+ def __init__(self, name: str, type_: str, render_opts: Optional[dict] = DEFAULT_RENDER_OPTS):
48
+ self.name = name
49
+ self.type = type_
50
+ self.root_nodes: List[TaskNode] = []
51
+ self.render_opts = render_opts
52
+
53
+ def add_root_node(self, node: TaskNode) -> None:
54
+ """Add a root-level node to the tree."""
55
+ self.root_nodes.append(node)
56
+
57
+ def render_tree(self) -> str:
58
+ """Render the tree as a console-friendly string."""
59
+ lines = []
60
+ for node in self.root_nodes:
61
+ node_str = self.render_opts.get(node.type, lambda x: str(x))(node)
62
+ condition_str = self.render_opts.get('condition', lambda x: str(x) if x else '')(node.condition)
63
+ if condition_str:
64
+ node_str = f"{node_str} {condition_str}"
65
+ lines.append(node_str)
66
+ self._render_children(node, "", lines)
67
+ return "\n".join(lines)
68
+
69
+ def _render_children(self, node: TaskNode, prefix: str, lines: List[str]) -> None:
70
+ """Helper method to recursively render child nodes."""
71
+ children_count = len(node.children)
72
+ for i, child in enumerate(node.children):
73
+ is_last = i == children_count - 1
74
+ branch = "└─ " if is_last else "├─ "
75
+ child_str = self.render_opts.get(child.type, lambda x: str(x))
76
+ condition_str = self.render_opts.get('condition', lambda x: str(x) if x else '')(child.condition)
77
+ render_str = f"{prefix}{branch}{child_str(child)}"
78
+ if child.description:
79
+ render_str += f" - [dim]{child.description}[/]"
80
+ if condition_str:
81
+ render_str += f" {condition_str}"
82
+ lines.append(render_str)
83
+ if child.children:
84
+ new_prefix = prefix + (" " if is_last else "│ ")
85
+ self._render_children(child, new_prefix, lines)
86
+
87
+ def get_subtree(self, node: TaskNode) -> 'RunnerTree':
88
+ """Get the subtree of this node."""
89
+ subtree = RunnerTree(node.name, node.type)
90
+ for child in node.children:
91
+ subtree.add_root_node(child)
92
+ return subtree
93
+
94
+
95
+ def build_runner_tree(config: DotMap, condition: Optional[str] = None, parent: Optional[TaskNode] = None, ancestor: Optional[TaskNode] = None) -> Union[RunnerTree, str]: # noqa: E501
96
+ """
97
+ Build a tree representation from a runner config.
98
+
99
+ Args:
100
+ config (DotMap): The runner config.
101
+
102
+ Returns:
103
+ A RunnerTree object or an error message string
104
+ """
105
+ tree = RunnerTree(config.name, config.type)
106
+
107
+ if config.type == 'workflow':
108
+ root_node = TaskNode(config.name, 'workflow', config.name, opts=config.options, default_opts=config.default_options, condition=condition, parent=parent, ancestor=ancestor) # noqa: E501
109
+ tree.add_root_node(root_node)
110
+
111
+ # Add tasks to the tree
112
+ for task_name, task_details in config.tasks.items():
113
+ id = f'{config.name}.{task_name}'
114
+ if task_name.startswith('_group'):
115
+ group_node = TaskNode(task_name, 'group', id, parent=root_node, ancestor=root_node)
116
+ root_node.add_child(group_node)
117
+ for subtask_name, subtask_details in task_details.items():
118
+ subtask_details = subtask_details or {}
119
+ id = f'{config.name}.{subtask_name}'
120
+ condition = subtask_details.get('if')
121
+ description = subtask_details.get('description')
122
+ subtask_node = TaskNode(subtask_name, 'task', id, opts=subtask_details, condition=condition, description=description, parent=group_node, ancestor=root_node) # noqa: E501
123
+ group_node.add_child(subtask_node)
124
+ else:
125
+ condition = task_details.get('if') if task_details else None
126
+ description = task_details.get('description') if task_details else None
127
+ task_node = TaskNode(task_name, 'task', id, opts=task_details, condition=condition, description=description, parent=root_node, ancestor=root_node) # noqa: E501
128
+ root_node.add_child(task_node)
129
+
130
+ elif config.type == 'scan':
131
+ id = f'{config.name}'
132
+ root_node = TaskNode(config.name, 'scan', id, opts=config.options, parent=parent)
133
+ tree.add_root_node(root_node)
134
+
135
+ # Add workflows to the tree
136
+ for workflow_name, workflow_details in config.workflows.items():
137
+ id = f'{config.name}.{workflow_name}'
138
+ condition = workflow_details.get('if') if isinstance(workflow_details, dict) else None
139
+ split_name = workflow_name.split('/')
140
+ wf_name = split_name[0]
141
+ wf_config = TemplateLoader(name=f'workflow/{wf_name}')
142
+ wf_config.name = workflow_name
143
+ wf_tree = build_runner_tree(wf_config, condition, parent=root_node, ancestor=root_node)
144
+ if isinstance(wf_tree, RunnerTree):
145
+ for wf_root_node in wf_tree.root_nodes:
146
+ root_node.add_child(wf_root_node)
147
+
148
+ elif config.type == 'task':
149
+ root_node = TaskNode(config.name, 'task', config.name, opts={}, parent=parent, ancestor=ancestor)
150
+ tree.add_root_node(root_node)
151
+
152
+ return tree
153
+
154
+
155
+ def walk_runner_tree(tree: RunnerTree, visit_func):
156
+ """
157
+ Walk the RunnerTree and visit each node.
158
+
159
+ Args:
160
+ tree (RunnerTree): The RunnerTree to walk.
161
+ visit_func (function): A function to call on each node.
162
+ """
163
+ for root_node in tree.root_nodes:
164
+ _walk_node(root_node, visit_func)
165
+
166
+
167
+ def _walk_node(node: TaskNode, visit_func):
168
+ """
169
+ Recursively walk the node and its children.
170
+
171
+ Args:
172
+ node (TaskNode): The node to walk.
173
+ visit_func (function): A function to call on each node.
174
+ """
175
+ visit_func(node)
176
+ for child in node.children:
177
+ _walk_node(child, visit_func)
178
+
179
+
180
+ def get_flat_node_list(tree: RunnerTree) -> List[TaskNode]:
181
+ """
182
+ Get the flat list of all nodes in the RunnerTree.
183
+
184
+ Args:
185
+ tree (RunnerTree): The RunnerTree to traverse.
186
+
187
+ Returns:
188
+ List[TaskNode]: The list of all nodes in the tree.
189
+ """
190
+ nodes = []
191
+
192
+ def collect_node(node: TaskNode):
193
+ nodes.append(node)
194
+
195
+ walk_runner_tree(tree, collect_node)
196
+ return nodes
secator/utils.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import fnmatch
2
- import inspect
3
2
  import importlib
3
+ import ipaddress
4
4
  import itertools
5
5
  import json
6
6
  import logging
@@ -15,9 +15,7 @@ import warnings
15
15
 
16
16
  from datetime import datetime, timedelta
17
17
  from functools import reduce
18
- from inspect import isclass
19
18
  from pathlib import Path
20
- from pkgutil import iter_modules
21
19
  from time import time
22
20
  import traceback
23
21
  from urllib.parse import urlparse, quote
@@ -26,7 +24,8 @@ import humanize
26
24
  import ifaddr
27
25
  import yaml
28
26
 
29
- from secator.definitions import (DEBUG_COMPONENT, VERSION, DEV_PACKAGE)
27
+ from secator.definitions import (DEBUG, VERSION, DEV_PACKAGE, IP, HOST, CIDR_RANGE,
28
+ MAC_ADDRESS, SLUG, UUID, EMAIL, IBAN, URL, PATH, HOST_PORT)
30
29
  from secator.config import CONFIG, ROOT_FOLDER, LIB_FOLDER, download_file
31
30
  from secator.rich import console
32
31
 
@@ -73,18 +72,21 @@ def expand_input(input, ctx):
73
72
  Returns:
74
73
  str: Input.
75
74
  """
75
+ piped_input = ctx.obj['piped_input']
76
+ dry_run = ctx.obj['dry_run']
76
77
  if input is None: # read from stdin
77
- if not ctx.obj['piped_input']:
78
- console.print('Waiting for input on stdin ...', style='bold yellow')
79
- rlist, _, _ = select.select([sys.stdin], [], [], CONFIG.cli.stdin_timeout)
80
- if rlist:
81
- data = sys.stdin.read().splitlines()
82
- else:
83
- console.print(
84
- 'No input passed on stdin. Showing help page.',
85
- style='bold red')
86
- return None
87
- return data
78
+ if not piped_input and not dry_run:
79
+ console.print('No input passed on stdin. Showing help page.', style='bold red')
80
+ ctx.get_help()
81
+ sys.exit(1)
82
+ elif piped_input:
83
+ rlist, _, _ = select.select([sys.stdin], [], [], CONFIG.cli.stdin_timeout)
84
+ if rlist:
85
+ data = sys.stdin.read().splitlines()
86
+ return data
87
+ else:
88
+ console.print('No input passed on stdin.', style='bold red')
89
+ sys.exit(1)
88
90
  elif os.path.exists(input):
89
91
  if os.path.isfile(input):
90
92
  with open(input, 'r') as f:
@@ -99,6 +101,9 @@ def expand_input(input, ctx):
99
101
  if isinstance(input, list) and len(input) == 1:
100
102
  return input[0]
101
103
 
104
+ if ctx.obj['dry_run'] and not input:
105
+ return ['TARGET']
106
+
102
107
  return input
103
108
 
104
109
 
@@ -140,75 +145,6 @@ def deduplicate(array, attr=None):
140
145
  return sorted(list(dict.fromkeys(array)))
141
146
 
142
147
 
143
- def discover_internal_tasks():
144
- """Find internal secator tasks."""
145
- from secator.runners import Runner
146
- package_dir = Path(__file__).resolve().parent / 'tasks'
147
- task_classes = []
148
- for (_, module_name, _) in iter_modules([str(package_dir)]):
149
- if module_name.startswith('_'):
150
- continue
151
- try:
152
- module = importlib.import_module(f'secator.tasks.{module_name}')
153
- except ImportError as e:
154
- console.print(f'[bold red]Could not import secator.tasks.{module_name}:[/]')
155
- console.print(f'\t[bold red]{type(e).__name__}[/]: {str(e)}')
156
- continue
157
- for attribute_name in dir(module):
158
- attribute = getattr(module, attribute_name)
159
- if isclass(attribute):
160
- bases = inspect.getmro(attribute)
161
- if Runner in bases and hasattr(attribute, '__task__'):
162
- task_classes.append(attribute)
163
-
164
- # Sort task_classes by category
165
- task_classes = sorted(
166
- task_classes,
167
- # key=lambda x: (get_command_category(x), x.__name__)
168
- key=lambda x: x.__name__)
169
-
170
- return task_classes
171
-
172
-
173
- def discover_external_tasks():
174
- """Find external secator tasks."""
175
- output = []
176
- sys.dont_write_bytecode = True
177
- for path in CONFIG.dirs.templates.glob('**/*.py'):
178
- try:
179
- task_name = path.stem
180
- module_name = f'secator.tasks.{task_name}'
181
-
182
- # console.print(f'Importing module {module_name} from {path}')
183
- spec = importlib.util.spec_from_file_location(module_name, path)
184
- module = importlib.util.module_from_spec(spec)
185
- # console.print(f'Adding module "{module_name}" to sys path')
186
- sys.modules[module_name] = module
187
-
188
- # console.print(f'Executing module "{module}"')
189
- spec.loader.exec_module(module)
190
-
191
- # console.print(f'Checking that {module} contains task {task_name}')
192
- if not hasattr(module, task_name):
193
- console.print(f'[bold orange1]Could not load external task "{task_name}" from module {path.name}[/] ({path})')
194
- continue
195
- cls = getattr(module, task_name)
196
- console.print(f'[bold green]Successfully loaded external task "{task_name}"[/] ({path})')
197
- output.append(cls)
198
- except Exception as e:
199
- console.print(f'[bold red]Could not load external module {path.name}. Reason: {str(e)}.[/] ({path})')
200
- sys.dont_write_bytecode = False
201
- return output
202
-
203
-
204
- def discover_tasks():
205
- """Find all secator tasks (internal + external)."""
206
- global _tasks
207
- if not _tasks:
208
- _tasks = discover_internal_tasks() + discover_external_tasks()
209
- return _tasks
210
-
211
-
212
148
  def import_dynamic(path, name=None):
213
149
  """Import class or module dynamically from path.
214
150
 
@@ -238,22 +174,6 @@ def import_dynamic(path, name=None):
238
174
  return None
239
175
 
240
176
 
241
- def get_command_cls(cls_name):
242
- """Get secator command by class name.
243
-
244
- Args:
245
- cls_name (str): Class name to load.
246
-
247
- Returns:
248
- cls: Class.
249
- """
250
- tasks_classes = discover_tasks()
251
- for task_cls in tasks_classes:
252
- if task_cls.__name__ == cls_name:
253
- return task_cls
254
- return None
255
-
256
-
257
177
  def get_command_category(command):
258
178
  """Get the category of a command.
259
179
 
@@ -383,7 +303,7 @@ def rich_to_ansi(text):
383
303
  tmp_console.print(text, end='', soft_wrap=True)
384
304
  return capture.get()
385
305
  except Exception:
386
- console.print(f'[bold red]Could not convert rich text to ansi: {text}[/]', highlight=False, markup=False)
306
+ print(f'Could not convert rich text to ansi: {text}[/]', file=sys.stderr)
387
307
  return text
388
308
 
389
309
 
@@ -397,11 +317,11 @@ def rich_escape(obj):
397
317
  any: Initial object, or escaped Rich string.
398
318
  """
399
319
  if isinstance(obj, str):
400
- return obj.replace('[', r'\[').replace(']', r'\]')
320
+ return obj.replace('[', r'\[').replace(']', r'\]').replace(r'\[/', r'\[\/')
401
321
  return obj
402
322
 
403
323
 
404
- def format_object(obj, obj_breaklines=False):
324
+ def format_debug_object(obj, obj_breaklines=False):
405
325
  """Format the debug object for printing.
406
326
 
407
327
  Args:
@@ -413,7 +333,7 @@ def format_object(obj, obj_breaklines=False):
413
333
  """
414
334
  sep = '\n ' if obj_breaklines else ', '
415
335
  if isinstance(obj, dict):
416
- return sep.join(f'[dim cyan]{k}[/] [dim yellow]->[/] [dim green]{v}[/]' for k, v in obj.items() if v is not None) # noqa: E501
336
+ return sep.join(f'[bold blue]{k}[/] [yellow]->[/] [blue]{v}[/]' for k, v in obj.items() if v is not None) # noqa: E501
417
337
  elif isinstance(obj, list):
418
338
  return f'[dim green]{sep.join(obj)}[/]'
419
339
  return ''
@@ -421,21 +341,25 @@ def format_object(obj, obj_breaklines=False):
421
341
 
422
342
  def debug(msg, sub='', id='', obj=None, lazy=None, obj_after=True, obj_breaklines=False, verbose=False):
423
343
  """Print debug log if DEBUG >= level."""
424
- if not DEBUG_COMPONENT == ['all']:
425
- if not DEBUG_COMPONENT or DEBUG_COMPONENT == [""]:
344
+ if not DEBUG == ['all'] and not DEBUG == ['1']:
345
+ if not DEBUG or DEBUG == [""]:
426
346
  return
427
-
428
347
  if sub:
429
- if verbose and sub not in DEBUG_COMPONENT:
430
- sub = f'debug.{sub}'
431
- if not any(sub.startswith(s) for s in DEBUG_COMPONENT):
348
+ for s in DEBUG:
349
+ if '*' in s and re.match(s + '$', sub):
350
+ break
351
+ elif not verbose and sub.startswith(s):
352
+ break
353
+ elif verbose and sub == s:
354
+ break
355
+ else:
432
356
  return
433
357
 
434
358
  if lazy:
435
359
  msg = lazy(msg)
436
360
 
437
361
  formatted_msg = f'[yellow4]{sub:13s}[/] ' if sub else ''
438
- obj_str = format_object(obj, obj_breaklines) if obj else ''
362
+ obj_str = format_debug_object(obj, obj_breaklines) if obj else ''
439
363
 
440
364
  # Constructing the message string based on object position
441
365
  if obj_str and not obj_after:
@@ -446,7 +370,12 @@ def debug(msg, sub='', id='', obj=None, lazy=None, obj_after=True, obj_breakline
446
370
  if id:
447
371
  formatted_msg += rf' [italic gray11]\[{id}][/]'
448
372
 
449
- console.print(rf'[dim]\[[magenta4]DBG[/]] {formatted_msg}[/]')
373
+ try:
374
+ console.print(rf'[dim]\[[magenta4]DBG[/]] {formatted_msg}[/]', highlight=False)
375
+ except Exception:
376
+ console.print(rf'[dim]\[[magenta4]DBG[/]] <MARKUP_DISABLED>{rich_escape(formatted_msg)}</MARKUP_DISABLED>[/]', highlight=False) # noqa: E501
377
+ if 'rich' in DEBUG:
378
+ raise
450
379
 
451
380
 
452
381
  def escape_mongodb_url(url):
@@ -498,6 +427,7 @@ def extract_domain_info(input, domain_only=False):
498
427
 
499
428
  Args:
500
429
  input (str): An URL or FQDN.
430
+ domain_only (bool): Return only the registered domain name.
501
431
 
502
432
  Returns:
503
433
  tldextract.ExtractResult: Extracted info.
@@ -507,9 +437,9 @@ def extract_domain_info(input, domain_only=False):
507
437
  if not result or not result.domain or not result.suffix:
508
438
  return None
509
439
  if domain_only:
510
- if not validators.domain(result.registered_domain):
440
+ if not validators.domain(result.top_domain_under_public_suffix):
511
441
  return None
512
- return result.registered_domain
442
+ return result.top_domain_under_public_suffix
513
443
  return result
514
444
 
515
445
 
@@ -787,15 +717,12 @@ def process_wordlist(val):
787
717
  if template_wordlist:
788
718
  val = template_wordlist
789
719
 
790
- if Path(val).exists():
791
- return val
792
- else:
793
- return download_file(
794
- val,
795
- target_folder=CONFIG.dirs.wordlists,
796
- offline_mode=CONFIG.offline_mode,
797
- type='wordlist'
798
- )
720
+ return download_file(
721
+ val,
722
+ target_folder=CONFIG.dirs.wordlists,
723
+ offline_mode=CONFIG.offline_mode,
724
+ type='wordlist'
725
+ )
799
726
 
800
727
 
801
728
  def convert_functions_to_strings(data):
@@ -815,3 +742,84 @@ def convert_functions_to_strings(data):
815
742
  return json.dumps(data.__name__) # or use inspect.getsource(data) if you want the actual function code
816
743
  else:
817
744
  return data
745
+
746
+
747
+ def headers_to_dict(header_opt):
748
+ headers = {}
749
+ for header in header_opt.split(';;'):
750
+ split = header.strip().split(':')
751
+ key = split[0].strip()
752
+ val = ':'.join(split[1:]).strip()
753
+ headers[key] = val
754
+ return headers
755
+
756
+
757
+ def format_object(obj, color='magenta', skip_keys=[]):
758
+ if isinstance(obj, list) and obj:
759
+ return ' [' + ', '.join([f'[{color}]{rich_escape(item)}[/]' for item in obj]) + ']'
760
+ elif isinstance(obj, dict) and obj.keys():
761
+ obj = {k: v for k, v in obj.items() if k.lower().replace('-', '_') not in skip_keys}
762
+ if obj:
763
+ return ' [' + ', '.join([f'[bold {color}]{rich_escape(k)}[/]: [{color}]{rich_escape(v)}[/]' for k, v in obj.items()]) + ']' # noqa: E501
764
+ return ''
765
+
766
+
767
+ def autodetect_type(target):
768
+ """Autodetect the type of a target.
769
+
770
+ Args:
771
+ target (str): The target to autodetect the type of.
772
+
773
+ Returns:
774
+ str: The type of the target.
775
+ """
776
+ if validators.url(target, simple_host=True):
777
+ return URL
778
+ elif validate_cidr_range(target):
779
+ return CIDR_RANGE
780
+ elif validators.ipv4(target) or validators.ipv6(target) or target == 'localhost':
781
+ return IP
782
+ elif validators.domain(target):
783
+ return HOST
784
+ elif validators.domain(target.split(':')[0]):
785
+ return HOST_PORT
786
+ elif validators.mac_address(target):
787
+ return MAC_ADDRESS
788
+ elif validators.email(target):
789
+ return EMAIL
790
+ elif validators.iban(target):
791
+ return IBAN
792
+ elif validators.uuid(target):
793
+ return UUID
794
+ elif Path(target).exists():
795
+ return PATH
796
+ elif validators.slug(target):
797
+ return SLUG
798
+
799
+ return str(type(target).__name__).lower()
800
+
801
+
802
+ def validate_cidr_range(target):
803
+ if '/' not in target:
804
+ return False
805
+ try:
806
+ ipaddress.ip_network(target, False)
807
+ return True
808
+ except ValueError:
809
+ return False
810
+
811
+
812
+ def get_versions_from_string(string):
813
+ """Get versions from a string.
814
+
815
+ Args:
816
+ string (str): String to get versions from.
817
+
818
+ Returns:
819
+ list[str]: List of versions.
820
+ """
821
+ regex = r'v?[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
822
+ matches = re.findall(regex, string)
823
+ if not matches:
824
+ return []
825
+ return matches