secator 0.10.1a12__py3-none-any.whl → 0.15.1__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 (73) hide show
  1. secator/celery.py +10 -5
  2. secator/celery_signals.py +2 -11
  3. secator/cli.py +309 -69
  4. secator/config.py +3 -2
  5. secator/configs/profiles/aggressive.yaml +6 -5
  6. secator/configs/profiles/default.yaml +6 -7
  7. secator/configs/profiles/insane.yaml +8 -0
  8. secator/configs/profiles/paranoid.yaml +8 -0
  9. secator/configs/profiles/polite.yaml +8 -0
  10. secator/configs/profiles/sneaky.yaml +8 -0
  11. secator/configs/profiles/tor.yaml +5 -0
  12. secator/configs/workflows/host_recon.yaml +11 -2
  13. secator/configs/workflows/url_dirsearch.yaml +5 -0
  14. secator/configs/workflows/url_params_fuzz.yaml +25 -0
  15. secator/configs/workflows/wordpress.yaml +4 -1
  16. secator/decorators.py +64 -34
  17. secator/definitions.py +8 -4
  18. secator/installer.py +84 -49
  19. secator/output_types/__init__.py +2 -1
  20. secator/output_types/certificate.py +78 -0
  21. secator/output_types/stat.py +3 -0
  22. secator/output_types/user_account.py +1 -1
  23. secator/report.py +2 -2
  24. secator/rich.py +1 -1
  25. secator/runners/_base.py +50 -11
  26. secator/runners/_helpers.py +15 -3
  27. secator/runners/command.py +85 -21
  28. secator/runners/scan.py +6 -3
  29. secator/runners/task.py +1 -0
  30. secator/runners/workflow.py +22 -4
  31. secator/tasks/_categories.py +25 -17
  32. secator/tasks/arjun.py +92 -0
  33. secator/tasks/bbot.py +33 -4
  34. secator/tasks/bup.py +4 -2
  35. secator/tasks/cariddi.py +17 -4
  36. secator/tasks/dalfox.py +4 -2
  37. secator/tasks/dirsearch.py +4 -2
  38. secator/tasks/dnsx.py +5 -2
  39. secator/tasks/dnsxbrute.py +4 -1
  40. secator/tasks/feroxbuster.py +5 -2
  41. secator/tasks/ffuf.py +7 -3
  42. secator/tasks/fping.py +4 -1
  43. secator/tasks/gau.py +5 -2
  44. secator/tasks/gf.py +4 -2
  45. secator/tasks/gitleaks.py +79 -0
  46. secator/tasks/gospider.py +5 -2
  47. secator/tasks/grype.py +5 -2
  48. secator/tasks/h8mail.py +4 -2
  49. secator/tasks/httpx.py +6 -3
  50. secator/tasks/katana.py +6 -3
  51. secator/tasks/maigret.py +4 -2
  52. secator/tasks/mapcidr.py +5 -3
  53. secator/tasks/msfconsole.py +8 -6
  54. secator/tasks/naabu.py +16 -5
  55. secator/tasks/nmap.py +31 -29
  56. secator/tasks/nuclei.py +18 -10
  57. secator/tasks/searchsploit.py +8 -3
  58. secator/tasks/subfinder.py +6 -3
  59. secator/tasks/testssl.py +276 -0
  60. secator/tasks/trivy.py +98 -0
  61. secator/tasks/wafw00f.py +85 -0
  62. secator/tasks/wpprobe.py +96 -0
  63. secator/tasks/wpscan.py +8 -4
  64. secator/template.py +61 -67
  65. secator/utils.py +31 -18
  66. secator/utils_test.py +34 -10
  67. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/METADATA +11 -3
  68. secator-0.15.1.dist-info/RECORD +128 -0
  69. secator/configs/profiles/stealth.yaml +0 -7
  70. secator-0.10.1a12.dist-info/RECORD +0 -116
  71. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/WHEEL +0 -0
  72. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/entry_points.txt +0 -0
  73. {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,8 @@
1
+ type: profile
2
+ name: insane
3
+ description: "Local LAN scanning or stress scanning"
4
+ opts:
5
+ rate_limit: 100000
6
+ delay: 0
7
+ timeout: 1
8
+ retries: 0
@@ -0,0 +1,8 @@
1
+ type: profile
2
+ name: paranoid
3
+ description: "Maximum stealth"
4
+ opts:
5
+ rate_limit: 5
6
+ delay: 5
7
+ timeout: 15
8
+ retries: 5
@@ -0,0 +1,8 @@
1
+ type: profile
2
+ name: polite
3
+ description: "Avoid overloading network"
4
+ opts:
5
+ rate_limit: 100
6
+ delay: 0
7
+ timeout: 10
8
+ retries: 5
@@ -0,0 +1,8 @@
1
+ type: profile
2
+ name: sneaky
3
+ description: "IDS/IPS evasion, sensitive networks"
4
+ opts:
5
+ rate_limit: 10
6
+ delay: 2
7
+ timeout: 15
8
+ retries: 5
@@ -0,0 +1,5 @@
1
+ type: profile
2
+ name: tor
3
+ description: "Anonymous scan using Tor network"
4
+ opts:
5
+ proxy: tor
@@ -7,10 +7,19 @@ input_types:
7
7
  - host
8
8
  - cidr_range
9
9
  tasks:
10
+ naabu:
11
+ description: Find open ports
12
+ ports: "-" # scan all ports
10
13
  nmap:
11
14
  description: Search for vulnerabilities on open ports
12
- skip_host_discovery: True
13
- ports: "-" # scan all ports
15
+ version_detection: True
16
+ script: vulners
17
+ targets_:
18
+ - port.host
19
+ - target.name
20
+ ports_:
21
+ - port.port
22
+ ports: "-" # default if no port found by naabu
14
23
  _group/1:
15
24
  httpx:
16
25
  description: Probe HTTP services on open ports
@@ -14,6 +14,11 @@ tasks:
14
14
  field: '{name}/FUZZ'
15
15
  cariddi:
16
16
  description: Crawl HTTP directories for content
17
+ info: True
18
+ secrets: True
19
+ errors: True
20
+ juicy_extensions: 1
21
+ juicy_endpoints: True
17
22
  targets_:
18
23
  - target.name
19
24
  - url.url
@@ -0,0 +1,25 @@
1
+ type: workflow
2
+ name: url_params_fuzz
3
+ alias: url_params_fuzz
4
+ description: Extract parameters from an URL and fuzz them
5
+ tags: [http, fuzz]
6
+ input_types:
7
+ - url
8
+ tasks:
9
+ arjun:
10
+ description: Extract parameters from URLs
11
+ ffuf:
12
+ description: Fuzz URL params
13
+ wordlist: https://raw.githubusercontent.com/danielmiessler/SecLists/refs/heads/master/Discovery/Web-Content/burp-parameter-names.txt
14
+ auto_calibration: true
15
+ follow_redirect: true
16
+ targets_:
17
+ - type: url
18
+ field: url
19
+ condition: item._source.startswith('arjun')
20
+ httpx:
21
+ description: Probe fuzzed URLs
22
+ targets_:
23
+ - type: url
24
+ field: url
25
+ condition: item._source.startswith('ffuf')
@@ -11,4 +11,7 @@ tasks:
11
11
  description: Nuclei Wordpress scan
12
12
  tags: wordpress
13
13
  wpscan:
14
- description: WPScan
14
+ description: WPScan
15
+ wpprobe:
16
+ description: WPProbe
17
+ tags: wordpress
secator/decorators.py CHANGED
@@ -13,22 +13,24 @@ 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
+ 'profiles': {'type': str, 'default': 'default', 'help': 'Profiles', 'short': 'pf'},
16
17
  'workspace': {'type': str, 'default': 'default', 'help': 'Workspace', 'short': 'ws'},
17
18
  'print_json': {'is_flag': True, 'short': 'json', 'default': False, 'help': 'Print items as JSON lines'},
18
19
  'print_raw': {'is_flag': True, 'short': 'raw', 'default': False, 'help': 'Print items in raw format'},
19
20
  'print_stat': {'is_flag': True, 'short': 'stat', 'default': False, 'help': 'Print runtime statistics'},
20
21
  'print_format': {'default': '', 'short': 'fmt', 'help': 'Output formatting string'},
21
22
  'enable_profiler': {'is_flag': True, 'short': 'prof', 'default': False, 'help': 'Enable runner profiling'},
22
- 'show': {'is_flag': True, 'default': False, 'help': 'Show command that will be run (tasks only)'},
23
- 'no_process': {'is_flag': True, 'default': False, 'help': 'Disable secator processing'},
23
+ 'no_process': {'is_flag': True, 'short': 'nps', 'default': False, 'help': 'Disable secator processing'},
24
24
  # 'filter': {'default': '', 'short': 'f', 'help': 'Results filter', 'short': 'of'}, # TODO add this
25
- 'quiet': {'is_flag': True, 'default': False, 'help': 'Enable quiet mode'},
25
+ 'quiet': {'is_flag': True, 'short': 'q', 'default': not CONFIG.runners.show_command_output, 'opposite': 'verbose', 'help': 'Enable quiet mode'}, # noqa: E501
26
+ 'dry_run': {'is_flag': True, 'short': 'dr', 'default': False, 'help': 'Enable dry run'},
27
+ 'show': {'is_flag': True, 'short': 'yml', 'default': False, 'help': 'Show runner yaml'},
26
28
  }
27
29
 
28
30
  RUNNER_GLOBAL_OPTS = {
29
31
  'sync': {'is_flag': True, 'help': 'Run tasks synchronously (automatic if no worker is alive)'},
30
32
  'worker': {'is_flag': True, 'default': False, 'help': 'Run tasks in worker'},
31
- 'no_poll': {'is_flag': True, 'default': False, 'help': 'Do not live poll for tasks results when running in worker'},
33
+ 'no_poll': {'is_flag': True, 'short': 'np', 'default': False, 'help': 'Do not live poll for tasks results when running in worker'}, # noqa: E501
32
34
  'proxy': {'type': str, 'help': 'HTTP proxy'},
33
35
  'driver': {'type': str, 'help': 'Export real-time results. E.g: "mongodb"'}
34
36
  # 'debug': {'type': int, 'default': 0, 'help': 'Debug mode'},
@@ -162,35 +164,39 @@ def get_command_options(config):
162
164
  elif opt in RUNNER_GLOBAL_OPTS:
163
165
  prefix = 'Execution'
164
166
 
167
+ # Get opt value from YAML config
168
+ opt_conf_value = task_config_opts.get(opt)
169
+
165
170
  # Get opt conf
166
171
  conf = opt_conf.copy()
172
+ opt_is_flag = conf.get('is_flag', False)
173
+ opt_default = conf.get('default', False if opt_is_flag else None)
174
+ opt_is_required = conf.get('required', False)
167
175
  conf['show_default'] = True
168
176
  conf['prefix'] = prefix
169
- opt_default = conf.get('default', None)
170
- opt_is_flag = conf.get('is_flag', False)
171
- opt_value_in_config = task_config_opts.get(opt)
177
+ conf['default'] = opt_default
178
+ conf['reverse'] = False
172
179
 
173
- # Check if opt already defined in config
174
- if opt_value_in_config:
175
- if conf.get('required', False):
180
+ # Change CLI opt defaults if opt was overriden in YAML config
181
+ if opt_conf_value:
182
+ if opt_is_required:
176
183
  debug('OPT (skipped: opt is required and defined in config)', obj={'opt': opt}, sub=f'cli.{config.name}', verbose=True) # noqa: E501
177
184
  continue
178
185
  mapped_value = cls.opt_value_map.get(opt)
179
186
  if callable(mapped_value):
180
- opt_value_in_config = mapped_value(opt_value_in_config)
187
+ opt_conf_value = mapped_value(opt_conf_value)
181
188
  elif mapped_value:
182
- opt_value_in_config = mapped_value
183
- if opt_value_in_config != opt_default:
189
+ opt_conf_value = mapped_value
190
+
191
+ # Handle option defaults
192
+ if opt_conf_value != opt_default:
184
193
  if opt in opt_cache:
185
194
  continue
186
195
  if opt_is_flag:
187
- conf['reverse'] = True
188
- conf['default'] = not conf['default']
189
- # print(f'{opt}: change default to {opt_value_in_config}')
190
- conf['default'] = opt_value_in_config
196
+ conf['default'] = opt_default = opt_conf_value
191
197
 
192
- # If opt is a flag but the default is True, add opposite flag
193
- if opt_is_flag and opt_default is True:
198
+ # Add reverse flag
199
+ if opt_default is True:
194
200
  conf['reverse'] = True
195
201
 
196
202
  # Check if opt already processed before
@@ -204,7 +210,7 @@ def get_command_options(config):
204
210
  all_opts[opt] = conf
205
211
 
206
212
  # Debug
207
- debug_conf = OrderedDict({'opt': opt, 'config_val': opt_value_in_config or 'N/A', **conf.copy()})
213
+ debug_conf = OrderedDict({'opt': opt, 'config_val': opt_conf_value or 'N/A', **conf.copy()})
208
214
  debug('OPT', obj=debug_conf, sub=f'cli.{config.name}', verbose=True)
209
215
 
210
216
  return all_opts
@@ -224,18 +230,28 @@ def decorate_command_options(opts):
224
230
  for opt_name, opt_conf in reversed_opts.items():
225
231
  conf = opt_conf.copy()
226
232
  short_opt = conf.pop('short', None)
227
- conf.pop('internal', None)
233
+ internal = conf.pop('internal', False)
234
+ display = conf.pop('display', True)
235
+ if internal and not display:
236
+ continue
228
237
  conf.pop('prefix', None)
229
238
  conf.pop('shlex', None)
230
239
  conf.pop('meta', None)
231
240
  conf.pop('supported', None)
232
241
  conf.pop('process', None)
242
+ conf.pop('requires_sudo', None)
233
243
  reverse = conf.pop('reverse', False)
244
+ opposite = conf.pop('opposite', None)
234
245
  long = f'--{opt_name}'
235
246
  short = f'-{short_opt}' if short_opt else f'-{opt_name}'
236
247
  if reverse:
237
- long += f'/--no-{opt_name}'
238
- short += f'/-n{short_opt}' if short else f'/-n{opt_name}'
248
+ if opposite:
249
+ long += f'/--{opposite}'
250
+ short += f'/-{opposite[0]}'
251
+ conf['help'] = conf['help'].replace(opt_name, f'{opt_name} / {opposite}')
252
+ else:
253
+ long += f'/--no-{opt_name}'
254
+ short += f'/-n{short_opt}' if short else f'/-n{opt_name}'
239
255
  f = click.option(long, short, **conf)(f)
240
256
  return f
241
257
  return decorator
@@ -255,7 +271,6 @@ def generate_cli_subcommand(cli_endpoint, func, **opts):
255
271
  def register_runner(cli_endpoint, config):
256
272
  name = config.name
257
273
  input_required = True
258
- input_type = 'targets'
259
274
  command_opts = {
260
275
  'no_args_is_help': True,
261
276
  'context_settings': {
@@ -266,37 +281,44 @@ def register_runner(cli_endpoint, config):
266
281
 
267
282
  if cli_endpoint.name == 'scan':
268
283
  runner_cls = Scan
284
+ input_required = False # allow targets from stdin
269
285
  short_help = config.description or ''
270
286
  short_help += f' [dim]alias: {config.alias}' if config.alias else ''
271
287
  command_opts.update({
272
288
  'name': name,
273
- 'short_help': short_help
289
+ 'short_help': short_help,
290
+ 'no_args_is_help': False
274
291
  })
292
+ input_types = config.input_types
275
293
 
276
294
  elif cli_endpoint.name == 'workflow':
277
295
  runner_cls = Workflow
296
+ input_required = False # allow targets from stdin
278
297
  short_help = config.description or ''
279
298
  short_help = f'{short_help:<55} [dim](alias)[/][bold cyan] {config.alias}' if config.alias else ''
280
299
  command_opts.update({
281
300
  'name': name,
282
- 'short_help': short_help
301
+ 'short_help': short_help,
302
+ 'no_args_is_help': False
283
303
  })
304
+ input_types = config.input_types
284
305
 
285
306
  elif cli_endpoint.name == 'task':
286
307
  runner_cls = Task
287
308
  input_required = False # allow targets from stdin
288
309
  task_cls = Task.get_task_class(config.name)
289
310
  task_category = get_command_category(task_cls)
290
- input_type = task_cls.input_type or 'targets'
291
- short_help = f'[magenta]{task_category:<15}[/]{task_cls.__doc__}'
311
+ short_help = f'[magenta]{task_category:<25}[/] {task_cls.__doc__}'
292
312
  command_opts.update({
293
313
  'name': name,
294
314
  'short_help': short_help,
295
315
  'no_args_is_help': False
296
316
  })
317
+ input_types = task_cls.input_types
297
318
 
298
319
  else:
299
320
  raise ValueError(f"Unrecognized runner endpoint name {cli_endpoint.name}")
321
+ input_types_str = '|'.join(input_types) if input_types else 'targets'
300
322
  options = get_command_options(config)
301
323
 
302
324
  # TODO: maybe allow this in the future
@@ -308,7 +330,7 @@ def register_runner(cli_endpoint, config):
308
330
  # for i in range(0, len(ctx.args), 2)
309
331
  # }
310
332
 
311
- @click.argument(input_type, required=input_required)
333
+ @click.argument(input_types_str, required=input_required)
312
334
  @decorate_command_options(options)
313
335
  @click.pass_context
314
336
  def func(ctx, **opts):
@@ -316,9 +338,16 @@ def register_runner(cli_endpoint, config):
316
338
  worker = opts.pop('worker')
317
339
  ws = opts.pop('workspace')
318
340
  driver = opts.pop('driver', '')
341
+ quiet = opts['quiet']
342
+ dry_run = opts['dry_run']
319
343
  show = opts['show']
320
344
  context = {'workspace_name': ws}
321
345
 
346
+ # Show runner yaml
347
+ if show:
348
+ config.print()
349
+ sys.exit(0)
350
+
322
351
  # Remove options whose values are default values
323
352
  for k, v in options.items():
324
353
  opt_name = k.replace('-', '_')
@@ -330,7 +359,7 @@ def register_runner(cli_endpoint, config):
330
359
  # opts.update(unknown_opts)
331
360
 
332
361
  # Expand input
333
- inputs = opts.pop(input_type)
362
+ inputs = opts.pop(input_types_str)
334
363
  inputs = expand_input(inputs, ctx)
335
364
 
336
365
  # Build hooks from driver name
@@ -359,7 +388,7 @@ def register_runner(cli_endpoint, config):
359
388
  hooks = deep_merge_dicts(*hooks)
360
389
 
361
390
  # Enable sync or not
362
- if sync or show:
391
+ if sync or dry_run:
363
392
  sync = True
364
393
  else:
365
394
  from secator.celery import is_celery_worker_alive
@@ -389,6 +418,7 @@ def register_runner(cli_endpoint, config):
389
418
  'piped_output': ctx.obj['piped_output'],
390
419
  'caller': 'cli',
391
420
  'sync': sync,
421
+ 'quiet': quiet
392
422
  })
393
423
 
394
424
  # Start runner
@@ -396,10 +426,10 @@ def register_runner(cli_endpoint, config):
396
426
  runner.run()
397
427
 
398
428
  generate_cli_subcommand(cli_endpoint, func, **command_opts)
399
- generate_rich_click_opt_groups(cli_endpoint, name, input_type, options)
429
+ generate_rich_click_opt_groups(cli_endpoint, name, input_types, options)
400
430
 
401
431
 
402
- def generate_rich_click_opt_groups(cli_endpoint, name, input_type, options):
432
+ def generate_rich_click_opt_groups(cli_endpoint, name, input_types, options):
403
433
  sortorder = {
404
434
  'Execution': 0,
405
435
  'Output': 1,
@@ -410,7 +440,7 @@ def generate_rich_click_opt_groups(cli_endpoint, name, input_type, options):
410
440
  opt_group = [
411
441
  {
412
442
  'name': 'Targets',
413
- 'options': [input_type],
443
+ 'options': input_types,
414
444
  },
415
445
  ]
416
446
  for prefix in prefixes:
secator/definitions.py CHANGED
@@ -23,10 +23,6 @@ ASCII = rf"""
23
23
  DEBUG = CONFIG.debug.level
24
24
  DEBUG_COMPONENT = CONFIG.debug.component.split(',')
25
25
 
26
- # Default tasks settings
27
- DEFAULT_NUCLEI_FLAGS = os.environ.get('DEFAULT_NUCLEI_FLAGS', '-stats -sj -si 20 -hm -or')
28
- DEFAULT_FEROXBUSTER_FLAGS = os.environ.get('DEFAULT_FEROXBUSTER_FLAGS', '--auto-bail --no-state')
29
-
30
26
  # Constants
31
27
  OPT_NOT_SUPPORTED = -1
32
28
  OPT_PIPE_INPUT = -1
@@ -44,6 +40,9 @@ AUTO_CALIBRATION = 'auto_calibration'
44
40
  CONTENT_TYPE = 'content_type'
45
41
  CONTENT_LENGTH = 'content_length'
46
42
  CIDR_RANGE = 'cidr_range'
43
+ DOCKER_IMAGE = 'docker_image'
44
+ FILENAME = 'filename'
45
+ GIT_REPOSITORY = 'git_repository'
47
46
  CPES = 'cpes'
48
47
  CVES = 'cves'
49
48
  DELAY = 'delay'
@@ -66,6 +65,7 @@ MATCH_CODES = 'match_codes'
66
65
  MATCH_REGEX = 'match_regex'
67
66
  MATCH_SIZE = 'match_size'
68
67
  MATCH_WORDS = 'match_words'
68
+ ORG_NAME = 'org_name'
69
69
  OUTPUT_PATH = 'output_path'
70
70
  PATH = 'path'
71
71
  PERCENT = 'percent'
@@ -104,9 +104,13 @@ REFERENCE = 'reference'
104
104
  REFERENCES = 'references'
105
105
  SEVERITY = 'severity'
106
106
  TAGS = 'tags'
107
+ TECHNOLOGY = 'technology'
107
108
  WEBSERVER = 'webserver'
108
109
  WORDLIST = 'wordlist'
109
110
  WORDS = 'words'
111
+ CERTIFICATE_STATUS_UNKNOWN = 'Unknown'
112
+ CERTIFICATE_STATUS_TRUSTED = 'Trusted'
113
+ CERTIFICATE_STATUS_REVOKED = 'Revoked'
110
114
 
111
115
 
112
116
  def is_importable(module_to_import):