secator 0.6.0__py3-none-any.whl → 0.8.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 +160 -185
- secator/celery_utils.py +268 -0
- secator/cli.py +427 -176
- secator/config.py +114 -68
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/subdomain_recon.yaml +2 -2
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_dirsearch.yaml +1 -1
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +170 -92
- secator/definitions.py +11 -4
- secator/exporters/__init__.py +7 -5
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +27 -19
- secator/exporters/gdrive.py +16 -11
- secator/exporters/json.py +3 -1
- secator/exporters/table.py +30 -2
- secator/exporters/txt.py +20 -16
- secator/hooks/gcs.py +53 -0
- secator/hooks/mongodb.py +53 -27
- secator/installer.py +277 -60
- secator/output_types/__init__.py +29 -11
- secator/output_types/_base.py +11 -1
- secator/output_types/error.py +36 -0
- secator/output_types/exploit.py +12 -8
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +8 -1
- secator/output_types/port.py +9 -2
- secator/output_types/progress.py +5 -0
- secator/output_types/record.py +5 -3
- secator/output_types/stat.py +33 -0
- secator/output_types/subdomain.py +1 -1
- secator/output_types/tag.py +8 -6
- secator/output_types/target.py +2 -2
- secator/output_types/url.py +14 -11
- secator/output_types/user_account.py +6 -6
- secator/output_types/vulnerability.py +8 -6
- secator/output_types/warning.py +24 -0
- secator/report.py +56 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +629 -638
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +404 -214
- secator/runners/scan.py +8 -24
- secator/runners/task.py +21 -55
- secator/runners/workflow.py +41 -40
- secator/scans/__init__.py +28 -0
- secator/serializers/dataclass.py +6 -0
- secator/serializers/json.py +10 -5
- secator/serializers/regex.py +12 -4
- secator/tasks/_categories.py +147 -42
- secator/tasks/bbot.py +295 -0
- secator/tasks/bup.py +99 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +14 -25
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +10 -20
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +4 -4
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +11 -13
- secator/tasks/h8mail.py +32 -42
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +19 -23
- secator/tasks/maigret.py +27 -25
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +22 -19
- secator/tasks/naabu.py +18 -2
- secator/tasks/nmap.py +82 -55
- secator/tasks/nuclei.py +13 -3
- secator/tasks/searchsploit.py +26 -11
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +91 -94
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +417 -78
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
- secator-0.8.0.dist-info/RECORD +115 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
- secator-0.6.0.dist-info/RECORD +0 -101
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/licenses/LICENSE +0 -0
secator/decorators.py
CHANGED
|
@@ -1,30 +1,33 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
|
|
2
3
|
from collections import OrderedDict
|
|
3
4
|
|
|
4
5
|
import rich_click as click
|
|
5
6
|
from rich_click.rich_click import _get_rich_console
|
|
6
7
|
from rich_click.rich_group import RichGroup
|
|
7
8
|
|
|
8
|
-
from secator.definitions import ADDONS_ENABLED, OPT_NOT_SUPPORTED
|
|
9
9
|
from secator.config import CONFIG
|
|
10
|
+
from secator.definitions import ADDONS_ENABLED, OPT_NOT_SUPPORTED
|
|
10
11
|
from secator.runners import Scan, Task, Workflow
|
|
11
|
-
from secator.utils import (deduplicate, expand_input, get_command_category
|
|
12
|
-
get_command_cls)
|
|
12
|
+
from secator.utils import (deduplicate, expand_input, get_command_category)
|
|
13
13
|
|
|
14
14
|
RUNNER_OPTS = {
|
|
15
15
|
'output': {'type': str, 'default': None, 'help': 'Output options (-o table,json,csv,gdrive)', 'short': 'o'},
|
|
16
16
|
'workspace': {'type': str, 'default': 'default', 'help': 'Workspace', 'short': 'ws'},
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
17
|
+
'print_json': {'is_flag': True, 'short': 'json', 'default': False, 'help': 'Print items as JSON lines'},
|
|
18
|
+
'print_raw': {'is_flag': True, 'short': 'raw', 'default': False, 'help': 'Print items in raw format'},
|
|
19
|
+
'print_stat': {'is_flag': True, 'short': 'stat', 'default': False, 'help': 'Print runtime statistics'},
|
|
20
|
+
'print_format': {'default': '', 'short': 'fmt', 'help': 'Output formatting string'},
|
|
21
|
+
'enable_profiler': {'is_flag': True, 'short': 'prof', 'default': False, 'help': 'Enable runner profiling'},
|
|
20
22
|
'show': {'is_flag': True, 'default': False, 'help': 'Show command that will be run (tasks only)'},
|
|
21
|
-
'
|
|
23
|
+
'no_process': {'is_flag': True, 'default': False, 'help': 'Disable secator processing'},
|
|
22
24
|
# 'filter': {'default': '', 'short': 'f', 'help': 'Results filter', 'short': 'of'}, # TODO add this
|
|
23
25
|
'quiet': {'is_flag': True, 'default': False, 'help': 'Enable quiet mode'},
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
RUNNER_GLOBAL_OPTS = {
|
|
27
29
|
'sync': {'is_flag': True, 'help': 'Run tasks synchronously (automatic if no worker is alive)'},
|
|
30
|
+
'worker': {'is_flag': True, 'default': False, 'help': 'Run tasks in worker'},
|
|
28
31
|
'proxy': {'type': str, 'help': 'HTTP proxy'},
|
|
29
32
|
'driver': {'type': str, 'help': 'Export real-time results. E.g: "mongodb"'}
|
|
30
33
|
# 'debug': {'type': int, 'default': 0, 'help': 'Debug mode'},
|
|
@@ -100,20 +103,34 @@ class OrderedGroup(RichGroup):
|
|
|
100
103
|
return self.commands
|
|
101
104
|
|
|
102
105
|
|
|
103
|
-
def get_command_options(
|
|
104
|
-
"""Get unified list of command options from a list of secator tasks classes.
|
|
106
|
+
def get_command_options(config):
|
|
107
|
+
"""Get unified list of command options from a list of secator tasks classes and optionally a Runner config.
|
|
105
108
|
|
|
106
109
|
Args:
|
|
107
|
-
|
|
110
|
+
config (TemplateLoader): Current runner config.
|
|
108
111
|
|
|
109
112
|
Returns:
|
|
110
113
|
list: List of deduplicated options.
|
|
111
114
|
"""
|
|
115
|
+
from secator.utils import debug
|
|
112
116
|
opt_cache = []
|
|
113
117
|
all_opts = OrderedDict({})
|
|
118
|
+
tasks = config.flat_tasks
|
|
119
|
+
tasks_cls = set([c['class'] for c in tasks.values()])
|
|
114
120
|
|
|
115
|
-
|
|
121
|
+
# Loop through tasks and set options
|
|
122
|
+
for cls in tasks_cls:
|
|
116
123
|
opts = OrderedDict(RUNNER_GLOBAL_OPTS, **RUNNER_OPTS, **cls.meta_opts, **cls.opts)
|
|
124
|
+
|
|
125
|
+
# Find opts defined in config corresponding to this task class
|
|
126
|
+
# TODO: rework this as this ignores subsequent tasks of the same task class
|
|
127
|
+
task_config_opts = {}
|
|
128
|
+
if config.type != 'task':
|
|
129
|
+
for k, v in tasks.items():
|
|
130
|
+
if v['class'] == cls:
|
|
131
|
+
task_config_opts = v['opts']
|
|
132
|
+
|
|
133
|
+
# Loop through options
|
|
117
134
|
for opt, opt_conf in opts.items():
|
|
118
135
|
|
|
119
136
|
# Get opt key map if any
|
|
@@ -126,6 +143,7 @@ def get_command_options(*tasks):
|
|
|
126
143
|
and opt not in RUNNER_GLOBAL_OPTS:
|
|
127
144
|
continue
|
|
128
145
|
|
|
146
|
+
# Opt is defined as unsupported
|
|
129
147
|
if opt_key_map.get(opt) == OPT_NOT_SUPPORTED:
|
|
130
148
|
continue
|
|
131
149
|
|
|
@@ -143,17 +161,50 @@ def get_command_options(*tasks):
|
|
|
143
161
|
elif opt in RUNNER_GLOBAL_OPTS:
|
|
144
162
|
prefix = 'Execution'
|
|
145
163
|
|
|
164
|
+
# Get opt conf
|
|
165
|
+
conf = opt_conf.copy()
|
|
166
|
+
conf['show_default'] = True
|
|
167
|
+
conf['prefix'] = prefix
|
|
168
|
+
opt_default = conf.get('default', None)
|
|
169
|
+
opt_is_flag = conf.get('is_flag', False)
|
|
170
|
+
opt_value_in_config = task_config_opts.get(opt)
|
|
171
|
+
|
|
172
|
+
# Check if opt already defined in config
|
|
173
|
+
if opt_value_in_config:
|
|
174
|
+
if conf.get('required', False):
|
|
175
|
+
debug('OPT (skipped: opt is required and defined in config)', obj={'opt': opt}, sub=f'cli.{config.name}', verbose=True) # noqa: E501
|
|
176
|
+
continue
|
|
177
|
+
mapped_value = cls.opt_value_map.get(opt)
|
|
178
|
+
if callable(mapped_value):
|
|
179
|
+
opt_value_in_config = mapped_value(opt_value_in_config)
|
|
180
|
+
elif mapped_value:
|
|
181
|
+
opt_value_in_config = mapped_value
|
|
182
|
+
if opt_value_in_config != opt_default:
|
|
183
|
+
if opt in opt_cache:
|
|
184
|
+
continue
|
|
185
|
+
if opt_is_flag:
|
|
186
|
+
conf['reverse'] = True
|
|
187
|
+
conf['default'] = not conf['default']
|
|
188
|
+
# print(f'{opt}: change default to {opt_value_in_config}')
|
|
189
|
+
conf['default'] = opt_value_in_config
|
|
190
|
+
|
|
191
|
+
# If opt is a flag but the default is True, add opposite flag
|
|
192
|
+
if opt_is_flag and opt_default is True:
|
|
193
|
+
conf['reverse'] = True
|
|
194
|
+
|
|
146
195
|
# Check if opt already processed before
|
|
147
|
-
opt = opt.replace('_', '-')
|
|
148
196
|
if opt in opt_cache:
|
|
197
|
+
# debug('OPT (skipped: opt is already in opt cache)', obj={'opt': opt}, sub=f'cli.{config.name}', verbose=True)
|
|
149
198
|
continue
|
|
150
199
|
|
|
151
200
|
# Build help
|
|
152
|
-
conf = opt_conf.copy()
|
|
153
|
-
conf['show_default'] = True
|
|
154
|
-
conf['prefix'] = prefix
|
|
155
|
-
all_opts[opt] = conf
|
|
156
201
|
opt_cache.append(opt)
|
|
202
|
+
opt = opt.replace('_', '-')
|
|
203
|
+
all_opts[opt] = conf
|
|
204
|
+
|
|
205
|
+
# Debug
|
|
206
|
+
debug_conf = OrderedDict({'opt': opt, 'config_val': opt_value_in_config or 'N/A', **conf.copy()})
|
|
207
|
+
debug('OPT', obj=debug_conf, sub=f'cli.{config.name}', verbose=True)
|
|
157
208
|
|
|
158
209
|
return all_opts
|
|
159
210
|
|
|
@@ -171,11 +222,19 @@ def decorate_command_options(opts):
|
|
|
171
222
|
reversed_opts = OrderedDict(list(opts.items())[::-1])
|
|
172
223
|
for opt_name, opt_conf in reversed_opts.items():
|
|
173
224
|
conf = opt_conf.copy()
|
|
174
|
-
|
|
175
|
-
conf.pop('internal',
|
|
225
|
+
short_opt = conf.pop('short', None)
|
|
226
|
+
conf.pop('internal', None)
|
|
176
227
|
conf.pop('prefix', None)
|
|
228
|
+
conf.pop('shlex', None)
|
|
229
|
+
conf.pop('meta', None)
|
|
230
|
+
conf.pop('supported', None)
|
|
231
|
+
conf.pop('process', None)
|
|
232
|
+
reverse = conf.pop('reverse', False)
|
|
177
233
|
long = f'--{opt_name}'
|
|
178
|
-
short = f'-{
|
|
234
|
+
short = f'-{short_opt}' if short_opt else f'-{opt_name}'
|
|
235
|
+
if reverse:
|
|
236
|
+
long += f'/--no-{opt_name}'
|
|
237
|
+
short += f'/-n{short_opt}' if short else f'/-n{opt_name}'
|
|
179
238
|
f = click.option(long, short, **conf)(f)
|
|
180
239
|
return f
|
|
181
240
|
return decorator
|
|
@@ -188,66 +247,56 @@ def task():
|
|
|
188
247
|
return decorator
|
|
189
248
|
|
|
190
249
|
|
|
250
|
+
def generate_cli_subcommand(cli_endpoint, func, **opts):
|
|
251
|
+
return cli_endpoint.command(**opts)(func)
|
|
252
|
+
|
|
253
|
+
|
|
191
254
|
def register_runner(cli_endpoint, config):
|
|
192
|
-
|
|
193
|
-
'print_cmd': True,
|
|
194
|
-
}
|
|
195
|
-
short_help = ''
|
|
196
|
-
input_type = 'targets'
|
|
255
|
+
name = config.name
|
|
197
256
|
input_required = True
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
257
|
+
input_type = 'targets'
|
|
258
|
+
command_opts = {
|
|
259
|
+
'no_args_is_help': True,
|
|
260
|
+
'context_settings': {
|
|
261
|
+
'ignore_unknown_options': False,
|
|
262
|
+
'allow_extra_args': False
|
|
263
|
+
}
|
|
264
|
+
}
|
|
201
265
|
|
|
202
266
|
if cli_endpoint.name == 'scan':
|
|
203
|
-
# TODO: this should be refactored to scan.get_tasks_from_conf() or scan.tasks
|
|
204
|
-
from secator.cli import ALL_CONFIGS
|
|
205
|
-
tasks = [
|
|
206
|
-
get_command_cls(task)
|
|
207
|
-
for workflow in ALL_CONFIGS.workflow
|
|
208
|
-
for task in Task.get_tasks_from_conf(workflow.tasks)
|
|
209
|
-
if workflow.name in list(config.workflows.keys())
|
|
210
|
-
]
|
|
211
|
-
input_type = 'targets'
|
|
212
|
-
name = config.name
|
|
213
|
-
short_help = config.description or ''
|
|
214
|
-
if config.alias:
|
|
215
|
-
short_help += f' [dim]alias: {config.alias}'
|
|
216
|
-
fmt_opts['print_start'] = True
|
|
217
|
-
fmt_opts['print_run_summary'] = True
|
|
218
|
-
fmt_opts['print_progress'] = False
|
|
219
267
|
runner_cls = Scan
|
|
268
|
+
short_help = config.description or ''
|
|
269
|
+
short_help += f' [dim]alias: {config.alias}' if config.alias else ''
|
|
270
|
+
command_opts.update({
|
|
271
|
+
'name': name,
|
|
272
|
+
'short_help': short_help
|
|
273
|
+
})
|
|
220
274
|
|
|
221
275
|
elif cli_endpoint.name == 'workflow':
|
|
222
|
-
# TODO: this should be refactored to workflow.get_tasks_from_conf() or workflow.tasks
|
|
223
|
-
tasks = [
|
|
224
|
-
get_command_cls(task) for task in Task.get_tasks_from_conf(config.tasks)
|
|
225
|
-
]
|
|
226
|
-
input_type = 'targets'
|
|
227
|
-
name = config.name
|
|
228
|
-
short_help = config.description or ''
|
|
229
|
-
if config.alias:
|
|
230
|
-
short_help = f'{short_help:<55} [dim](alias)[/][bold cyan] {config.alias}'
|
|
231
|
-
fmt_opts['print_start'] = True
|
|
232
|
-
fmt_opts['print_run_summary'] = True
|
|
233
|
-
fmt_opts['print_progress'] = False
|
|
234
276
|
runner_cls = Workflow
|
|
277
|
+
short_help = config.description or ''
|
|
278
|
+
short_help = f'{short_help:<55} [dim](alias)[/][bold cyan] {config.alias}' if config.alias else ''
|
|
279
|
+
command_opts.update({
|
|
280
|
+
'name': name,
|
|
281
|
+
'short_help': short_help
|
|
282
|
+
})
|
|
235
283
|
|
|
236
284
|
elif cli_endpoint.name == 'task':
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
]
|
|
285
|
+
runner_cls = Task
|
|
286
|
+
input_required = False # allow targets from stdin
|
|
240
287
|
task_cls = Task.get_task_class(config.name)
|
|
241
288
|
task_category = get_command_category(task_cls)
|
|
242
289
|
input_type = task_cls.input_type or 'targets'
|
|
243
|
-
name = config.name
|
|
244
290
|
short_help = f'[magenta]{task_category:<15}[/]{task_cls.__doc__}'
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
291
|
+
command_opts.update({
|
|
292
|
+
'name': name,
|
|
293
|
+
'short_help': short_help,
|
|
294
|
+
'no_args_is_help': False
|
|
295
|
+
})
|
|
249
296
|
|
|
250
|
-
|
|
297
|
+
else:
|
|
298
|
+
raise ValueError(f"Unrecognized runner endpoint name {cli_endpoint.name}")
|
|
299
|
+
options = get_command_options(config)
|
|
251
300
|
|
|
252
301
|
# TODO: maybe allow this in the future
|
|
253
302
|
# def get_unknown_opts(ctx):
|
|
@@ -262,24 +311,59 @@ def register_runner(cli_endpoint, config):
|
|
|
262
311
|
@decorate_command_options(options)
|
|
263
312
|
@click.pass_context
|
|
264
313
|
def func(ctx, **opts):
|
|
265
|
-
opts.update(fmt_opts)
|
|
266
314
|
sync = opts['sync']
|
|
267
|
-
|
|
315
|
+
worker = opts.pop('worker')
|
|
268
316
|
ws = opts.pop('workspace')
|
|
269
317
|
driver = opts.pop('driver', '')
|
|
270
318
|
show = opts['show']
|
|
271
319
|
context = {'workspace_name': ws}
|
|
320
|
+
|
|
321
|
+
# Remove options whose values are default values
|
|
322
|
+
for k, v in options.items():
|
|
323
|
+
opt_name = k.replace('-', '_')
|
|
324
|
+
if opt_name in opts and opts[opt_name] == v.get('default', None):
|
|
325
|
+
del opts[opt_name]
|
|
326
|
+
|
|
272
327
|
# TODO: maybe allow this in the future
|
|
273
328
|
# unknown_opts = get_unknown_opts(ctx)
|
|
274
329
|
# opts.update(unknown_opts)
|
|
275
|
-
|
|
276
|
-
|
|
330
|
+
|
|
331
|
+
# Expand input
|
|
332
|
+
inputs = opts.pop(input_type)
|
|
333
|
+
inputs = expand_input(inputs, ctx)
|
|
334
|
+
|
|
335
|
+
# Build hooks from driver name
|
|
336
|
+
hooks = []
|
|
337
|
+
drivers = driver.split(',') if driver else []
|
|
338
|
+
console = _get_rich_console()
|
|
339
|
+
supported_drivers = ['mongodb', 'gcs']
|
|
340
|
+
for driver in drivers:
|
|
341
|
+
if driver in supported_drivers:
|
|
342
|
+
if not ADDONS_ENABLED[driver]:
|
|
343
|
+
console.print(f'[bold red]Missing "{driver}" addon: please run `secator install addons {driver}`[/].')
|
|
344
|
+
sys.exit(1)
|
|
345
|
+
from secator.utils import import_dynamic
|
|
346
|
+
driver_hooks = import_dynamic(f'secator.hooks.{driver}', 'HOOKS')
|
|
347
|
+
if driver_hooks is None:
|
|
348
|
+
console.print(f'[bold red]Missing "secator.hooks.{driver}.HOOKS".[/]')
|
|
349
|
+
sys.exit(1)
|
|
350
|
+
hooks.append(driver_hooks)
|
|
351
|
+
else:
|
|
352
|
+
supported_drivers_str = ', '.join([f'[bold green]{_}[/]' for _ in supported_drivers])
|
|
353
|
+
console.print(f'[bold red]Driver "{driver}" is not supported.[/]')
|
|
354
|
+
console.print(f'Supported drivers: {supported_drivers_str}')
|
|
355
|
+
sys.exit(1)
|
|
356
|
+
|
|
357
|
+
from secator.utils import deep_merge_dicts
|
|
358
|
+
hooks = deep_merge_dicts(*hooks)
|
|
359
|
+
|
|
360
|
+
# Enable sync or not
|
|
277
361
|
if sync or show:
|
|
278
362
|
sync = True
|
|
279
363
|
else:
|
|
280
364
|
from secator.celery import is_celery_worker_alive
|
|
281
365
|
worker_alive = is_celery_worker_alive()
|
|
282
|
-
if not worker_alive:
|
|
366
|
+
if not worker_alive and not worker:
|
|
283
367
|
sync = True
|
|
284
368
|
else:
|
|
285
369
|
sync = False
|
|
@@ -289,34 +373,28 @@ def register_runner(cli_endpoint, config):
|
|
|
289
373
|
if (broker_protocol == 'redis' or backend_protocol == 'redis') and not ADDONS_ENABLED['redis']:
|
|
290
374
|
_get_rich_console().print('[bold red]Missing `redis` addon: please run `secator install addons redis`[/].')
|
|
291
375
|
sys.exit(1)
|
|
292
|
-
|
|
376
|
+
|
|
377
|
+
from secator.utils import debug
|
|
378
|
+
debug('Run options', obj=opts, sub='cli')
|
|
379
|
+
|
|
380
|
+
# Set run options
|
|
293
381
|
opts.update({
|
|
294
|
-
'
|
|
295
|
-
'
|
|
296
|
-
'
|
|
297
|
-
'
|
|
382
|
+
'print_cmd': True,
|
|
383
|
+
'print_item': True,
|
|
384
|
+
'print_line': True,
|
|
385
|
+
'print_progress': True,
|
|
386
|
+
'print_remote_info': not sync,
|
|
387
|
+
'piped_input': ctx.obj['piped_input'],
|
|
388
|
+
'piped_output': ctx.obj['piped_output'],
|
|
389
|
+
'caller': 'cli',
|
|
390
|
+
'sync': sync,
|
|
298
391
|
})
|
|
299
392
|
|
|
300
|
-
#
|
|
301
|
-
hooks =
|
|
302
|
-
if driver == 'mongodb':
|
|
303
|
-
if not ADDONS_ENABLED['mongodb']:
|
|
304
|
-
_get_rich_console().print('[bold red]Missing `mongodb` addon: please run `secator install addons mongodb`[/].')
|
|
305
|
-
sys.exit(1)
|
|
306
|
-
from secator.hooks.mongodb import MONGODB_HOOKS
|
|
307
|
-
hooks = MONGODB_HOOKS
|
|
308
|
-
|
|
309
|
-
# Build exporters
|
|
310
|
-
runner = runner_cls(config, targets, run_opts=opts, hooks=hooks, context=context)
|
|
393
|
+
# Start runner
|
|
394
|
+
runner = runner_cls(config, inputs, run_opts=opts, hooks=hooks, context=context)
|
|
311
395
|
runner.run()
|
|
312
396
|
|
|
313
|
-
|
|
314
|
-
cli_endpoint.command(
|
|
315
|
-
name=config.name,
|
|
316
|
-
context_settings=settings,
|
|
317
|
-
no_args_is_help=no_args_is_help,
|
|
318
|
-
short_help=short_help)(func)
|
|
319
|
-
|
|
397
|
+
generate_cli_subcommand(cli_endpoint, func, **command_opts)
|
|
320
398
|
generate_rich_click_opt_groups(cli_endpoint, name, input_type, options)
|
|
321
399
|
|
|
322
400
|
|
secator/definitions.py
CHANGED
|
@@ -9,7 +9,7 @@ from secator.config import CONFIG, ROOT_FOLDER
|
|
|
9
9
|
|
|
10
10
|
# Globals
|
|
11
11
|
VERSION = version('secator')
|
|
12
|
-
ASCII =
|
|
12
|
+
ASCII = rf"""
|
|
13
13
|
__
|
|
14
14
|
________ _________ _/ /_____ _____
|
|
15
15
|
/ ___/ _ \/ ___/ __ `/ __/ __ \/ ___/
|
|
@@ -24,14 +24,19 @@ DEBUG = CONFIG.debug.level
|
|
|
24
24
|
DEBUG_COMPONENT = CONFIG.debug.component.split(',')
|
|
25
25
|
|
|
26
26
|
# Default tasks settings
|
|
27
|
-
DEFAULT_HTTPX_FLAGS = os.environ.get('DEFAULT_HTTPX_FLAGS', '-td')
|
|
28
|
-
DEFAULT_KATANA_FLAGS = os.environ.get('DEFAULT_KATANA_FLAGS', '-jc -js-crawl -known-files all -or -ob')
|
|
29
27
|
DEFAULT_NUCLEI_FLAGS = os.environ.get('DEFAULT_NUCLEI_FLAGS', '-stats -sj -si 20 -hm -or')
|
|
30
28
|
DEFAULT_FEROXBUSTER_FLAGS = os.environ.get('DEFAULT_FEROXBUSTER_FLAGS', '--auto-bail --no-state')
|
|
31
29
|
|
|
32
30
|
# Constants
|
|
33
31
|
OPT_NOT_SUPPORTED = -1
|
|
34
32
|
OPT_PIPE_INPUT = -1
|
|
33
|
+
STATE_COLORS = {
|
|
34
|
+
'PENDING': 'dim yellow3',
|
|
35
|
+
'RUNNING': 'bold yellow3',
|
|
36
|
+
'SUCCESS': 'bold green',
|
|
37
|
+
'FAILURE': 'bold red',
|
|
38
|
+
'REVOKED': 'bold magenta'
|
|
39
|
+
}
|
|
35
40
|
|
|
36
41
|
# Vocab
|
|
37
42
|
ALIVE = 'alive'
|
|
@@ -54,6 +59,7 @@ FILTER_SIZE = 'filter_size'
|
|
|
54
59
|
HEADER = 'header'
|
|
55
60
|
HOST = 'host'
|
|
56
61
|
IP = 'ip'
|
|
62
|
+
PROTOCOL = 'protocol'
|
|
57
63
|
LINES = 'lines'
|
|
58
64
|
METHOD = 'method'
|
|
59
65
|
MATCH_CODES = 'match_codes'
|
|
@@ -119,7 +125,8 @@ ADDONS_ENABLED = {}
|
|
|
119
125
|
|
|
120
126
|
for addon, module in [
|
|
121
127
|
('worker', 'eventlet'),
|
|
122
|
-
('
|
|
128
|
+
('gdrive', 'gspread'),
|
|
129
|
+
('gcs', 'google.cloud.storage'),
|
|
123
130
|
('mongodb', 'pymongo'),
|
|
124
131
|
('redis', 'redis'),
|
|
125
132
|
('dev', 'flake8'),
|
secator/exporters/__init__.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
'ConsoleExporter',
|
|
3
|
+
'CsvExporter',
|
|
4
|
+
'GdriveExporter',
|
|
5
|
+
'JsonExporter',
|
|
6
|
+
'TableExporter',
|
|
7
|
+
'TxtExporter'
|
|
7
8
|
]
|
|
9
|
+
from secator.exporters.console import ConsoleExporter
|
|
8
10
|
from secator.exporters.csv import CsvExporter
|
|
9
11
|
from secator.exporters.gdrive import GdriveExporter
|
|
10
12
|
from secator.exporters.json import JsonExporter
|
secator/exporters/csv.py
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
import csv as _csv
|
|
2
2
|
|
|
3
|
+
from dataclasses import fields
|
|
4
|
+
|
|
3
5
|
from secator.exporters._base import Exporter
|
|
4
6
|
from secator.rich import console
|
|
7
|
+
from secator.output_types import FINDING_TYPES
|
|
8
|
+
from secator.output_types import Info
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
class CsvExporter(Exporter):
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
12
|
+
def send(self):
|
|
13
|
+
results = self.report.data['results']
|
|
14
|
+
if not results:
|
|
15
|
+
return
|
|
16
|
+
csv_paths = []
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
for output_type, items in results.items():
|
|
19
|
+
output_cls = [o for o in FINDING_TYPES if o._type == output_type][0]
|
|
20
|
+
keys = [o.name for o in fields(output_cls)]
|
|
21
|
+
items = [i.toDict() for i in items]
|
|
22
|
+
if not items:
|
|
23
|
+
continue
|
|
24
|
+
csv_path = f'{self.report.output_folder}/report_{output_type}.csv'
|
|
25
|
+
csv_paths.append(csv_path)
|
|
26
|
+
with open(csv_path, 'w', newline='') as output_file:
|
|
27
|
+
dict_writer = _csv.DictWriter(output_file, keys)
|
|
28
|
+
dict_writer.writeheader()
|
|
29
|
+
dict_writer.writerows(items)
|
|
23
30
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
if len(csv_paths) == 1:
|
|
32
|
+
csv_paths_str = csv_paths[0]
|
|
33
|
+
else:
|
|
34
|
+
csv_paths_str = '\n • ' + '\n • '.join(csv_paths)
|
|
28
35
|
|
|
29
|
-
|
|
36
|
+
info = Info(message=f'Saved CSV reports to {csv_paths_str}')
|
|
37
|
+
console.print(info)
|
secator/exporters/gdrive.py
CHANGED
|
@@ -4,6 +4,7 @@ import yaml
|
|
|
4
4
|
|
|
5
5
|
from secator.config import CONFIG
|
|
6
6
|
from secator.exporters._base import Exporter
|
|
7
|
+
from secator.output_types import Info, Error
|
|
7
8
|
from secator.rich import console
|
|
8
9
|
from secator.utils import pluralize
|
|
9
10
|
|
|
@@ -16,20 +17,22 @@ class GdriveExporter(Exporter):
|
|
|
16
17
|
title = self.report.data['info']['title']
|
|
17
18
|
sheet_title = f'{self.report.data["info"]["title"]}_{self.report.timestamp}'
|
|
18
19
|
results = self.report.data['results']
|
|
19
|
-
if not CONFIG.addons.
|
|
20
|
-
|
|
20
|
+
if not CONFIG.addons.gdrive.credentials_path:
|
|
21
|
+
error = Error('Missing CONFIG.addons.gdrive.credentials_path to save to Google Sheets')
|
|
22
|
+
console.print(error)
|
|
21
23
|
return
|
|
22
|
-
if not CONFIG.addons.
|
|
23
|
-
|
|
24
|
+
if not CONFIG.addons.gdrive.drive_parent_folder_id:
|
|
25
|
+
error = Error('Missing CONFIG.addons.gdrive.drive_parent_folder_id to save to Google Sheets.')
|
|
26
|
+
console.print(error)
|
|
24
27
|
return
|
|
25
|
-
client = gspread.service_account(CONFIG.addons.
|
|
28
|
+
client = gspread.service_account(CONFIG.addons.gdrive.credentials_path)
|
|
26
29
|
|
|
27
30
|
# Create workspace folder if it doesn't exist
|
|
28
|
-
folder_id = self.get_folder_by_name(ws, parent_id=CONFIG.addons.
|
|
31
|
+
folder_id = self.get_folder_by_name(ws, parent_id=CONFIG.addons.gdrive.drive_parent_folder_id)
|
|
29
32
|
if ws and not folder_id:
|
|
30
33
|
folder_id = self.create_folder(
|
|
31
34
|
folder_name=ws,
|
|
32
|
-
parent_id=CONFIG.addons.
|
|
35
|
+
parent_id=CONFIG.addons.gdrive.drive_parent_folder_id)
|
|
33
36
|
|
|
34
37
|
# Create worksheet
|
|
35
38
|
sheet = client.create(title, folder_id=folder_id)
|
|
@@ -57,8 +60,9 @@ class GdriveExporter(Exporter):
|
|
|
57
60
|
]
|
|
58
61
|
csv_path = f'{self.report.output_folder}/report_{output_type}.csv'
|
|
59
62
|
if not os.path.exists(csv_path):
|
|
60
|
-
|
|
63
|
+
error = Error(
|
|
61
64
|
f'Unable to find CSV at {csv_path}. For Google sheets reports, please enable CSV reports as well.')
|
|
65
|
+
console.print(error)
|
|
62
66
|
return
|
|
63
67
|
sheet_title = pluralize(output_type).upper()
|
|
64
68
|
ws = sheet.add_worksheet(sheet_title, rows=len(items), cols=len(keys))
|
|
@@ -79,12 +83,13 @@ class GdriveExporter(Exporter):
|
|
|
79
83
|
ws = sheet.get_worksheet(0)
|
|
80
84
|
sheet.del_worksheet(ws)
|
|
81
85
|
|
|
82
|
-
|
|
86
|
+
info = Info(message=f'Saved Google Sheets reports to [u magenta]{sheet.url}')
|
|
87
|
+
console.print(info)
|
|
83
88
|
|
|
84
89
|
def create_folder(self, folder_name, parent_id=None):
|
|
85
90
|
from googleapiclient.discovery import build
|
|
86
91
|
from google.oauth2 import service_account
|
|
87
|
-
creds = service_account.Credentials.from_service_account_file(CONFIG.addons.
|
|
92
|
+
creds = service_account.Credentials.from_service_account_file(CONFIG.addons.gdrive.credentials_path)
|
|
88
93
|
service = build('drive', 'v3', credentials=creds)
|
|
89
94
|
body = {
|
|
90
95
|
'name': folder_name,
|
|
@@ -98,7 +103,7 @@ class GdriveExporter(Exporter):
|
|
|
98
103
|
def list_folders(self, parent_id):
|
|
99
104
|
from googleapiclient.discovery import build
|
|
100
105
|
from google.oauth2 import service_account
|
|
101
|
-
creds = service_account.Credentials.from_service_account_file(CONFIG.addons.
|
|
106
|
+
creds = service_account.Credentials.from_service_account_file(CONFIG.addons.gdrive.credentials_path)
|
|
102
107
|
service = build('drive', 'v3', credentials=creds)
|
|
103
108
|
driveid = service.files().get(fileId='root').execute()['id']
|
|
104
109
|
response = service.files().list(
|
secator/exporters/json.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from secator.exporters._base import Exporter
|
|
2
|
+
from secator.output_types import Info
|
|
2
3
|
from secator.rich import console
|
|
3
4
|
from secator.serializers.dataclass import dumps_dataclass
|
|
4
5
|
|
|
@@ -11,4 +12,5 @@ class JsonExporter(Exporter):
|
|
|
11
12
|
with open(json_path, 'w') as f:
|
|
12
13
|
f.write(dumps_dataclass(self.report.data, indent=2))
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
info = Info(f'Saved JSON report to {json_path}')
|
|
16
|
+
console.print(info)
|
secator/exporters/table.py
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
from secator.exporters._base import Exporter
|
|
2
|
-
from secator.utils import
|
|
2
|
+
from secator.utils import pluralize
|
|
3
|
+
from secator.rich import build_table, console
|
|
4
|
+
from rich.markdown import Markdown
|
|
5
|
+
from secator.output_types import OutputType
|
|
3
6
|
|
|
4
7
|
|
|
5
8
|
class TableExporter(Exporter):
|
|
6
9
|
def send(self):
|
|
7
|
-
|
|
10
|
+
results = self.report.data['results']
|
|
11
|
+
if not results:
|
|
12
|
+
return
|
|
13
|
+
title = self.report.title
|
|
14
|
+
_print = console.print
|
|
15
|
+
_print()
|
|
16
|
+
if title:
|
|
17
|
+
title = ' '.join(title.capitalize().split('_')) + ' results'
|
|
18
|
+
h1 = Markdown(f'# {title}')
|
|
19
|
+
_print(h1, style='bold magenta', width=50)
|
|
20
|
+
_print()
|
|
21
|
+
for output_type, items in results.items():
|
|
22
|
+
if output_type == 'progress':
|
|
23
|
+
continue
|
|
24
|
+
if items:
|
|
25
|
+
is_output_type = isinstance(items[0], OutputType)
|
|
26
|
+
output_fields = items[0]._table_fields if is_output_type else None
|
|
27
|
+
sort_by = items[0]._sort_by if is_output_type else []
|
|
28
|
+
_table = build_table(
|
|
29
|
+
items,
|
|
30
|
+
output_fields=output_fields,
|
|
31
|
+
sort_by=sort_by)
|
|
32
|
+
title = pluralize(items[0]._type).upper() if is_output_type else 'Results'
|
|
33
|
+
_print(f':wrench: {title}', style='bold gold3', justify='left')
|
|
34
|
+
_print(_table)
|
|
35
|
+
_print()
|