secator 0.22.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.
- secator/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +453 -0
- secator/celery_signals.py +138 -0
- secator/celery_utils.py +320 -0
- secator/cli.py +2035 -0
- secator/cli_helper.py +395 -0
- secator/click.py +87 -0
- secator/config.py +670 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +8 -0
- secator/configs/profiles/all_ports.yaml +7 -0
- secator/configs/profiles/full.yaml +31 -0
- secator/configs/profiles/http_headless.yaml +7 -0
- secator/configs/profiles/http_record.yaml +8 -0
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/passive.yaml +11 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +31 -0
- secator/configs/scans/host.yaml +23 -0
- secator/configs/scans/network.yaml +30 -0
- secator/configs/scans/subdomain.yaml +27 -0
- secator/configs/scans/url.yaml +19 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +48 -0
- secator/configs/workflows/code_scan.yaml +29 -0
- secator/configs/workflows/domain_recon.yaml +46 -0
- secator/configs/workflows/host_recon.yaml +95 -0
- secator/configs/workflows/subdomain_recon.yaml +120 -0
- secator/configs/workflows/url_bypass.yaml +15 -0
- secator/configs/workflows/url_crawl.yaml +98 -0
- secator/configs/workflows/url_dirsearch.yaml +62 -0
- secator/configs/workflows/url_fuzz.yaml +68 -0
- secator/configs/workflows/url_params_fuzz.yaml +66 -0
- secator/configs/workflows/url_secrets_hunt.yaml +23 -0
- secator/configs/workflows/url_vuln.yaml +91 -0
- secator/configs/workflows/user_hunt.yaml +29 -0
- secator/configs/workflows/wordpress.yaml +38 -0
- secator/cve.py +718 -0
- secator/decorators.py +7 -0
- secator/definitions.py +168 -0
- secator/exporters/__init__.py +14 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +37 -0
- secator/exporters/gdrive.py +123 -0
- secator/exporters/json.py +16 -0
- secator/exporters/table.py +36 -0
- secator/exporters/txt.py +28 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/gcs.py +80 -0
- secator/hooks/mongodb.py +281 -0
- secator/installer.py +694 -0
- secator/loader.py +128 -0
- secator/output_types/__init__.py +49 -0
- secator/output_types/_base.py +108 -0
- secator/output_types/certificate.py +78 -0
- secator/output_types/domain.py +50 -0
- secator/output_types/error.py +42 -0
- secator/output_types/exploit.py +58 -0
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +47 -0
- secator/output_types/port.py +55 -0
- secator/output_types/progress.py +36 -0
- secator/output_types/record.py +36 -0
- secator/output_types/stat.py +41 -0
- secator/output_types/state.py +29 -0
- secator/output_types/subdomain.py +45 -0
- secator/output_types/tag.py +69 -0
- secator/output_types/target.py +38 -0
- secator/output_types/url.py +112 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +101 -0
- secator/output_types/warning.py +30 -0
- secator/report.py +140 -0
- secator/rich.py +130 -0
- secator/runners/__init__.py +14 -0
- secator/runners/_base.py +1240 -0
- secator/runners/_helpers.py +218 -0
- secator/runners/celery.py +18 -0
- secator/runners/command.py +1178 -0
- secator/runners/python.py +126 -0
- secator/runners/scan.py +87 -0
- secator/runners/task.py +81 -0
- secator/runners/workflow.py +168 -0
- secator/scans/__init__.py +29 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +39 -0
- secator/serializers/json.py +45 -0
- secator/serializers/regex.py +25 -0
- secator/tasks/__init__.py +8 -0
- secator/tasks/_categories.py +487 -0
- secator/tasks/arjun.py +113 -0
- secator/tasks/arp.py +53 -0
- secator/tasks/arpscan.py +70 -0
- secator/tasks/bbot.py +372 -0
- secator/tasks/bup.py +118 -0
- secator/tasks/cariddi.py +193 -0
- secator/tasks/dalfox.py +87 -0
- secator/tasks/dirsearch.py +84 -0
- secator/tasks/dnsx.py +186 -0
- secator/tasks/feroxbuster.py +93 -0
- secator/tasks/ffuf.py +135 -0
- secator/tasks/fping.py +85 -0
- secator/tasks/gau.py +102 -0
- secator/tasks/getasn.py +60 -0
- secator/tasks/gf.py +36 -0
- secator/tasks/gitleaks.py +96 -0
- secator/tasks/gospider.py +84 -0
- secator/tasks/grype.py +109 -0
- secator/tasks/h8mail.py +75 -0
- secator/tasks/httpx.py +167 -0
- secator/tasks/jswhois.py +36 -0
- secator/tasks/katana.py +203 -0
- secator/tasks/maigret.py +87 -0
- secator/tasks/mapcidr.py +42 -0
- secator/tasks/msfconsole.py +179 -0
- secator/tasks/naabu.py +85 -0
- secator/tasks/nmap.py +487 -0
- secator/tasks/nuclei.py +151 -0
- secator/tasks/search_vulns.py +225 -0
- secator/tasks/searchsploit.py +109 -0
- secator/tasks/sshaudit.py +299 -0
- secator/tasks/subfinder.py +48 -0
- secator/tasks/testssl.py +283 -0
- secator/tasks/trivy.py +130 -0
- secator/tasks/trufflehog.py +240 -0
- secator/tasks/urlfinder.py +100 -0
- secator/tasks/wafw00f.py +106 -0
- secator/tasks/whois.py +34 -0
- secator/tasks/wpprobe.py +116 -0
- secator/tasks/wpscan.py +202 -0
- secator/tasks/x8.py +94 -0
- secator/tasks/xurlfind3r.py +83 -0
- secator/template.py +294 -0
- secator/thread.py +24 -0
- secator/tree.py +196 -0
- secator/utils.py +922 -0
- secator/utils_test.py +297 -0
- secator/workflows/__init__.py +29 -0
- secator-0.22.0.dist-info/METADATA +447 -0
- secator-0.22.0.dist-info/RECORD +150 -0
- secator-0.22.0.dist-info/WHEEL +4 -0
- secator-0.22.0.dist-info/entry_points.txt +2 -0
- secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/cli_helper.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from contextlib import nullcontext
|
|
8
|
+
|
|
9
|
+
import psutil
|
|
10
|
+
import rich_click as click
|
|
11
|
+
from rich_click.rich_click import _get_rich_console
|
|
12
|
+
|
|
13
|
+
from secator.config import CONFIG
|
|
14
|
+
from secator.click import CLICK_LIST
|
|
15
|
+
from secator.definitions import ADDONS_ENABLED
|
|
16
|
+
from secator.runners import Scan, Task, Workflow
|
|
17
|
+
from secator.template import get_config_options
|
|
18
|
+
from secator.tree import build_runner_tree
|
|
19
|
+
from secator.utils import (deduplicate, expand_input, get_command_category)
|
|
20
|
+
from secator.loader import get_configs_by_type
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
WORKSPACES = next(os.walk(CONFIG.dirs.reports))[1]
|
|
24
|
+
WORKSPACES_STR = '|'.join([f'[dim yellow3]{_}[/]' for _ in WORKSPACES])
|
|
25
|
+
PROFILES_STR = ','.join([f'[dim yellow3]{_.name}[/]' for _ in get_configs_by_type('profile')])
|
|
26
|
+
DRIVERS_STR = ','.join([f'[dim yellow3]{_}[/]' for _ in ['mongodb', 'gcs']])
|
|
27
|
+
DRIVER_DEFAULTS_STR = ','.join(CONFIG.drivers.defaults) if CONFIG.drivers.defaults else None
|
|
28
|
+
PROFILE_DEFAULTS_STR = ','.join(CONFIG.profiles.defaults) if CONFIG.profiles.defaults else None
|
|
29
|
+
EXPORTERS_STR = ','.join([f'[dim yellow3]{_}[/]' for _ in ['csv', 'gdrive', 'json', 'table', 'txt']])
|
|
30
|
+
|
|
31
|
+
CLI_OUTPUT_OPTS = {
|
|
32
|
+
'output': {'type': str, 'default': None, 'help': f'Output options [{EXPORTERS_STR}] [dim orange4](comma-separated)[/]', 'short': 'o'}, # noqa: E501
|
|
33
|
+
'fmt': {'default': '', 'short': 'fmt', 'internal_name': 'print_format', 'help': 'Output formatting string'},
|
|
34
|
+
'json': {'is_flag': True, 'short': 'json', 'internal_name': 'print_json', 'default': False, 'help': 'Print items as JSON lines'}, # noqa: E501
|
|
35
|
+
'raw': {'is_flag': True, 'short': 'raw', 'internal_name': 'print_raw', 'default': False, 'help': 'Print items in raw format'}, # noqa: E501
|
|
36
|
+
'stat': {'is_flag': True, 'short': 'stat', 'internal_name': 'print_stat', 'default': False, 'help': 'Print runtime statistics'}, # noqa: E501
|
|
37
|
+
'quiet': {'is_flag': True, 'short': 'q', 'default': not CONFIG.cli.show_command_output, 'opposite': 'verbose', 'help': 'Hide or show original command output'}, # noqa: E501
|
|
38
|
+
'yaml': {'is_flag': True, 'short': 'yaml', 'default': False, 'help': 'Show runner yaml'},
|
|
39
|
+
'tree': {'is_flag': True, 'short': 'tree', 'default': False, 'help': 'Show runner tree'},
|
|
40
|
+
'dry_run': {'is_flag': True, 'short': 'dry', 'default': False, 'help': 'Show dry run'},
|
|
41
|
+
'process': {'is_flag': True, 'short': 'ps', 'default': True, 'help': 'Enable / disable secator processing', 'reverse': True}, # noqa: E501
|
|
42
|
+
'version': {'is_flag': True, 'help': 'Show runner version'},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
CLI_EXEC_OPTS = {
|
|
46
|
+
'workspace': {'type': str, 'default': 'default', 'help': f'Workspace [{WORKSPACES_STR}|[dim orange4]<new>[/]]', 'short': 'ws'}, # noqa: E501
|
|
47
|
+
'profiles': {'type': str, 'help': f'Profiles [{PROFILES_STR}] [dim orange4](comma-separated)[/]', 'default': PROFILE_DEFAULTS_STR, 'short': 'pf'}, # noqa: E501
|
|
48
|
+
'driver': {'type': str, 'help': f'Drivers [{DRIVERS_STR}] [dim orange4](comma-separated)[/]', 'default': DRIVER_DEFAULTS_STR}, # noqa: E501
|
|
49
|
+
'sync': {'is_flag': True, 'help': 'Run tasks locally or in worker', 'opposite': 'worker'},
|
|
50
|
+
'no_poll': {'is_flag': True, 'short': 'np', 'default': False, 'help': 'Do not live poll for tasks results when running in worker'}, # noqa: E501
|
|
51
|
+
'enable_pyinstrument': {'is_flag': True, 'short': 'pyinstrument', 'default': False, 'help': 'Enable pyinstrument profiling'}, # noqa: E501
|
|
52
|
+
'enable_memray': {'is_flag': True, 'short': 'memray', 'default': False, 'help': 'Enable memray profiling'},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
CLI_TYPE_MAPPING = {
|
|
56
|
+
'str': str,
|
|
57
|
+
'list': CLICK_LIST,
|
|
58
|
+
'int': int,
|
|
59
|
+
'float': float,
|
|
60
|
+
# 'choice': click.Choice,
|
|
61
|
+
# 'file': click.Path(exists=True, file_okay=True, dir_okay=False, readable=True),
|
|
62
|
+
# 'dir': click.Path(exists=True, file_okay=False, dir_okay=True, readable=True),
|
|
63
|
+
# 'path': click.Path(exists=True, file_okay=True, dir_okay=True, readable=True),
|
|
64
|
+
# 'url': click.URL,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
DEFAULT_CLI_OPTIONS = list(CLI_OUTPUT_OPTS.keys()) + list(CLI_EXEC_OPTS.keys())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def decorate_command_options(opts):
|
|
71
|
+
"""Add click.option decorator to decorate click command.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
opts (dict): Dict of command options.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
function: Decorator.
|
|
78
|
+
"""
|
|
79
|
+
def decorator(f):
|
|
80
|
+
reversed_opts = OrderedDict(list(opts.items())[::-1])
|
|
81
|
+
for opt_name, opt_conf in reversed_opts.items():
|
|
82
|
+
conf = opt_conf.copy()
|
|
83
|
+
short_opt = conf.pop('short', None)
|
|
84
|
+
internal = conf.pop('internal', False)
|
|
85
|
+
display = conf.pop('display', True)
|
|
86
|
+
internal_name = conf.pop('internal_name', None)
|
|
87
|
+
if internal and not display:
|
|
88
|
+
continue
|
|
89
|
+
conf.pop('shlex', None)
|
|
90
|
+
conf.pop('meta', None)
|
|
91
|
+
conf.pop('supported', None)
|
|
92
|
+
conf.pop('process', None)
|
|
93
|
+
conf.pop('pre_process', None)
|
|
94
|
+
conf.pop('requires_sudo', None)
|
|
95
|
+
conf.pop('prefix', None)
|
|
96
|
+
applies_to = conf.pop('applies_to', None)
|
|
97
|
+
default_from = conf.pop('default_from', None)
|
|
98
|
+
reverse = conf.pop('reverse', False)
|
|
99
|
+
opposite = conf.pop('opposite', None)
|
|
100
|
+
long = f'--{opt_name}'
|
|
101
|
+
short = f'-{short_opt}' if short_opt else f'-{opt_name}'
|
|
102
|
+
if reverse:
|
|
103
|
+
if opposite:
|
|
104
|
+
long += f'/--{opposite}'
|
|
105
|
+
short += f'/-{opposite}' if len(short) > 2 else f'/-{opposite[0]}'
|
|
106
|
+
conf['help'] = conf['help'].replace(opt_name, f'{opt_name} / {opposite}')
|
|
107
|
+
else:
|
|
108
|
+
long += f'/--no-{opt_name}'
|
|
109
|
+
short += f'/-n{short_opt}' if short_opt else f'/-n{opt_name}'
|
|
110
|
+
if applies_to:
|
|
111
|
+
applies_to_str = ", ".join(f'[bold yellow3]{_}[/]' for _ in applies_to)
|
|
112
|
+
conf['help'] += rf' \[[dim]{applies_to_str}[/]]'
|
|
113
|
+
if default_from:
|
|
114
|
+
conf['help'] += rf' \[[dim]default from: [dim yellow3]{default_from}[/][/]]'
|
|
115
|
+
args = [long, short]
|
|
116
|
+
if internal_name:
|
|
117
|
+
args.append(internal_name)
|
|
118
|
+
f = click.option(*args, **conf)(f)
|
|
119
|
+
return f
|
|
120
|
+
return decorator
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def generate_cli_subcommand(cli_endpoint, func, **opts):
|
|
124
|
+
return cli_endpoint.command(**opts)(func)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def register_runner(cli_endpoint, config):
|
|
128
|
+
name = config.name
|
|
129
|
+
input_types = []
|
|
130
|
+
command_opts = {
|
|
131
|
+
'no_args_is_help': True,
|
|
132
|
+
'context_settings': {
|
|
133
|
+
'ignore_unknown_options': False,
|
|
134
|
+
'allow_extra_args': False
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if cli_endpoint.name == 'scan':
|
|
139
|
+
runner_cls = Scan
|
|
140
|
+
short_help = config.description or ''
|
|
141
|
+
short_help += f' [dim]alias: {config.alias}' if config.alias else ''
|
|
142
|
+
command_opts.update({
|
|
143
|
+
'name': name,
|
|
144
|
+
'short_help': short_help,
|
|
145
|
+
'no_args_is_help': False
|
|
146
|
+
})
|
|
147
|
+
input_types = config.input_types
|
|
148
|
+
|
|
149
|
+
elif cli_endpoint.name == 'workflow':
|
|
150
|
+
runner_cls = Workflow
|
|
151
|
+
short_help = config.description or ''
|
|
152
|
+
short_help = f'{short_help:<55} [dim](alias)[/][bold cyan] {config.alias}' if config.alias else ''
|
|
153
|
+
command_opts.update({
|
|
154
|
+
'name': name,
|
|
155
|
+
'short_help': short_help,
|
|
156
|
+
'no_args_is_help': False
|
|
157
|
+
})
|
|
158
|
+
input_types = config.input_types
|
|
159
|
+
|
|
160
|
+
elif cli_endpoint.name == 'task':
|
|
161
|
+
runner_cls = Task
|
|
162
|
+
task_cls = Task.get_task_class(config.name)
|
|
163
|
+
task_category = get_command_category(task_cls)
|
|
164
|
+
short_help = f'[magenta]{task_category:<25}[/] {task_cls.__doc__}'
|
|
165
|
+
command_opts.update({
|
|
166
|
+
'name': name,
|
|
167
|
+
'short_help': short_help,
|
|
168
|
+
'no_args_is_help': False
|
|
169
|
+
})
|
|
170
|
+
input_types = task_cls.input_types
|
|
171
|
+
else:
|
|
172
|
+
raise ValueError(f"Unrecognized runner endpoint name {cli_endpoint.name}")
|
|
173
|
+
input_types_str = '|'.join(input_types) if input_types else 'targets'
|
|
174
|
+
default_inputs = None if config.default_inputs == {} else config.default_inputs
|
|
175
|
+
input_required = default_inputs is None
|
|
176
|
+
options = get_config_options(
|
|
177
|
+
config,
|
|
178
|
+
exec_opts=CLI_EXEC_OPTS,
|
|
179
|
+
output_opts=CLI_OUTPUT_OPTS,
|
|
180
|
+
type_mapping=CLI_TYPE_MAPPING
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# TODO: maybe allow this in the future
|
|
184
|
+
# def get_unknown_opts(ctx):
|
|
185
|
+
# return {
|
|
186
|
+
# (ctx.args[i][2:]
|
|
187
|
+
# if str(ctx.args[i]).startswith("--") \
|
|
188
|
+
# else ctx.args[i][1:]): ctx.args[i+1]
|
|
189
|
+
# for i in range(0, len(ctx.args), 2)
|
|
190
|
+
# }
|
|
191
|
+
|
|
192
|
+
@click.argument('inputs', metavar=input_types_str, required=False)
|
|
193
|
+
@decorate_command_options(options)
|
|
194
|
+
@click.pass_context
|
|
195
|
+
def func(ctx, **opts):
|
|
196
|
+
console = _get_rich_console()
|
|
197
|
+
version = opts['version']
|
|
198
|
+
sync = opts['sync']
|
|
199
|
+
ws = opts.pop('workspace')
|
|
200
|
+
driver = opts.pop('driver', '')
|
|
201
|
+
quiet = opts['quiet']
|
|
202
|
+
dry_run = opts['dry_run']
|
|
203
|
+
yaml = opts['yaml']
|
|
204
|
+
tree = opts['tree']
|
|
205
|
+
context = {'workspace_name': ws}
|
|
206
|
+
enable_pyinstrument = opts['enable_pyinstrument']
|
|
207
|
+
enable_memray = opts['enable_memray']
|
|
208
|
+
contextmanager = nullcontext()
|
|
209
|
+
process = None
|
|
210
|
+
|
|
211
|
+
# Set dry run
|
|
212
|
+
ctx.obj['dry_run'] = dry_run
|
|
213
|
+
ctx.obj['input_types'] = input_types
|
|
214
|
+
ctx.obj['input_required'] = input_required
|
|
215
|
+
ctx.obj['default_inputs'] = default_inputs
|
|
216
|
+
|
|
217
|
+
# Show version
|
|
218
|
+
if version:
|
|
219
|
+
if not cli_endpoint.name == 'task':
|
|
220
|
+
console.print(f'[bold red]Version information is not available for {cli_endpoint.name}.[/]')
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
data = task_cls.get_version_info()
|
|
223
|
+
current = data['version']
|
|
224
|
+
latest = data['latest_version']
|
|
225
|
+
installed = data['installed']
|
|
226
|
+
if not installed:
|
|
227
|
+
console.print(f'[bold red]{task_cls.__name__} is not installed.[/]')
|
|
228
|
+
else:
|
|
229
|
+
console.print(f'{task_cls.__name__} version: [bold green]{current}[/] (recommended: [bold green]{latest}[/])')
|
|
230
|
+
sys.exit(0)
|
|
231
|
+
|
|
232
|
+
# Show runner yaml
|
|
233
|
+
if yaml:
|
|
234
|
+
config.print()
|
|
235
|
+
sys.exit(0)
|
|
236
|
+
|
|
237
|
+
# Show runner tree
|
|
238
|
+
if tree:
|
|
239
|
+
tree = build_runner_tree(config)
|
|
240
|
+
console.print(tree.render_tree())
|
|
241
|
+
sys.exit(0)
|
|
242
|
+
|
|
243
|
+
# TODO: maybe allow this in the future
|
|
244
|
+
# unknown_opts = get_unknown_opts(ctx)
|
|
245
|
+
# opts.update(unknown_opts)
|
|
246
|
+
|
|
247
|
+
# Expand input
|
|
248
|
+
inputs = opts.pop('inputs')
|
|
249
|
+
inputs = expand_input(inputs, ctx)
|
|
250
|
+
|
|
251
|
+
# Build hooks from driver name
|
|
252
|
+
hooks = []
|
|
253
|
+
drivers = driver.split(',') if driver else []
|
|
254
|
+
drivers = list(set(CONFIG.drivers.defaults + drivers))
|
|
255
|
+
supported_drivers = ['mongodb', 'gcs']
|
|
256
|
+
for driver in drivers:
|
|
257
|
+
if driver in supported_drivers:
|
|
258
|
+
if not ADDONS_ENABLED[driver]:
|
|
259
|
+
console.print(f'[bold red]Missing "{driver}" addon: please run `secator install addons {driver}`[/].')
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
from secator.utils import import_dynamic
|
|
262
|
+
driver_hooks = import_dynamic(f'secator.hooks.{driver}', 'HOOKS')
|
|
263
|
+
if driver_hooks is None:
|
|
264
|
+
console.print(f'[bold red]Missing "secator.hooks.{driver}.HOOKS".[/]')
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
hooks.append(driver_hooks)
|
|
267
|
+
else:
|
|
268
|
+
supported_drivers_str = ', '.join([f'[bold green]{_}[/]' for _ in supported_drivers])
|
|
269
|
+
console.print(f'[bold red]Driver "{driver}" is not supported.[/]')
|
|
270
|
+
console.print(f'Supported drivers: {supported_drivers_str}')
|
|
271
|
+
sys.exit(1)
|
|
272
|
+
|
|
273
|
+
if enable_pyinstrument or enable_memray:
|
|
274
|
+
if not ADDONS_ENABLED["trace"]:
|
|
275
|
+
console.print(
|
|
276
|
+
'[bold red]Missing "trace" addon: please run `secator install addons trace`[/].'
|
|
277
|
+
)
|
|
278
|
+
sys.exit(1)
|
|
279
|
+
import memray
|
|
280
|
+
output_file = f'trace_memray_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}.bin'
|
|
281
|
+
contextmanager = memray.Tracker(output_file)
|
|
282
|
+
|
|
283
|
+
from secator.utils import deep_merge_dicts
|
|
284
|
+
hooks = deep_merge_dicts(*hooks)
|
|
285
|
+
|
|
286
|
+
# Enable sync or not
|
|
287
|
+
if sync or dry_run:
|
|
288
|
+
sync = True
|
|
289
|
+
else:
|
|
290
|
+
from secator.celery import is_celery_worker_alive
|
|
291
|
+
worker_alive = is_celery_worker_alive()
|
|
292
|
+
if not worker_alive and not sync:
|
|
293
|
+
sync = True
|
|
294
|
+
else:
|
|
295
|
+
sync = False
|
|
296
|
+
broker_protocol = CONFIG.celery.broker_url.split('://')[0]
|
|
297
|
+
backend_protocol = CONFIG.celery.result_backend.split('://')[0]
|
|
298
|
+
if CONFIG.celery.broker_url and \
|
|
299
|
+
(broker_protocol == 'redis' or backend_protocol == 'redis') \
|
|
300
|
+
and not ADDONS_ENABLED['redis']:
|
|
301
|
+
_get_rich_console().print('[bold red]Missing `redis` addon: please run `secator install addons redis`[/].')
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
|
|
304
|
+
from secator.utils import debug
|
|
305
|
+
debug('Run options', obj=opts, sub='cli')
|
|
306
|
+
|
|
307
|
+
# Set run options
|
|
308
|
+
opts.update({
|
|
309
|
+
'print_cmd': True,
|
|
310
|
+
'print_item': True,
|
|
311
|
+
'print_line': True,
|
|
312
|
+
'print_progress': True,
|
|
313
|
+
'print_profiles': True,
|
|
314
|
+
'print_start': True,
|
|
315
|
+
'print_target': True,
|
|
316
|
+
'print_end': True,
|
|
317
|
+
'print_remote_info': not sync,
|
|
318
|
+
'piped_input': ctx.obj['piped_input'],
|
|
319
|
+
'piped_output': ctx.obj['piped_output'],
|
|
320
|
+
'caller': 'cli',
|
|
321
|
+
'sync': sync,
|
|
322
|
+
'quiet': quiet
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
# Start runner
|
|
326
|
+
with contextmanager:
|
|
327
|
+
if enable_memray:
|
|
328
|
+
process = psutil.Process()
|
|
329
|
+
console.print(
|
|
330
|
+
f"[bold yellow3]Initial RAM Usage: {process.memory_info().rss / 1024 ** 2} MB[/]"
|
|
331
|
+
)
|
|
332
|
+
item_count = 0
|
|
333
|
+
runner = runner_cls(
|
|
334
|
+
config, inputs, run_opts=opts, hooks=hooks, context=context
|
|
335
|
+
)
|
|
336
|
+
for item in runner:
|
|
337
|
+
del item
|
|
338
|
+
item_count += 1
|
|
339
|
+
if process and item_count % 100 == 0:
|
|
340
|
+
console.print(
|
|
341
|
+
f"[bold yellow3]RAM Usage: {process.memory_info().rss / 1024 ** 2} MB[/]"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if enable_memray:
|
|
345
|
+
console.print(f"[bold green]Memray output file: {output_file}[/]")
|
|
346
|
+
os.system(f"memray flamegraph {output_file}")
|
|
347
|
+
|
|
348
|
+
generate_cli_subcommand(cli_endpoint, func, **command_opts)
|
|
349
|
+
generate_rich_click_opt_groups(cli_endpoint, name, input_types, options)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def generate_rich_click_opt_groups(cli_endpoint, name, input_types, options):
|
|
353
|
+
sortorder = {
|
|
354
|
+
'Execution': 0,
|
|
355
|
+
'Output': 1,
|
|
356
|
+
'Meta': 2,
|
|
357
|
+
'Config.*': 3,
|
|
358
|
+
'Shared task': 4,
|
|
359
|
+
'Task.*': 5,
|
|
360
|
+
'Workflow.*': 6,
|
|
361
|
+
'Scan.*': 7,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
def match_sort_order(prefix):
|
|
365
|
+
for k, v in sortorder.items():
|
|
366
|
+
if re.match(k, prefix):
|
|
367
|
+
return v
|
|
368
|
+
return 8
|
|
369
|
+
|
|
370
|
+
prefixes = deduplicate([opt['prefix'] for opt in options.values()])
|
|
371
|
+
prefixes = sorted(prefixes, key=match_sort_order)
|
|
372
|
+
opt_group = [
|
|
373
|
+
{
|
|
374
|
+
'name': 'Targets',
|
|
375
|
+
'options': input_types,
|
|
376
|
+
},
|
|
377
|
+
]
|
|
378
|
+
for prefix in prefixes:
|
|
379
|
+
prefix_opts = [
|
|
380
|
+
opt for opt, conf in options.items()
|
|
381
|
+
if conf['prefix'] == prefix
|
|
382
|
+
]
|
|
383
|
+
if prefix not in ['Execution', 'Output']:
|
|
384
|
+
prefix_opts = sorted(prefix_opts)
|
|
385
|
+
opt_names = [f'--{opt_name}' for opt_name in prefix_opts]
|
|
386
|
+
if prefix == 'Output':
|
|
387
|
+
opt_names.append('--help')
|
|
388
|
+
opt_group.append({
|
|
389
|
+
'name': prefix + ' options',
|
|
390
|
+
'options': opt_names
|
|
391
|
+
})
|
|
392
|
+
aliases = [cli_endpoint.name, *cli_endpoint.aliases]
|
|
393
|
+
for alias in aliases:
|
|
394
|
+
endpoint_name = f'secator {alias} {name}'
|
|
395
|
+
click.rich_click.OPTION_GROUPS[endpoint_name] = opt_group
|
secator/click.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from collections import OrderedDict
|
|
2
|
+
|
|
3
|
+
import rich_click as click
|
|
4
|
+
from rich_click.rich_click import _get_rich_console
|
|
5
|
+
from rich_click.rich_group import RichGroup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ListParamType(click.ParamType):
|
|
9
|
+
"""Custom click param type to convert comma-separated strings to lists."""
|
|
10
|
+
name = "list"
|
|
11
|
+
|
|
12
|
+
def convert(self, value, param, ctx):
|
|
13
|
+
if value is None:
|
|
14
|
+
return []
|
|
15
|
+
if isinstance(value, list):
|
|
16
|
+
return value
|
|
17
|
+
return [v.strip() for v in value.split(',') if v.strip()]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
CLICK_LIST = ListParamType()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class OrderedGroup(RichGroup):
|
|
24
|
+
def __init__(self, name=None, commands=None, **attrs):
|
|
25
|
+
super(OrderedGroup, self).__init__(name, commands, **attrs)
|
|
26
|
+
self.commands = commands or OrderedDict()
|
|
27
|
+
|
|
28
|
+
def command(self, *args, **kwargs):
|
|
29
|
+
"""Behaves the same as `click.Group.command()` but supports aliases.
|
|
30
|
+
"""
|
|
31
|
+
def decorator(f):
|
|
32
|
+
aliases = kwargs.pop("aliases", None)
|
|
33
|
+
if aliases:
|
|
34
|
+
max_width = _get_rich_console().width
|
|
35
|
+
aliases_str = ', '.join(f'[bold cyan]{alias}[/]' for alias in aliases)
|
|
36
|
+
padding = max_width // 4
|
|
37
|
+
|
|
38
|
+
name = kwargs.pop("name", None)
|
|
39
|
+
if not name:
|
|
40
|
+
raise click.UsageError("`name` command argument is required when using aliases.")
|
|
41
|
+
|
|
42
|
+
f.__doc__ = f.__doc__ or '\0'.ljust(padding+1)
|
|
43
|
+
f.__doc__ = f'{f.__doc__:<{padding}}[dim](aliases)[/] {aliases_str}'
|
|
44
|
+
base_command = super(OrderedGroup, self).command(
|
|
45
|
+
name, *args, **kwargs
|
|
46
|
+
)(f)
|
|
47
|
+
for alias in aliases:
|
|
48
|
+
cmd = super(OrderedGroup, self).command(alias, *args, hidden=True, **kwargs)(f)
|
|
49
|
+
cmd.help = f"Alias for '{name}'.\n\n{cmd.help}"
|
|
50
|
+
cmd.params = base_command.params
|
|
51
|
+
|
|
52
|
+
else:
|
|
53
|
+
cmd = super(OrderedGroup, self).command(*args, **kwargs)(f)
|
|
54
|
+
|
|
55
|
+
return cmd
|
|
56
|
+
return decorator
|
|
57
|
+
|
|
58
|
+
def group(self, *args, **kwargs):
|
|
59
|
+
"""Behaves the same as `click.Group.group()` but supports aliases.
|
|
60
|
+
"""
|
|
61
|
+
def decorator(f):
|
|
62
|
+
aliases = kwargs.pop('aliases', [])
|
|
63
|
+
aliased_group = []
|
|
64
|
+
if aliases:
|
|
65
|
+
max_width = _get_rich_console().width
|
|
66
|
+
aliases_str = ', '.join(f'[bold cyan]{alias}[/]' for alias in aliases)
|
|
67
|
+
padding = max_width // 4
|
|
68
|
+
f.__doc__ = f.__doc__ or '\0'.ljust(padding+1)
|
|
69
|
+
f.__doc__ = f'{f.__doc__:<{padding}}[dim](aliases)[/] {aliases_str}'
|
|
70
|
+
for alias in aliases:
|
|
71
|
+
grp = super(OrderedGroup, self).group(
|
|
72
|
+
alias, *args, hidden=True, **kwargs)(f)
|
|
73
|
+
aliased_group.append(grp)
|
|
74
|
+
|
|
75
|
+
# create the main group
|
|
76
|
+
grp = super(OrderedGroup, self).group(*args, **kwargs)(f)
|
|
77
|
+
grp.aliases = aliases
|
|
78
|
+
|
|
79
|
+
# for all of the aliased groups, share the main group commands
|
|
80
|
+
for aliased in aliased_group:
|
|
81
|
+
aliased.commands = grp.commands
|
|
82
|
+
|
|
83
|
+
return grp
|
|
84
|
+
return decorator
|
|
85
|
+
|
|
86
|
+
def list_commands(self, ctx):
|
|
87
|
+
return self.commands
|