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.

Files changed (90) hide show
  1. secator/celery.py +160 -185
  2. secator/celery_utils.py +268 -0
  3. secator/cli.py +427 -176
  4. secator/config.py +114 -68
  5. secator/configs/workflows/host_recon.yaml +5 -3
  6. secator/configs/workflows/port_scan.yaml +7 -3
  7. secator/configs/workflows/subdomain_recon.yaml +2 -2
  8. secator/configs/workflows/url_bypass.yaml +10 -0
  9. secator/configs/workflows/url_dirsearch.yaml +1 -1
  10. secator/configs/workflows/url_vuln.yaml +1 -1
  11. secator/decorators.py +170 -92
  12. secator/definitions.py +11 -4
  13. secator/exporters/__init__.py +7 -5
  14. secator/exporters/console.py +10 -0
  15. secator/exporters/csv.py +27 -19
  16. secator/exporters/gdrive.py +16 -11
  17. secator/exporters/json.py +3 -1
  18. secator/exporters/table.py +30 -2
  19. secator/exporters/txt.py +20 -16
  20. secator/hooks/gcs.py +53 -0
  21. secator/hooks/mongodb.py +53 -27
  22. secator/installer.py +277 -60
  23. secator/output_types/__init__.py +29 -11
  24. secator/output_types/_base.py +11 -1
  25. secator/output_types/error.py +36 -0
  26. secator/output_types/exploit.py +12 -8
  27. secator/output_types/info.py +24 -0
  28. secator/output_types/ip.py +8 -1
  29. secator/output_types/port.py +9 -2
  30. secator/output_types/progress.py +5 -0
  31. secator/output_types/record.py +5 -3
  32. secator/output_types/stat.py +33 -0
  33. secator/output_types/subdomain.py +1 -1
  34. secator/output_types/tag.py +8 -6
  35. secator/output_types/target.py +2 -2
  36. secator/output_types/url.py +14 -11
  37. secator/output_types/user_account.py +6 -6
  38. secator/output_types/vulnerability.py +8 -6
  39. secator/output_types/warning.py +24 -0
  40. secator/report.py +56 -23
  41. secator/rich.py +44 -39
  42. secator/runners/_base.py +629 -638
  43. secator/runners/_helpers.py +5 -91
  44. secator/runners/celery.py +18 -0
  45. secator/runners/command.py +404 -214
  46. secator/runners/scan.py +8 -24
  47. secator/runners/task.py +21 -55
  48. secator/runners/workflow.py +41 -40
  49. secator/scans/__init__.py +28 -0
  50. secator/serializers/dataclass.py +6 -0
  51. secator/serializers/json.py +10 -5
  52. secator/serializers/regex.py +12 -4
  53. secator/tasks/_categories.py +147 -42
  54. secator/tasks/bbot.py +295 -0
  55. secator/tasks/bup.py +99 -0
  56. secator/tasks/cariddi.py +38 -49
  57. secator/tasks/dalfox.py +3 -0
  58. secator/tasks/dirsearch.py +14 -25
  59. secator/tasks/dnsx.py +49 -30
  60. secator/tasks/dnsxbrute.py +4 -1
  61. secator/tasks/feroxbuster.py +10 -20
  62. secator/tasks/ffuf.py +3 -2
  63. secator/tasks/fping.py +4 -4
  64. secator/tasks/gau.py +5 -0
  65. secator/tasks/gf.py +2 -2
  66. secator/tasks/gospider.py +4 -0
  67. secator/tasks/grype.py +11 -13
  68. secator/tasks/h8mail.py +32 -42
  69. secator/tasks/httpx.py +58 -21
  70. secator/tasks/katana.py +19 -23
  71. secator/tasks/maigret.py +27 -25
  72. secator/tasks/mapcidr.py +2 -3
  73. secator/tasks/msfconsole.py +22 -19
  74. secator/tasks/naabu.py +18 -2
  75. secator/tasks/nmap.py +82 -55
  76. secator/tasks/nuclei.py +13 -3
  77. secator/tasks/searchsploit.py +26 -11
  78. secator/tasks/subfinder.py +5 -1
  79. secator/tasks/wpscan.py +91 -94
  80. secator/template.py +61 -45
  81. secator/thread.py +24 -0
  82. secator/utils.py +417 -78
  83. secator/utils_test.py +48 -23
  84. secator/workflows/__init__.py +28 -0
  85. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
  86. secator-0.8.0.dist-info/RECORD +115 -0
  87. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
  88. secator-0.6.0.dist-info/RECORD +0 -101
  89. {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
  90. {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
- 'json': {'is_flag': True, 'default': False, 'help': 'Enable JSON mode'},
18
- 'orig': {'is_flag': True, 'default': False, 'help': 'Enable original output (no schema conversion)'},
19
- 'raw': {'is_flag': True, 'default': False, 'help': 'Enable text output for piping to other tools'},
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
- 'format': {'default': '', 'short': 'fmt', 'help': 'Output formatting string'},
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(*tasks):
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
- tasks (list): List of secator command classes.
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
- for cls in tasks:
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
- short = conf.pop('short', None)
175
- conf.pop('internal', False)
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'-{short}' if short else f'-{opt_name}'
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
- fmt_opts = {
193
- 'print_cmd': True,
194
- }
195
- short_help = ''
196
- input_type = 'targets'
255
+ name = config.name
197
256
  input_required = True
198
- runner_cls = None
199
- tasks = []
200
- no_args_is_help = True
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
- tasks = [
238
- get_command_cls(config.name)
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
- fmt_opts['print_item_count'] = True
246
- runner_cls = Task
247
- no_args_is_help = False
248
- input_required = False
291
+ command_opts.update({
292
+ 'name': name,
293
+ 'short_help': short_help,
294
+ 'no_args_is_help': False
295
+ })
249
296
 
250
- options = get_command_options(*tasks)
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
- # debug = opts['debug']
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
- targets = opts.pop(input_type)
276
- targets = expand_input(targets)
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
- opts['sync'] = sync
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
- 'print_item': not sync,
295
- 'print_line': sync,
296
- 'print_remote_status': not sync,
297
- 'print_start': not sync
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
- # Build hooks from driver name
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
- settings = {'ignore_unknown_options': False, 'allow_extra_args': False}
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 = f"""
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
- ('google', 'gspread'),
128
+ ('gdrive', 'gspread'),
129
+ ('gcs', 'google.cloud.storage'),
123
130
  ('mongodb', 'pymongo'),
124
131
  ('redis', 'redis'),
125
132
  ('dev', 'flake8'),
@@ -1,10 +1,12 @@
1
1
  __all__ = [
2
- 'CsvExporter',
3
- 'GdriveExporter',
4
- 'JsonExporter',
5
- 'TableExporter',
6
- 'TxtExporter'
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
@@ -0,0 +1,10 @@
1
+ from secator.exporters._base import Exporter
2
+ from secator.rich import console
3
+
4
+
5
+ class ConsoleExporter(Exporter):
6
+ def send(self):
7
+ results = self.report.data['results']
8
+ for items in results.values():
9
+ for item in items:
10
+ console.print(item)
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
- def send(self):
9
- results = self.report.data['results']
10
- csv_paths = []
12
+ def send(self):
13
+ results = self.report.data['results']
14
+ if not results:
15
+ return
16
+ csv_paths = []
11
17
 
12
- for output_type, items in results.items():
13
- items = [i.toDict() for i in items]
14
- if not items:
15
- continue
16
- keys = list(items[0].keys())
17
- csv_path = f'{self.report.output_folder}/report_{output_type}.csv'
18
- csv_paths.append(csv_path)
19
- with open(csv_path, 'w', newline='') as output_file:
20
- dict_writer = _csv.DictWriter(output_file, keys)
21
- dict_writer.writeheader()
22
- dict_writer.writerows(items)
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
- if len(csv_paths) == 1:
25
- csv_paths_str = csv_paths[0]
26
- else:
27
- csv_paths_str = '\n • ' + '\n • '.join(csv_paths)
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
- console.print(f':file_cabinet: Saved CSV reports to {csv_paths_str}')
36
+ info = Info(message=f'Saved CSV reports to {csv_paths_str}')
37
+ console.print(info)
@@ -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.google.credentials_path:
20
- console.print(':file_cabinet: Missing CONFIG.addons.google.credentials_path to save to Google Sheets', style='red')
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.google.drive_parent_folder_id:
23
- console.print(':file_cabinet: Missing CONFIG.addons.google.drive_parent_folder_id to save to Google Sheets.', style='red') # noqa: E501
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.google.credentials_path)
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.google.drive_parent_folder_id)
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.google.drive_parent_folder_id)
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
- console.print(
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
- console.print(f':file_cabinet: Saved Google Sheets reports to [u magenta]{sheet.url}[/]')
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.google.credentials_path)
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.google.credentials_path)
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
- console.print(f':file_cabinet: Saved JSON report to {json_path}')
15
+ info = Info(f'Saved JSON report to {json_path}')
16
+ console.print(info)
@@ -1,7 +1,35 @@
1
1
  from secator.exporters._base import Exporter
2
- from secator.utils import print_results_table
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
- print_results_table(self.report.runner.results, self.report.title)
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()