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.
- secator/celery.py +40 -24
- secator/celery_signals.py +71 -68
- secator/celery_utils.py +43 -27
- secator/cli.py +520 -280
- secator/cli_helper.py +394 -0
- secator/click.py +87 -0
- secator/config.py +67 -39
- secator/configs/profiles/http_headless.yaml +6 -0
- secator/configs/profiles/http_record.yaml +6 -0
- secator/configs/profiles/tor.yaml +1 -1
- secator/configs/scans/domain.yaml +4 -2
- secator/configs/scans/host.yaml +1 -1
- secator/configs/scans/network.yaml +1 -4
- secator/configs/scans/subdomain.yaml +13 -1
- secator/configs/scans/url.yaml +1 -2
- secator/configs/workflows/cidr_recon.yaml +6 -4
- secator/configs/workflows/code_scan.yaml +1 -1
- secator/configs/workflows/host_recon.yaml +29 -3
- secator/configs/workflows/subdomain_recon.yaml +67 -16
- secator/configs/workflows/url_crawl.yaml +44 -15
- secator/configs/workflows/url_dirsearch.yaml +4 -4
- secator/configs/workflows/url_fuzz.yaml +25 -17
- secator/configs/workflows/url_params_fuzz.yaml +7 -0
- secator/configs/workflows/url_vuln.yaml +33 -8
- secator/configs/workflows/user_hunt.yaml +2 -1
- secator/configs/workflows/wordpress.yaml +5 -3
- secator/cve.py +718 -0
- secator/decorators.py +0 -454
- secator/definitions.py +49 -30
- secator/exporters/_base.py +2 -2
- secator/exporters/console.py +2 -2
- secator/exporters/table.py +4 -3
- secator/exporters/txt.py +1 -1
- secator/hooks/mongodb.py +2 -4
- secator/installer.py +77 -49
- secator/loader.py +116 -0
- secator/output_types/_base.py +3 -0
- secator/output_types/certificate.py +63 -63
- secator/output_types/error.py +4 -5
- secator/output_types/info.py +2 -2
- secator/output_types/ip.py +3 -1
- secator/output_types/progress.py +5 -9
- secator/output_types/state.py +17 -17
- secator/output_types/tag.py +3 -0
- secator/output_types/target.py +10 -2
- secator/output_types/url.py +19 -7
- secator/output_types/vulnerability.py +11 -7
- secator/output_types/warning.py +2 -2
- secator/report.py +27 -15
- secator/rich.py +18 -10
- secator/runners/_base.py +447 -234
- secator/runners/_helpers.py +133 -24
- secator/runners/command.py +182 -102
- secator/runners/scan.py +33 -5
- secator/runners/task.py +13 -7
- secator/runners/workflow.py +105 -72
- secator/scans/__init__.py +2 -2
- secator/serializers/dataclass.py +20 -20
- secator/tasks/__init__.py +4 -4
- secator/tasks/_categories.py +39 -27
- secator/tasks/arjun.py +9 -5
- secator/tasks/bbot.py +53 -21
- secator/tasks/bup.py +19 -5
- secator/tasks/cariddi.py +24 -3
- secator/tasks/dalfox.py +26 -7
- secator/tasks/dirsearch.py +10 -4
- secator/tasks/dnsx.py +70 -25
- secator/tasks/feroxbuster.py +11 -3
- secator/tasks/ffuf.py +42 -6
- secator/tasks/fping.py +20 -8
- secator/tasks/gau.py +3 -1
- secator/tasks/gf.py +5 -4
- secator/tasks/gitleaks.py +2 -2
- secator/tasks/gospider.py +7 -1
- secator/tasks/grype.py +5 -4
- secator/tasks/h8mail.py +2 -1
- secator/tasks/httpx.py +18 -5
- secator/tasks/katana.py +35 -15
- secator/tasks/maigret.py +4 -4
- secator/tasks/mapcidr.py +3 -3
- secator/tasks/msfconsole.py +4 -4
- secator/tasks/naabu.py +5 -4
- secator/tasks/nmap.py +12 -14
- secator/tasks/nuclei.py +3 -3
- secator/tasks/searchsploit.py +6 -5
- secator/tasks/subfinder.py +2 -2
- secator/tasks/testssl.py +264 -263
- secator/tasks/trivy.py +5 -5
- secator/tasks/wafw00f.py +21 -3
- secator/tasks/wpprobe.py +90 -83
- secator/tasks/wpscan.py +6 -5
- secator/template.py +218 -104
- secator/thread.py +15 -15
- secator/tree.py +196 -0
- secator/utils.py +131 -123
- secator/utils_test.py +60 -19
- secator/workflows/__init__.py +2 -2
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/METADATA +37 -36
- secator-0.16.0.dist-info/RECORD +132 -0
- secator/configs/profiles/default.yaml +0 -8
- secator/configs/workflows/url_nuclei.yaml +0 -11
- secator/tasks/dnsxbrute.py +0 -42
- secator-0.15.0.dist-info/RECORD +0 -128
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
- {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 (
|
|
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
|
|
78
|
-
console.print('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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'[
|
|
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
|
|
425
|
-
if not
|
|
344
|
+
if not DEBUG == ['all'] and not DEBUG == ['1']:
|
|
345
|
+
if not DEBUG or DEBUG == [""]:
|
|
426
346
|
return
|
|
427
|
-
|
|
428
347
|
if sub:
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
440
|
+
if not validators.domain(result.top_domain_under_public_suffix):
|
|
511
441
|
return None
|
|
512
|
-
return result.
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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
|