secator 0.1.0__py2.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 (99) hide show
  1. secator/.gitignore +162 -0
  2. secator/__init__.py +0 -0
  3. secator/celery.py +421 -0
  4. secator/cli.py +927 -0
  5. secator/config.py +137 -0
  6. secator/configs/__init__.py +0 -0
  7. secator/configs/profiles/__init__.py +0 -0
  8. secator/configs/profiles/aggressive.yaml +7 -0
  9. secator/configs/profiles/default.yaml +9 -0
  10. secator/configs/profiles/stealth.yaml +7 -0
  11. secator/configs/scans/__init__.py +0 -0
  12. secator/configs/scans/domain.yaml +18 -0
  13. secator/configs/scans/host.yaml +14 -0
  14. secator/configs/scans/network.yaml +17 -0
  15. secator/configs/scans/subdomain.yaml +8 -0
  16. secator/configs/scans/url.yaml +12 -0
  17. secator/configs/workflows/__init__.py +0 -0
  18. secator/configs/workflows/cidr_recon.yaml +28 -0
  19. secator/configs/workflows/code_scan.yaml +11 -0
  20. secator/configs/workflows/host_recon.yaml +41 -0
  21. secator/configs/workflows/port_scan.yaml +34 -0
  22. secator/configs/workflows/subdomain_recon.yaml +33 -0
  23. secator/configs/workflows/url_crawl.yaml +29 -0
  24. secator/configs/workflows/url_dirsearch.yaml +29 -0
  25. secator/configs/workflows/url_fuzz.yaml +35 -0
  26. secator/configs/workflows/url_nuclei.yaml +11 -0
  27. secator/configs/workflows/url_vuln.yaml +55 -0
  28. secator/configs/workflows/user_hunt.yaml +10 -0
  29. secator/configs/workflows/wordpress.yaml +14 -0
  30. secator/decorators.py +346 -0
  31. secator/definitions.py +183 -0
  32. secator/exporters/__init__.py +12 -0
  33. secator/exporters/_base.py +3 -0
  34. secator/exporters/csv.py +29 -0
  35. secator/exporters/gdrive.py +118 -0
  36. secator/exporters/json.py +14 -0
  37. secator/exporters/table.py +7 -0
  38. secator/exporters/txt.py +24 -0
  39. secator/hooks/__init__.py +0 -0
  40. secator/hooks/mongodb.py +212 -0
  41. secator/output_types/__init__.py +24 -0
  42. secator/output_types/_base.py +95 -0
  43. secator/output_types/exploit.py +50 -0
  44. secator/output_types/ip.py +33 -0
  45. secator/output_types/port.py +45 -0
  46. secator/output_types/progress.py +35 -0
  47. secator/output_types/record.py +34 -0
  48. secator/output_types/subdomain.py +42 -0
  49. secator/output_types/tag.py +46 -0
  50. secator/output_types/target.py +30 -0
  51. secator/output_types/url.py +76 -0
  52. secator/output_types/user_account.py +41 -0
  53. secator/output_types/vulnerability.py +97 -0
  54. secator/report.py +95 -0
  55. secator/rich.py +123 -0
  56. secator/runners/__init__.py +12 -0
  57. secator/runners/_base.py +873 -0
  58. secator/runners/_helpers.py +154 -0
  59. secator/runners/command.py +674 -0
  60. secator/runners/scan.py +67 -0
  61. secator/runners/task.py +107 -0
  62. secator/runners/workflow.py +137 -0
  63. secator/serializers/__init__.py +8 -0
  64. secator/serializers/dataclass.py +33 -0
  65. secator/serializers/json.py +15 -0
  66. secator/serializers/regex.py +17 -0
  67. secator/tasks/__init__.py +10 -0
  68. secator/tasks/_categories.py +304 -0
  69. secator/tasks/cariddi.py +102 -0
  70. secator/tasks/dalfox.py +66 -0
  71. secator/tasks/dirsearch.py +88 -0
  72. secator/tasks/dnsx.py +56 -0
  73. secator/tasks/dnsxbrute.py +34 -0
  74. secator/tasks/feroxbuster.py +89 -0
  75. secator/tasks/ffuf.py +85 -0
  76. secator/tasks/fping.py +44 -0
  77. secator/tasks/gau.py +43 -0
  78. secator/tasks/gf.py +34 -0
  79. secator/tasks/gospider.py +71 -0
  80. secator/tasks/grype.py +78 -0
  81. secator/tasks/h8mail.py +80 -0
  82. secator/tasks/httpx.py +104 -0
  83. secator/tasks/katana.py +128 -0
  84. secator/tasks/maigret.py +78 -0
  85. secator/tasks/mapcidr.py +32 -0
  86. secator/tasks/msfconsole.py +176 -0
  87. secator/tasks/naabu.py +52 -0
  88. secator/tasks/nmap.py +341 -0
  89. secator/tasks/nuclei.py +97 -0
  90. secator/tasks/searchsploit.py +53 -0
  91. secator/tasks/subfinder.py +40 -0
  92. secator/tasks/wpscan.py +177 -0
  93. secator/utils.py +404 -0
  94. secator/utils_test.py +183 -0
  95. secator-0.1.0.dist-info/METADATA +379 -0
  96. secator-0.1.0.dist-info/RECORD +99 -0
  97. secator-0.1.0.dist-info/WHEEL +5 -0
  98. secator-0.1.0.dist-info/entry_points.txt +2 -0
  99. secator-0.1.0.dist-info/licenses/LICENSE +60 -0
secator/cli.py ADDED
@@ -0,0 +1,927 @@
1
+ import json
2
+ import os
3
+ import re
4
+ import sys
5
+
6
+ import rich_click as click
7
+ from dotmap import DotMap
8
+ from fp.fp import FreeProxy
9
+ from jinja2 import Template
10
+ from rich.markdown import Markdown
11
+ from rich.rule import Rule
12
+
13
+ from secator.config import ConfigLoader
14
+ from secator.decorators import OrderedGroup, register_runner
15
+ from secator.definitions import (ASCII, CVES_FOLDER, DATA_FOLDER,
16
+ OPT_NOT_SUPPORTED, PAYLOADS_FOLDER,
17
+ ROOT_FOLDER, SCRIPTS_FOLDER, VERSION,
18
+ WORKER_ADDON_ENABLED, DEV_ADDON_ENABLED, DEV_PACKAGE)
19
+ from secator.rich import console
20
+ from secator.runners import Command
21
+ from secator.serializers.dataclass import loads_dataclass
22
+ from secator.utils import (debug, detect_host, discover_tasks, find_list_item,
23
+ flatten, print_results_table)
24
+
25
+ click.rich_click.USE_RICH_MARKUP = True
26
+
27
+ ALL_TASKS = discover_tasks()
28
+ ALL_CONFIGS = ConfigLoader.load_all()
29
+ ALL_WORKFLOWS = ALL_CONFIGS.workflow
30
+ ALL_SCANS = ALL_CONFIGS.scan
31
+ DEFAULT_CMD_OPTS = {
32
+ 'no_capture': True,
33
+ 'print_cmd': True,
34
+ }
35
+
36
+
37
+ #-----#
38
+ # CLI #
39
+ #-----#
40
+
41
+ @click.group(cls=OrderedGroup, invoke_without_command=True)
42
+ @click.option('--no-banner', '-nb', is_flag=True, default=False)
43
+ @click.option('--version', '-version', is_flag=True, default=False)
44
+ @click.pass_context
45
+ def cli(ctx, no_banner, version):
46
+ """Secator CLI."""
47
+ if not no_banner:
48
+ print(ASCII, file=sys.stderr)
49
+ if ctx.invoked_subcommand is None:
50
+ if version:
51
+ print(f'Current Version: v{VERSION}')
52
+ else:
53
+ ctx.get_help()
54
+
55
+
56
+ #------#
57
+ # TASK #
58
+ #------#
59
+
60
+ @cli.group(aliases=['x', 't'])
61
+ def task():
62
+ """Run a task."""
63
+ pass
64
+
65
+
66
+ for cls in ALL_TASKS:
67
+ config = DotMap({'name': cls.__name__, 'type': 'task'})
68
+ register_runner(task, config)
69
+
70
+ #----------#
71
+ # WORKFLOW #
72
+ #----------#
73
+
74
+
75
+ @cli.group(cls=OrderedGroup, aliases=['w'])
76
+ def workflow():
77
+ """Run a workflow."""
78
+ pass
79
+
80
+
81
+ for config in sorted(ALL_WORKFLOWS, key=lambda x: x['name']):
82
+ register_runner(workflow, config)
83
+
84
+
85
+ #------#
86
+ # SCAN #
87
+ #------#
88
+
89
+ @cli.group(cls=OrderedGroup, aliases=['s'])
90
+ def scan():
91
+ """Run a scan."""
92
+ pass
93
+
94
+
95
+ for config in sorted(ALL_SCANS, key=lambda x: x['name']):
96
+ register_runner(scan, config)
97
+
98
+
99
+ #--------#
100
+ # WORKER #
101
+ #--------#
102
+
103
+ @cli.command(name='worker', context_settings=dict(ignore_unknown_options=True), aliases=['wk'])
104
+ @click.option('-n', '--hostname', type=str, default='runner', help='Celery worker hostname (unique).')
105
+ @click.option('-c', '--concurrency', type=int, default=100, help='Number of child processes processing the queue.')
106
+ @click.option('-r', '--reload', is_flag=True, help='Autoreload Celery on code changes.')
107
+ @click.option('-Q', '--queue', type=str, default='', help='Listen to a specific queue.')
108
+ @click.option('-P', '--pool', type=str, default='eventlet', help='Pool implementation.')
109
+ @click.option('--check', is_flag=True, help='Check if Celery worker is alive.')
110
+ @click.option('--dev', is_flag=True, help='Start a worker in dev mode (celery multi).')
111
+ @click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
112
+ @click.option('--show', is_flag=True, help='Show command (celery multi).')
113
+ def worker(hostname, concurrency, reload, queue, pool, check, dev, stop, show):
114
+ """Run a worker."""
115
+ if not WORKER_ADDON_ENABLED:
116
+ console.print('[bold red]Missing worker addon: please run `secator install addons worker`[/].')
117
+ sys.exit(1)
118
+ from secator.celery import app, is_celery_worker_alive
119
+ debug('conf', obj=dict(app.conf), obj_breaklines=True, sub='celery.app.conf', level=4)
120
+ debug('registered tasks', obj=list(app.tasks.keys()), obj_breaklines=True, sub='celery.tasks', level=4)
121
+ if check:
122
+ is_celery_worker_alive()
123
+ return
124
+ if not queue:
125
+ queue = 'io,cpu,' + ','.join([r['queue'] for r in app.conf.task_routes.values()])
126
+ app_str = 'secator.celery.app'
127
+ celery = f'{sys.executable} -m celery'
128
+ if dev:
129
+ subcmd = 'stop' if stop else 'show' if show else 'start'
130
+ logfile = '%n.log'
131
+ pidfile = '%n.pid'
132
+ queues = '-Q:1 celery -Q:2 io -Q:3 cpu'
133
+ concur = '-c:1 10 -c:2 100 -c:3 4'
134
+ pool = 'eventlet'
135
+ cmd = f'{celery} -A {app_str} multi {subcmd} 3 {queues} -P {pool} {concur} --logfile={logfile} --pidfile={pidfile}'
136
+ else:
137
+ cmd = f'{celery} -A {app_str} worker -n {hostname} -Q {queue}'
138
+ if pool:
139
+ cmd += f' -P {pool}'
140
+ if concurrency:
141
+ cmd += f' -c {concurrency}'
142
+ if reload:
143
+ patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
144
+ cmd = f'watchmedo auto-restart --directory=./ --patterns="{patterns}" --recursive -- {cmd}'
145
+ Command.run_command(
146
+ cmd,
147
+ name='secator worker',
148
+ **DEFAULT_CMD_OPTS
149
+ )
150
+
151
+
152
+ #--------#
153
+ # REPORT #
154
+ #--------#
155
+
156
+
157
+ @cli.group(aliases=['r'])
158
+ def report():
159
+ """Reports."""
160
+ pass
161
+
162
+
163
+ @report.command('show')
164
+ @click.argument('json_path')
165
+ @click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
166
+ def report_show(json_path, exclude_fields):
167
+ """Show a JSON report as a nicely-formatted table."""
168
+ with open(json_path, 'r') as f:
169
+ report = loads_dataclass(f.read())
170
+ results = flatten(list(report['results'].values()))
171
+ exclude_fields = exclude_fields.split(',')
172
+ print_results_table(
173
+ results,
174
+ title=report['info']['title'],
175
+ exclude_fields=exclude_fields)
176
+
177
+
178
+ #--------#
179
+ # DEPLOY #
180
+ #--------#
181
+
182
+ # TODO: work on this
183
+ # @cli.group(aliases=['d'])
184
+ # def deploy():
185
+ # """Deploy secator."""
186
+ # pass
187
+
188
+ # @deploy.command()
189
+ # def docker_compose():
190
+ # """Deploy secator on docker-compose."""
191
+ # pass
192
+
193
+ # @deploy.command()
194
+ # @click.option('-t', '--target', type=str, default='minikube', help='Deployment target amongst minikube, gke')
195
+ # def k8s():
196
+ # """Deploy secator on Kubernetes."""
197
+ # pass
198
+
199
+
200
+ #--------#
201
+ # HEALTH #
202
+ #--------#
203
+
204
+
205
+ def which(command):
206
+ """Run which on a command.
207
+
208
+ Args:
209
+ command (str): Command to check.
210
+
211
+ Returns:
212
+ secator.Command: Command instance.
213
+ """
214
+ return Command.run_command(
215
+ f'which {command}',
216
+ quiet=True,
217
+ print_errors=False
218
+ )
219
+
220
+
221
+ def version(cls):
222
+ """Get version for a Command.
223
+
224
+ Args:
225
+ cls: Command class.
226
+
227
+ Returns:
228
+ string: Version string or 'n/a' if not found.
229
+ """
230
+ base_cmd = cls.cmd.split(' ')[0]
231
+ if cls.version_flag == OPT_NOT_SUPPORTED:
232
+ return 'N/A'
233
+ version_flag = cls.version_flag or f'{cls.opt_prefix}version'
234
+ version_cmd = f'{base_cmd} {version_flag}'
235
+ return get_version(version_cmd)
236
+
237
+
238
+ def get_version(version_cmd):
239
+ """Run version command and match first version number found.
240
+
241
+ Args:
242
+ version_cmd (str): Command to get the version.
243
+
244
+ Returns:
245
+ str: Version string.
246
+ """
247
+ regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
248
+ ret = Command.run_command(
249
+ version_cmd,
250
+ quiet=True,
251
+ print_errors=False
252
+ )
253
+ match = re.findall(regex, ret.output)
254
+ if not match:
255
+ return 'n/a'
256
+ return match[0]
257
+
258
+
259
+ @cli.command(name='health', aliases=['h'])
260
+ @click.option('--json', '-json', is_flag=True, default=False, help='JSON lines output')
261
+ @click.option('--debug', '-debug', is_flag=True, default=False, help='Debug health output')
262
+ def health(json, debug):
263
+ """Health."""
264
+ tools = [cls for cls in ALL_TASKS]
265
+ status = {'tools': {}, 'languages': {}, 'secator': {}}
266
+
267
+ def print_status(cmd, return_code, version=None, bin=None, category=None):
268
+ s = '[bold green]ok [/]' if return_code == 0 else '[bold red]failed [/]'
269
+ s = f'[bold magenta]{cmd:<15}[/] {s} '
270
+ if return_code == 0 and version:
271
+ if version == 'N/A':
272
+ s += f'[dim blue]{version:<12}[/]'
273
+ else:
274
+ s += f'[bold blue]{version:<12}[/]'
275
+ elif category:
276
+ s += ' '*12 + f'[dim]# secator install {category} {cmd}'
277
+ if bin:
278
+ s += f'[dim gold3]{bin}[/]'
279
+ console.print(s, highlight=False)
280
+
281
+ # Check secator
282
+ console.print(':wrench: [bold gold3]Checking secator ...[/]')
283
+ ret = which('secator')
284
+ if not json:
285
+ print_status('secator', ret.return_code, VERSION, ret.output, None)
286
+ status['secator'] = {'installed': ret.return_code == 0}
287
+
288
+ # Check languages
289
+ console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
290
+ version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version', 'rustc': '--version'}
291
+ for lang, version_flag in version_cmds.items():
292
+ ret = which(lang)
293
+ ret2 = get_version(f'{lang} {version_flag}')
294
+ if not json:
295
+ print_status(lang, ret.return_code, ret2, ret.output, 'lang')
296
+ status['languages'][lang] = {'installed': ret.return_code == 0}
297
+
298
+ # Check tools
299
+ console.print('\n:wrench: [bold gold3]Checking installed tools ...[/]')
300
+ for tool in tools:
301
+ cmd = tool.cmd.split(' ')[0]
302
+ ret = which(cmd)
303
+ ret2 = version(tool)
304
+ if not json:
305
+ print_status(tool.__name__, ret.return_code, ret2, ret.output, 'tools')
306
+ status['tools'][tool.__name__] = {'installed': ret.return_code == 0}
307
+
308
+ # Print JSON health
309
+ if json:
310
+ console.print(status)
311
+
312
+ #---------#
313
+ # INSTALL #
314
+ #---------#
315
+
316
+
317
+ def run_install(cmd, title, next_steps=None):
318
+ with console.status(f'[bold yellow] Installing {title}...'):
319
+ ret = Command.run_command(
320
+ cmd,
321
+ cls_attributes={'shell': True},
322
+ print_cmd=True,
323
+ print_line=True
324
+ )
325
+ if ret.return_code != 0:
326
+ console.print(f':exclamation_mark: Failed to install {title}.', style='bold red')
327
+ else:
328
+ console.print(f':tada: {title.capitalize()} installed successfully !', style='bold green')
329
+ if next_steps:
330
+ console.print('[bold gold3]:wrench: Next steps:[/]')
331
+ for ix, step in enumerate(next_steps):
332
+ console.print(f' :keycap_{ix}: {step}')
333
+ sys.exit(ret.return_code)
334
+
335
+
336
+ @cli.group(aliases=['i'])
337
+ def install():
338
+ "Installations."
339
+ pass
340
+
341
+
342
+ @install.group()
343
+ def addons():
344
+ "Install addons."
345
+ pass
346
+
347
+
348
+ @addons.command('worker')
349
+ def install_worker():
350
+ "Install worker addon."
351
+ run_install(
352
+ cmd=f'{sys.executable} -m pip install secator[worker]',
353
+ title='worker addon',
354
+ next_steps=[
355
+ 'Run "secator worker" to run a Celery worker using the file system as a backend and broker.',
356
+ 'Run "secator x httpx testphp.vulnweb.com" to admire your task running in a worker.',
357
+ '[dim]\[optional][/dim] Run "secator install addons redis" to install the Redis addon.'
358
+ ]
359
+ )
360
+
361
+
362
+ @addons.command('google')
363
+ def install_google():
364
+ "Install google addon."
365
+ run_install(
366
+ cmd=f'{sys.executable} -m pip install secator[google]',
367
+ title='google addon',
368
+ next_steps=[
369
+ 'Set the "GOOGLE_CREDENTIALS_PATH" and "GOOGLE_DRIVE_PARENT_FOLDER_ID" environment variables.',
370
+ 'Run "secator x httpx testphp.vulnweb.com -o gdrive" to admire your results flowing to Google Drive.'
371
+ ]
372
+ )
373
+
374
+
375
+ @addons.command('mongodb')
376
+ def install_mongodb():
377
+ "Install mongodb addon."
378
+ run_install(
379
+ cmd=f'{sys.executable} -m pip install secator[mongodb]',
380
+ title='mongodb addon',
381
+ next_steps=[
382
+ '[dim]\[optional][/] Run "docker run --name mongo -p 27017:27017 -d mongo:latest" to run a local MongoDB instance.',
383
+ 'Set the "MONGODB_URL=mongodb://<url>" environment variable pointing to your MongoDB instance.',
384
+ 'Run "secator x httpx testphp.vulnweb.com -driver mongodb" to save results to MongoDB.'
385
+ ]
386
+ )
387
+
388
+
389
+ @addons.command('redis')
390
+ def install_redis():
391
+ "Install redis addon."
392
+ run_install(
393
+ cmd=f'{sys.executable} -m pip install secator[redis]',
394
+ title='redis addon',
395
+ next_steps=[
396
+ '[dim]\[optional][/] Run "docker run --name redis -p 6379:6379 -d redis" to run a local Redis instance.',
397
+ 'Set the "CELERY_BROKER_URL=redis://<url>" environment variable pointing to your Redis instance.',
398
+ 'Set the "CELERY_RESULT_BACKEND=redis://<url>" environment variable pointing to your Redis instance.',
399
+ 'Run "secator worker" to run a worker.',
400
+ 'Run "secator x httpx testphp.vulnweb.com" to run a test task.'
401
+ ]
402
+ )
403
+
404
+
405
+ @addons.command('dev')
406
+ def install_dev():
407
+ "Install dev addon."
408
+ run_install(
409
+ cmd=f'{sys.executable} -m pip install secator[dev]',
410
+ title='dev addon',
411
+ next_steps=[
412
+ 'Run "secator test lint" to run lint tests.',
413
+ 'Run "secator test unit" to run unit tests.',
414
+ 'Run "secator test integration" to run integration tests.',
415
+ ]
416
+ )
417
+
418
+
419
+ @install.group()
420
+ def langs():
421
+ "Install languages."
422
+ pass
423
+
424
+
425
+ @langs.command('go')
426
+ def install_go():
427
+ """Install Go."""
428
+ run_install(
429
+ cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_go.sh | sudo sh',
430
+ title='Go'
431
+ )
432
+
433
+
434
+ @langs.command('ruby')
435
+ def install_ruby():
436
+ """Install Ruby."""
437
+ run_install(
438
+ cmd='wget -O - https://raw.githubusercontent.com/freelabz/secator/main/scripts/install_ruby.sh | sudo sh',
439
+ title='Ruby'
440
+ )
441
+
442
+
443
+ @install.command('tools')
444
+ @click.argument('cmds', required=False)
445
+ def install_tools(cmds):
446
+ """Install supported tools."""
447
+ if cmds is not None:
448
+ cmds = cmds.split(',')
449
+ tools = [cls for cls in ALL_TASKS if cls.__name__ in cmds]
450
+ else:
451
+ tools = ALL_TASKS
452
+
453
+ for ix, cls in enumerate(tools):
454
+ with console.status(f'[bold yellow][{ix}/{len(tools)}] Installing {cls.__name__} ...'):
455
+ cls.install()
456
+ console.print()
457
+
458
+
459
+ @install.command('cves')
460
+ @click.option('--force', is_flag=True)
461
+ def install_cves(force):
462
+ """Install CVEs to file system for passive vulnerability search."""
463
+ cve_json_path = f'{CVES_FOLDER}/circl-cve-search-expanded.json'
464
+ if not os.path.exists(cve_json_path) or force:
465
+ with console.status('[bold yellow]Downloading zipped CVEs from cve.circl.lu ...[/]'):
466
+ Command.run_command(
467
+ 'wget https://cve.circl.lu/static/circl-cve-search-expanded.json.gz',
468
+ cwd=CVES_FOLDER,
469
+ **DEFAULT_CMD_OPTS
470
+ )
471
+ with console.status('[bold yellow]Unzipping CVEs ...[/]'):
472
+ Command.run_command(
473
+ f'gunzip {CVES_FOLDER}/circl-cve-search-expanded.json.gz',
474
+ cwd=CVES_FOLDER,
475
+ **DEFAULT_CMD_OPTS
476
+ )
477
+ with console.status(f'[bold yellow]Installing CVEs to {CVES_FOLDER} ...[/]'):
478
+ with open(cve_json_path, 'r') as f:
479
+ for line in f:
480
+ data = json.loads(line)
481
+ cve_id = data['id']
482
+ cve_path = f'{CVES_FOLDER}/{cve_id}.json'
483
+ with open(cve_path, 'w') as f:
484
+ f.write(line)
485
+ console.print(f'CVE saved to {cve_path}')
486
+ console.print(':tada: CVEs installed successfully !', style='bold green')
487
+
488
+
489
+ #-------#
490
+ # ALIAS #
491
+ #-------#
492
+
493
+
494
+ @cli.group(aliases=['a'])
495
+ def alias():
496
+ """Aliases."""
497
+ pass
498
+
499
+
500
+ @alias.command('enable')
501
+ @click.pass_context
502
+ def enable_aliases(ctx):
503
+ """Enable aliases."""
504
+ fpath = f'{DATA_FOLDER}/.aliases'
505
+ aliases = ctx.invoke(list_aliases, silent=True)
506
+ aliases_str = '\n'.join(aliases)
507
+ with open(fpath, 'w') as f:
508
+ f.write(aliases_str)
509
+ console.print('')
510
+ console.print(f':file_cabinet: Alias file written to {fpath}', style='bold green')
511
+ console.print('To load the aliases, run:')
512
+ md = f"""
513
+ ```sh
514
+ source {fpath} # load the aliases in the current shell
515
+ echo "source {fpath} >> ~/.bashrc" # or add this line to your ~/.bashrc to load them automatically
516
+ ```
517
+ """
518
+ console.print(Markdown(md))
519
+ console.print()
520
+
521
+
522
+ @alias.command('disable')
523
+ @click.pass_context
524
+ def disable_aliases(ctx):
525
+ """Disable aliases."""
526
+ fpath = f'{DATA_FOLDER}/.unalias'
527
+ aliases = ctx.invoke(list_aliases, silent=True)
528
+ aliases_str = ''
529
+ for alias in aliases:
530
+ aliases_str += alias.split('=')[0].replace('alias', 'unalias') + '\n'
531
+ console.print(f':file_cabinet: Unalias file written to {fpath}', style='bold green')
532
+ console.print('To unload the aliases, run:')
533
+ with open(fpath, 'w') as f:
534
+ f.write(aliases_str)
535
+ md = f"""
536
+ ```sh
537
+ source {fpath}
538
+ ```
539
+ """
540
+ console.print(Markdown(md))
541
+ console.print()
542
+
543
+
544
+ @alias.command('list')
545
+ @click.option('--silent', is_flag=True, default=False, help='No print')
546
+ def list_aliases(silent):
547
+ """List aliases"""
548
+ aliases = []
549
+ aliases.extend([
550
+ f'alias {task.__name__}="secator x {task.__name__}"'
551
+ for task in ALL_TASKS
552
+ ])
553
+ aliases.extend([
554
+ f'alias {workflow.alias}="secator w {workflow.name}"'
555
+ for workflow in ALL_WORKFLOWS
556
+ ])
557
+ aliases.extend([
558
+ f'alias {workflow.name}="secator w {workflow.name}"'
559
+ for workflow in ALL_WORKFLOWS
560
+ ])
561
+ aliases.extend([
562
+ f'alias scan_{scan.name}="secator s {scan.name}"'
563
+ for scan in ALL_SCANS
564
+ ])
565
+ aliases.append('alias listx="secator x"')
566
+ aliases.append('alias listw="secator w"')
567
+ aliases.append('alias lists="secator s"')
568
+
569
+ if silent:
570
+ return aliases
571
+ console.print('Aliases:')
572
+ for alias in aliases:
573
+ alias_split = alias.split('=')
574
+ alias_name, alias_cmd = alias_split[0].replace('alias ', ''), alias_split[1].replace('"', '')
575
+ console.print(f'[bold magenta]{alias_name:<15}-> {alias_cmd}')
576
+
577
+ return aliases
578
+
579
+
580
+ #-------#
581
+ # UTILS #
582
+ #-------#
583
+
584
+
585
+ @cli.group(aliases=['u'])
586
+ def utils():
587
+ """Utilities."""
588
+ pass
589
+
590
+
591
+ @utils.command()
592
+ @click.option('--timeout', type=float, default=0.2, help='Proxy timeout (in seconds)')
593
+ @click.option('--number', '-n', type=int, default=1, help='Number of proxies')
594
+ def get_proxy(timeout, number):
595
+ """Get a random proxy."""
596
+ proxy = FreeProxy(timeout=timeout, rand=True, anonym=True)
597
+ for _ in range(number):
598
+ url = proxy.get()
599
+ print(url)
600
+
601
+
602
+ @utils.command()
603
+ @click.argument('name', type=str, default=None, required=False)
604
+ @click.option('--host', '-h', type=str, default=None, help='Specify LHOST for revshell, otherwise autodetected.')
605
+ @click.option('--port', '-p', type=int, default=9001, show_default=True, help='Specify PORT for revshell')
606
+ @click.option('--interface', '-i', type=str, help='Interface to use to detect IP')
607
+ @click.option('--listen', '-l', is_flag=True, default=False, help='Spawn netcat listener on specified port')
608
+ def revshells(name, host, port, interface, listen):
609
+ """Show reverse shell source codes and run netcat listener."""
610
+ if host is None: # detect host automatically
611
+ host = detect_host(interface)
612
+ if not host:
613
+ console.print(
614
+ f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of available interfaces.',
615
+ style='bold red')
616
+ return
617
+
618
+ with open(f'{SCRIPTS_FOLDER}/revshells.json') as f:
619
+ shells = json.loads(f.read())
620
+ for sh in shells:
621
+ sh['alias'] = '_'.join(sh['name'].lower()
622
+ .replace('-c', '')
623
+ .replace('-e', '')
624
+ .replace('-i', '')
625
+ .replace('c#', 'cs')
626
+ .replace('#', '')
627
+ .replace('(', '')
628
+ .replace(')', '')
629
+ .strip()
630
+ .split(' ')).replace('_1', '')
631
+ cmd = re.sub(r"\s\s+", "", sh.get('command', ''), flags=re.UNICODE)
632
+ cmd = cmd.replace('\n', ' ')
633
+ sh['cmd_short'] = (cmd[:30] + '..') if len(cmd) > 30 else cmd
634
+
635
+ shell = [
636
+ shell for shell in shells if shell['name'] == name or shell['alias'] == name
637
+ ]
638
+ if not shell:
639
+ console.print('Available shells:', style='bold yellow')
640
+ shells_str = [
641
+ '[bold magenta]{alias:<20}[/][dim white]{name:<20}[/][dim gold3]{cmd_short:<20}[/]'.format(**sh)
642
+ for sh in shells
643
+ ]
644
+ console.print('\n'.join(shells_str))
645
+ else:
646
+ shell = shell[0]
647
+ command = shell['command']
648
+ alias = shell['alias']
649
+ name = shell['name']
650
+ command_str = Template(command).render(ip=host, port=port, shell='bash')
651
+ console.print(Rule(f'[bold gold3]{alias}[/] - [bold red]{name} REMOTE SHELL', style='bold red', align='left'))
652
+ lang = shell.get('lang') or 'sh'
653
+ if len(command.splitlines()) == 1:
654
+ console.print()
655
+ print(f'\033[0;36m{command_str}')
656
+ else:
657
+ md = Markdown(f'```{lang}\n{command_str}\n```')
658
+ console.print(md)
659
+ console.print(f'Save this script as rev.{lang} and run it on your target', style='dim italic')
660
+ console.print()
661
+ console.print(Rule(style='bold red'))
662
+
663
+ if listen:
664
+ console.print(f'Starting netcat listener on port {port} ...', style='bold gold3')
665
+ cmd = f'nc -lvnp {port}'
666
+ Command.run_command(
667
+ cmd,
668
+ **DEFAULT_CMD_OPTS
669
+ )
670
+
671
+
672
+ @utils.command()
673
+ @click.option('--directory', '-d', type=str, default=PAYLOADS_FOLDER, show_default=True, help='HTTP server directory')
674
+ @click.option('--host', '-h', type=str, default=None, help='HTTP host')
675
+ @click.option('--port', '-p', type=int, default=9001, help='HTTP server port')
676
+ @click.option('--interface', '-i', type=str, default=None, help='Interface to use to auto-detect host IP')
677
+ def serve(directory, host, port, interface):
678
+ """Serve payloads in HTTP server."""
679
+ LSE_URL = 'https://github.com/diego-treitos/linux-smart-enumeration/releases/latest/download/lse.sh'
680
+ LINPEAS_URL = 'https://github.com/carlospolop/PEASS-ng/releases/latest/download/linpeas.sh'
681
+ SUDOKILLER_URL = 'https://raw.githubusercontent.com/TH3xACE/SUDO_KILLER/master/SUDO_KILLERv2.4.2.sh'
682
+ PAYLOADS = [
683
+ {
684
+ 'fname': 'lse.sh',
685
+ 'description': 'Linux Smart Enumeration',
686
+ 'command': f'wget {LSE_URL} -O lse.sh && chmod 700 lse.sh'
687
+ },
688
+ {
689
+ 'fname': 'linpeas.sh',
690
+ 'description': 'Linux Privilege Escalation Awesome Script',
691
+ 'command': f'wget {LINPEAS_URL} -O linpeas.sh && chmod 700 linpeas.sh'
692
+ },
693
+ {
694
+ 'fname': 'sudo_killer.sh',
695
+ 'description': 'SUDO_KILLER',
696
+ 'command': f'wget {SUDOKILLER_URL} -O sudo_killer.sh && chmod 700 sudo_killer.sh'
697
+ }
698
+ ]
699
+ for ix, payload in enumerate(PAYLOADS):
700
+ descr = payload.get('description', '')
701
+ fname = payload['fname']
702
+ if not os.path.exists(f'{directory}/{fname}'):
703
+ with console.status(f'[bold yellow][{ix}/{len(PAYLOADS)}] Downloading {fname} [dim]({descr})[/] ...[/]'):
704
+ cmd = payload['command']
705
+ console.print(f'[bold magenta]{fname} [dim]({descr})[/] ...[/]', )
706
+ opts = DEFAULT_CMD_OPTS.copy()
707
+ opts['no_capture'] = False
708
+ Command.run_command(
709
+ cmd,
710
+ cls_attributes={'shell': True},
711
+ cwd=directory,
712
+ **opts
713
+ )
714
+ console.print()
715
+
716
+ console.print(Rule())
717
+ console.print(f'Available payloads in {directory}: ', style='bold yellow')
718
+ opts = DEFAULT_CMD_OPTS.copy()
719
+ opts['print_cmd'] = False
720
+ for fname in os.listdir(directory):
721
+ if not host:
722
+ host = detect_host(interface)
723
+ if not host:
724
+ console.print(
725
+ f'Interface "{interface}" could not be found. Run "ifconfig" to see the list of interfaces.',
726
+ style='bold red')
727
+ return
728
+ payload = find_list_item(PAYLOADS, fname, key='fname', default={})
729
+ fdescr = payload.get('description', 'No description')
730
+ console.print(f'{fname} [dim]({fdescr})[/]', style='bold magenta')
731
+ console.print(f'wget http://{host}:{port}/{fname}', style='dim italic')
732
+ console.print('')
733
+ console.print(Rule())
734
+ console.print('Starting HTTP server ...', style='bold yellow')
735
+ Command.run_command(
736
+ f'{sys.executable} -m http.server {port}',
737
+ cwd=directory,
738
+ **DEFAULT_CMD_OPTS
739
+ )
740
+
741
+
742
+ @utils.command()
743
+ @click.argument('record_name', type=str, default=None)
744
+ @click.option('--script', '-s', type=str, default=None, help='Script to run. See scripts/stories/ for examples.')
745
+ @click.option('--interactive', '-i', is_flag=True, default=False, help='Interactive record.')
746
+ @click.option('--width', '-w', type=int, default=None, help='Recording width')
747
+ @click.option('--height', '-h', type=int, default=None, help='Recording height')
748
+ @click.option('--output-dir', type=str, default=f'{ROOT_FOLDER}/images')
749
+ def record(record_name, script, interactive, width, height, output_dir):
750
+ """Record secator session using asciinema."""
751
+ # 120 x 30 is a good ratio for GitHub
752
+ width = width or console.size.width
753
+ height = height or console.size.height
754
+ attrs = {
755
+ 'shell': False,
756
+ 'env': {
757
+ 'RECORD': '1',
758
+ 'LINES': str(height),
759
+ 'PS1': '$ ',
760
+ 'COLUMNS': str(width),
761
+ 'TERM': 'xterm-256color'
762
+ }
763
+ }
764
+ output_cast_path = f'{output_dir}/{record_name}.cast'
765
+ output_gif_path = f'{output_dir}/{record_name}.gif'
766
+
767
+ # Run automated 'story' script with asciinema-automation
768
+ if script:
769
+ # If existing cast file, remove it
770
+ if os.path.exists(output_cast_path):
771
+ os.unlink(output_cast_path)
772
+ console.print(f'Removed existing {output_cast_path}', style='bold green')
773
+
774
+ with console.status('[bold gold3]Recording with asciinema ...[/]'):
775
+ Command.run_command(
776
+ f'asciinema-automation -aa "-c /bin/sh" {script} {output_cast_path} --timeout 200',
777
+ cls_attributes=attrs,
778
+ raw=True,
779
+ **DEFAULT_CMD_OPTS,
780
+ )
781
+ console.print(f'Generated {output_cast_path}', style='bold green')
782
+ elif interactive:
783
+ os.environ.update(attrs['env'])
784
+ Command.run_command(
785
+ f'asciinema rec -c /bin/bash --stdin --overwrite {output_cast_path}',
786
+ )
787
+
788
+ # Resize cast file
789
+ if os.path.exists(output_cast_path):
790
+ with console.status('[bold gold3]Cleaning up .cast and set custom settings ...'):
791
+ with open(output_cast_path, 'r') as f:
792
+ lines = f.readlines()
793
+ updated_lines = []
794
+ for ix, line in enumerate(lines):
795
+ tmp_line = json.loads(line)
796
+ if ix == 0:
797
+ tmp_line['width'] = width
798
+ tmp_line['height'] = height
799
+ tmp_line['env']['SHELL'] = '/bin/sh'
800
+ lines[0] = json.dumps(tmp_line) + '\n'
801
+ updated_lines.append(json.dumps(tmp_line) + '\n')
802
+ elif tmp_line[2].endswith(' \r'):
803
+ tmp_line[2] = tmp_line[2].replace(' \r', '')
804
+ updated_lines.append(json.dumps(tmp_line) + '\n')
805
+ else:
806
+ updated_lines.append(line)
807
+ with open(output_cast_path, 'w') as f:
808
+ f.writelines(updated_lines)
809
+ console.print('')
810
+
811
+ # Edit cast file to reduce long timeouts
812
+ with console.status('[bold gold3] Editing cast file to reduce long commands ...'):
813
+ Command.run_command(
814
+ f'asciinema-edit quantize --range 1 {output_cast_path} --out {output_cast_path}.tmp',
815
+ cls_attributes=attrs,
816
+ raw=True,
817
+ **DEFAULT_CMD_OPTS,
818
+ )
819
+ if os.path.exists(f'{output_cast_path}.tmp'):
820
+ os.replace(f'{output_cast_path}.tmp', output_cast_path)
821
+ console.print(f'Edited {output_cast_path}', style='bold green')
822
+
823
+ # Convert to GIF
824
+ with console.status(f'[bold gold3]Converting to {output_gif_path} ...[/]'):
825
+ Command.run_command(
826
+ f'agg {output_cast_path} {output_gif_path}',
827
+ cls_attributes=attrs,
828
+ **DEFAULT_CMD_OPTS,
829
+ )
830
+ console.print(f'Generated {output_gif_path}', style='bold green')
831
+
832
+
833
+ #------#
834
+ # TEST #
835
+ #------#
836
+
837
+
838
+ @cli.group(cls=OrderedGroup)
839
+ def test():
840
+ """Tests."""
841
+ if not DEV_PACKAGE:
842
+ console.print('[bold red]You MUST use a development version of secator to run tests.[/]')
843
+ sys.exit(1)
844
+ if not DEV_ADDON_ENABLED:
845
+ console.print('[bold red]Missing dev addon: please run `secator install addons dev`')
846
+ sys.exit(1)
847
+ pass
848
+
849
+
850
+ def run_test(cmd, name):
851
+ """Run a test and return the result.
852
+
853
+ Args:
854
+ cmd: Command to run.
855
+ name: Name of the test.
856
+ """
857
+ result = Command.run_command(
858
+ cmd,
859
+ name=name + ' tests',
860
+ cwd=ROOT_FOLDER,
861
+ **DEFAULT_CMD_OPTS
862
+ )
863
+ if result.return_code == 0:
864
+ console.print(f':tada: {name.capitalize()} tests passed !', style='bold green')
865
+ sys.exit(result.return_code)
866
+
867
+
868
+ @test.command()
869
+ def lint():
870
+ """Run lint tests."""
871
+ cmd = f'{sys.executable} -m flake8 secator/'
872
+ run_test(cmd, 'lint')
873
+
874
+
875
+ @test.command()
876
+ @click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
877
+ @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
878
+ @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
879
+ @click.option('--test', '-t', type=str, help='Secator test to run')
880
+ @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
881
+ def unit(tasks, workflows, scans, test, debug=False):
882
+ """Run unit tests."""
883
+ os.environ['TEST_TASKS'] = tasks or ''
884
+ os.environ['TEST_WORKFLOWS'] = workflows or ''
885
+ os.environ['TEST_SCANS'] = scans or ''
886
+ os.environ['DEBUG'] = str(debug)
887
+ os.environ['DEFAULT_STORE_HTTP_RESPONSES'] = '0'
888
+ os.environ['DEFAULT_SKIP_CVE_SEARCH'] = '1'
889
+
890
+ cmd = f'{sys.executable} -m coverage run --omit="*test*" -m unittest'
891
+ if test:
892
+ if not test.startswith('tests.unit'):
893
+ test = f'tests.unit.{test}'
894
+ cmd += f' {test}'
895
+ else:
896
+ cmd += ' discover -v tests.unit'
897
+ run_test(cmd, 'unit')
898
+
899
+
900
+ @test.command()
901
+ @click.option('--tasks', type=str, default='', help='Secator tasks to test (comma-separated)')
902
+ @click.option('--workflows', type=str, default='', help='Secator workflows to test (comma-separated)')
903
+ @click.option('--scans', type=str, default='', help='Secator scans to test (comma-separated)')
904
+ @click.option('--test', '-t', type=str, help='Secator test to run')
905
+ @click.option('--debug', '-d', type=int, default=0, help='Add debug information')
906
+ def integration(tasks, workflows, scans, test, debug):
907
+ """Run integration tests."""
908
+ os.environ['TEST_TASKS'] = tasks or ''
909
+ os.environ['TEST_WORKFLOWS'] = workflows or ''
910
+ os.environ['TEST_SCANS'] = scans or ''
911
+ os.environ['DEBUG'] = str(debug)
912
+ os.environ['DEFAULT_SKIP_CVE_SEARCH'] = '1'
913
+ cmd = f'{sys.executable} -m unittest'
914
+ if test:
915
+ if not test.startswith('tests.integration'):
916
+ test = f'tests.integration.{test}'
917
+ cmd += f' {test}'
918
+ else:
919
+ cmd += ' discover -v tests.integration'
920
+ run_test(cmd, 'integration')
921
+
922
+
923
+ @test.command()
924
+ def coverage():
925
+ """Run coverage report."""
926
+ cmd = f'{sys.executable} -m coverage report -m --omit=*/site-packages/*,*/tests/*'
927
+ run_test(cmd, 'coverage')