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.
- secator/celery.py +10 -5
- secator/celery_signals.py +2 -11
- secator/cli.py +309 -69
- secator/config.py +3 -2
- secator/configs/profiles/aggressive.yaml +6 -5
- secator/configs/profiles/default.yaml +6 -7
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/workflows/host_recon.yaml +11 -2
- secator/configs/workflows/url_dirsearch.yaml +5 -0
- secator/configs/workflows/url_params_fuzz.yaml +25 -0
- secator/configs/workflows/wordpress.yaml +4 -1
- secator/decorators.py +64 -34
- secator/definitions.py +8 -4
- secator/installer.py +84 -49
- secator/output_types/__init__.py +2 -1
- secator/output_types/certificate.py +78 -0
- secator/output_types/stat.py +3 -0
- secator/output_types/user_account.py +1 -1
- secator/report.py +2 -2
- secator/rich.py +1 -1
- secator/runners/_base.py +50 -11
- secator/runners/_helpers.py +15 -3
- secator/runners/command.py +85 -21
- secator/runners/scan.py +6 -3
- secator/runners/task.py +1 -0
- secator/runners/workflow.py +22 -4
- secator/tasks/_categories.py +25 -17
- secator/tasks/arjun.py +92 -0
- secator/tasks/bbot.py +33 -4
- secator/tasks/bup.py +4 -2
- secator/tasks/cariddi.py +17 -4
- secator/tasks/dalfox.py +4 -2
- secator/tasks/dirsearch.py +4 -2
- secator/tasks/dnsx.py +5 -2
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +5 -2
- secator/tasks/ffuf.py +7 -3
- secator/tasks/fping.py +4 -1
- secator/tasks/gau.py +5 -2
- secator/tasks/gf.py +4 -2
- secator/tasks/gitleaks.py +79 -0
- secator/tasks/gospider.py +5 -2
- secator/tasks/grype.py +5 -2
- secator/tasks/h8mail.py +4 -2
- secator/tasks/httpx.py +6 -3
- secator/tasks/katana.py +6 -3
- secator/tasks/maigret.py +4 -2
- secator/tasks/mapcidr.py +5 -3
- secator/tasks/msfconsole.py +8 -6
- secator/tasks/naabu.py +16 -5
- secator/tasks/nmap.py +31 -29
- secator/tasks/nuclei.py +18 -10
- secator/tasks/searchsploit.py +8 -3
- secator/tasks/subfinder.py +6 -3
- secator/tasks/testssl.py +276 -0
- secator/tasks/trivy.py +98 -0
- secator/tasks/wafw00f.py +85 -0
- secator/tasks/wpprobe.py +96 -0
- secator/tasks/wpscan.py +8 -4
- secator/template.py +61 -67
- secator/utils.py +31 -18
- secator/utils_test.py +34 -10
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/METADATA +11 -3
- secator-0.15.1.dist-info/RECORD +128 -0
- secator/configs/profiles/stealth.yaml +0 -7
- secator-0.10.1a12.dist-info/RECORD +0 -116
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/WHEEL +0 -0
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/entry_points.txt +0 -0
- {secator-0.10.1a12.dist-info → secator-0.15.1.dist-info}/licenses/LICENSE +0 -0
secator/tasks/trivy.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from secator.config import CONFIG
|
|
6
|
+
from secator.decorators import task
|
|
7
|
+
from secator.definitions import (THREADS, OUTPUT_PATH, OPT_NOT_SUPPORTED, HEADER, DELAY, FOLLOW_REDIRECT,
|
|
8
|
+
DOCKER_IMAGE, PATH, GIT_REPOSITORY, PROXY, RATE_LIMIT, RETRIES, TIMEOUT,
|
|
9
|
+
USER_AGENT)
|
|
10
|
+
from secator.tasks._categories import Vuln
|
|
11
|
+
from secator.output_types import Vulnerability, Tag, Info, Error
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@task()
|
|
15
|
+
class trivy(Vuln):
|
|
16
|
+
"""Comprehensive and versatile security scanner."""
|
|
17
|
+
cmd = 'trivy'
|
|
18
|
+
tags = ['vuln', 'scan']
|
|
19
|
+
input_flag = None
|
|
20
|
+
input_types = [DOCKER_IMAGE, PATH, GIT_REPOSITORY]
|
|
21
|
+
json_flag = '-f json'
|
|
22
|
+
opts = {
|
|
23
|
+
"mode": {"type": click.Choice(['image', 'fs', 'repo']), 'default': 'image', 'help': 'Trivy mode', 'required': True} # noqa: E501
|
|
24
|
+
}
|
|
25
|
+
opt_key_map = {
|
|
26
|
+
THREADS: OPT_NOT_SUPPORTED,
|
|
27
|
+
HEADER: OPT_NOT_SUPPORTED,
|
|
28
|
+
DELAY: OPT_NOT_SUPPORTED,
|
|
29
|
+
FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
|
|
30
|
+
PROXY: OPT_NOT_SUPPORTED,
|
|
31
|
+
RATE_LIMIT: OPT_NOT_SUPPORTED,
|
|
32
|
+
RETRIES: OPT_NOT_SUPPORTED,
|
|
33
|
+
TIMEOUT: OPT_NOT_SUPPORTED,
|
|
34
|
+
USER_AGENT: OPT_NOT_SUPPORTED
|
|
35
|
+
}
|
|
36
|
+
output_types = [Tag, Vulnerability]
|
|
37
|
+
install_version = 'v0.61.1'
|
|
38
|
+
install_cmd = (
|
|
39
|
+
'curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh |'
|
|
40
|
+
f'sudo sh -s -- -b {CONFIG.dirs.bin} [install_version]'
|
|
41
|
+
)
|
|
42
|
+
install_github_handle = 'aquasecurity/trivy'
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def on_cmd(self):
|
|
46
|
+
mode = self.get_opt_value('mode')
|
|
47
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
48
|
+
if not output_path:
|
|
49
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
50
|
+
self.output_path = output_path
|
|
51
|
+
self.cmd = self.cmd.replace(f' -mode {mode}', '').replace('trivy', f'trivy {mode}')
|
|
52
|
+
self.cmd += f' -o {self.output_path}'
|
|
53
|
+
|
|
54
|
+
@staticmethod
|
|
55
|
+
def on_cmd_done(self):
|
|
56
|
+
if not os.path.exists(self.output_path):
|
|
57
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
61
|
+
with open(self.output_path, 'r') as f:
|
|
62
|
+
results = yaml.safe_load(f.read()).get('Results', [])
|
|
63
|
+
for item in results:
|
|
64
|
+
for vuln in item.get('Vulnerabilities', []):
|
|
65
|
+
vuln_id = vuln['VulnerabilityID']
|
|
66
|
+
extra_data = {}
|
|
67
|
+
if 'PkgName' in vuln:
|
|
68
|
+
extra_data['product'] = vuln['PkgName']
|
|
69
|
+
if 'InstalledVersion' in vuln:
|
|
70
|
+
extra_data['version'] = vuln['InstalledVersion']
|
|
71
|
+
cvss = vuln.get('CVSS', {})
|
|
72
|
+
cvss_score = -1
|
|
73
|
+
for _, cvss_data in cvss.items():
|
|
74
|
+
cvss_score = cvss_data.get('V3Score', -1) or cvss_data.get('V2Score', -1)
|
|
75
|
+
data = {
|
|
76
|
+
'name': vuln_id,
|
|
77
|
+
'id': vuln_id,
|
|
78
|
+
'provider': vuln.get('DataSource', {}).get('ID', ''),
|
|
79
|
+
'description': vuln.get('Description'),
|
|
80
|
+
'matched_at': self.inputs[0],
|
|
81
|
+
'confidence': 'high',
|
|
82
|
+
'severity': vuln['Severity'].lower(),
|
|
83
|
+
'cvss_score': cvss_score,
|
|
84
|
+
'reference': vuln.get('PrimaryURL', ''),
|
|
85
|
+
'references': vuln.get('References', []),
|
|
86
|
+
'extra_data': extra_data
|
|
87
|
+
}
|
|
88
|
+
if vuln_id.startswith('CVE'):
|
|
89
|
+
remote_data = Vuln.lookup_cve(vuln_id)
|
|
90
|
+
if remote_data:
|
|
91
|
+
data.update(remote_data)
|
|
92
|
+
yield Vulnerability(**data)
|
|
93
|
+
for secret in item.get('Secrets', []):
|
|
94
|
+
yield Tag(
|
|
95
|
+
name=secret['RuleID'],
|
|
96
|
+
match=secret['Match'],
|
|
97
|
+
extra_data={k: v for k, v in secret.items() if k not in ['RuleID', 'Match']}
|
|
98
|
+
)
|
secator/tasks/wafw00f.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import yaml
|
|
3
|
+
|
|
4
|
+
from secator.decorators import task
|
|
5
|
+
from secator.runners import Command
|
|
6
|
+
from secator.definitions import (OUTPUT_PATH, HEADER, PROXY, URL, TIMEOUT)
|
|
7
|
+
from secator.output_types import Tag, Info, Error
|
|
8
|
+
from secator.tasks._categories import OPTS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@task()
|
|
12
|
+
class wafw00f(Command):
|
|
13
|
+
"""Web Application Firewall Fingerprinting tool."""
|
|
14
|
+
cmd = 'wafw00f'
|
|
15
|
+
tags = ['waf', 'scan']
|
|
16
|
+
input_types = [URL]
|
|
17
|
+
input_flag = None
|
|
18
|
+
file_flag = '-i'
|
|
19
|
+
json_flag = '-f json'
|
|
20
|
+
encoding = 'ansi'
|
|
21
|
+
opt_prefix = '--'
|
|
22
|
+
meta_opts = {
|
|
23
|
+
PROXY: OPTS[PROXY],
|
|
24
|
+
HEADER: OPTS[HEADER],
|
|
25
|
+
TIMEOUT: OPTS[TIMEOUT]
|
|
26
|
+
}
|
|
27
|
+
opts = {
|
|
28
|
+
'list': {'is_flag': True, 'default': False, 'help': 'List all WAFs that WAFW00F is able to detect'},
|
|
29
|
+
'waf_type': {'type': str, 'short': 'wt', 'help': 'Test for one specific WAF'},
|
|
30
|
+
'find_all': {'is_flag': True, 'short': 'ta', 'default': False, 'help': 'Find all WAFs which match the signatures, do not stop testing on the first one'}, # noqa: E501
|
|
31
|
+
'no_follow_redirects': {'is_flag': True, 'short': 'nfr', 'default': False, 'help': 'Do not follow redirections given by 3xx responses'}, # noqa: E501
|
|
32
|
+
}
|
|
33
|
+
opt_key_map = {
|
|
34
|
+
HEADER: 'headers',
|
|
35
|
+
PROXY: 'proxy',
|
|
36
|
+
'waf_type': 'test',
|
|
37
|
+
'find_all': 'findall',
|
|
38
|
+
'no_follow_redirects': 'noredirect',
|
|
39
|
+
}
|
|
40
|
+
output_types = [Tag]
|
|
41
|
+
install_version = 'v2.3.1'
|
|
42
|
+
install_cmd = 'pipx install git+https://github.com/EnableSecurity/wafw00f.git@[install_version] --force'
|
|
43
|
+
install_github_handle = 'EnableSecurity/wafw00f'
|
|
44
|
+
proxy_http = True
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def on_cmd(self):
|
|
48
|
+
self.output_path = self.get_opt_value(OUTPUT_PATH)
|
|
49
|
+
if not self.output_path:
|
|
50
|
+
self.output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
51
|
+
self.cmd += f' -o {self.output_path}'
|
|
52
|
+
|
|
53
|
+
self.headers = self.get_opt_value(HEADER)
|
|
54
|
+
if self.headers:
|
|
55
|
+
header_file = f'{self.reports_folder}/.inputs/headers.txt'
|
|
56
|
+
with open(header_file, 'w') as f:
|
|
57
|
+
for header in self.headers.split(';;'):
|
|
58
|
+
f.write(f'{header}\n')
|
|
59
|
+
self.cmd = self.cmd.replace(self.headers, header_file)
|
|
60
|
+
|
|
61
|
+
@staticmethod
|
|
62
|
+
def on_cmd_done(self):
|
|
63
|
+
# Skip parsing if list mode
|
|
64
|
+
list_mode = self.get_opt_value('list')
|
|
65
|
+
if list_mode:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if not os.path.exists(self.output_path):
|
|
69
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
73
|
+
with open(self.output_path, 'r') as f:
|
|
74
|
+
results = yaml.safe_load(f.read())
|
|
75
|
+
|
|
76
|
+
if len(results) > 0 and results[0]['detected']:
|
|
77
|
+
waf_name = results[0]['firewall']
|
|
78
|
+
url = results[0]['url']
|
|
79
|
+
match = results[0]['trigger_url']
|
|
80
|
+
manufacter = results[0]['manufacturer']
|
|
81
|
+
yield Tag(
|
|
82
|
+
name=waf_name + ' WAF',
|
|
83
|
+
match=url,
|
|
84
|
+
extra_data={'waf_name': waf_name, 'manufacter': manufacter, 'trigger_url': match}
|
|
85
|
+
)
|
secator/tasks/wpprobe.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import yaml
|
|
3
|
+
|
|
4
|
+
from secator.decorators import task
|
|
5
|
+
from secator.runners import Command
|
|
6
|
+
from secator.definitions import OUTPUT_PATH, THREADS, URL
|
|
7
|
+
from secator.output_types import Vulnerability, Tag, Info, Warning
|
|
8
|
+
from secator.tasks._categories import OPTS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@task()
|
|
12
|
+
class wpprobe(Command):
|
|
13
|
+
"""Fast wordpress plugin enumeration tool."""
|
|
14
|
+
cmd = 'wpprobe'
|
|
15
|
+
tags = ['vuln', 'scan', 'wordpress']
|
|
16
|
+
file_flag = '-f'
|
|
17
|
+
input_flag = '-u'
|
|
18
|
+
input_types = [URL]
|
|
19
|
+
opt_prefix = '-'
|
|
20
|
+
opts = {
|
|
21
|
+
'mode': {'type': click.Choice(['scan', 'update', 'update-db']), 'default': 'scan', 'help': 'WPProbe mode', 'required': True, 'internal': True}, # noqa: E501
|
|
22
|
+
'output_path': {'type': str, 'default': None, 'help': 'Output JSON file path', 'internal': True, 'display': False}, # noqa: E501
|
|
23
|
+
}
|
|
24
|
+
meta_opts = {
|
|
25
|
+
THREADS: OPTS[THREADS]
|
|
26
|
+
}
|
|
27
|
+
opt_key_map = {
|
|
28
|
+
THREADS: 't'
|
|
29
|
+
}
|
|
30
|
+
output_types = [Vulnerability, Tag]
|
|
31
|
+
install_version = 'v0.5.6'
|
|
32
|
+
install_cmd = 'go install github.com/Chocapikk/wpprobe@[install_version]'
|
|
33
|
+
install_github_handle = 'Chocapikk/wpprobe'
|
|
34
|
+
install_post = {
|
|
35
|
+
'*': 'wpprobe update && wpprobe update-db'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def on_cmd(self):
|
|
40
|
+
mode = self.get_opt_value('mode')
|
|
41
|
+
if mode == 'update' or mode == 'update-db':
|
|
42
|
+
self.cmd = f'{wpprobe.cmd} {mode}'
|
|
43
|
+
return
|
|
44
|
+
self.cmd = self.cmd.replace(wpprobe.cmd, f'{wpprobe.cmd} {mode}')
|
|
45
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
46
|
+
if not output_path:
|
|
47
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
48
|
+
self.output_path = output_path
|
|
49
|
+
self.cmd += f' -o {self.output_path}'
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def on_cmd_done(self):
|
|
53
|
+
if not self.get_opt_value('mode') == 'scan':
|
|
54
|
+
return
|
|
55
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
56
|
+
with open(self.output_path, 'r') as f:
|
|
57
|
+
results = yaml.safe_load(f.read())
|
|
58
|
+
if not results or 'url' not in results:
|
|
59
|
+
yield Warning(message='No results found !')
|
|
60
|
+
return
|
|
61
|
+
url = results['url']
|
|
62
|
+
for plugin_name, plugin_data in results['plugins'].items():
|
|
63
|
+
for plugin_data_version in plugin_data:
|
|
64
|
+
plugin_version = plugin_data_version['version']
|
|
65
|
+
yield Tag(
|
|
66
|
+
name=f'Wordpress plugin - {plugin_name} {plugin_version}',
|
|
67
|
+
match=url,
|
|
68
|
+
extra_data={
|
|
69
|
+
'name': plugin_name,
|
|
70
|
+
'version': plugin_version
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
severities = plugin_data_version.get('severities', {})
|
|
74
|
+
for severity, severity_data in severities.items():
|
|
75
|
+
if severity == 'None':
|
|
76
|
+
severity = 'unknown'
|
|
77
|
+
for item in severity_data:
|
|
78
|
+
for vuln in item['vulnerabilities']:
|
|
79
|
+
auth_type = item.get('auth_type')
|
|
80
|
+
extra_data = {
|
|
81
|
+
'plugin_name': plugin_name,
|
|
82
|
+
'plugin_version': plugin_version,
|
|
83
|
+
}
|
|
84
|
+
if auth_type:
|
|
85
|
+
extra_data['auth_type'] = auth_type
|
|
86
|
+
yield Vulnerability(
|
|
87
|
+
name=vuln['title'],
|
|
88
|
+
id=vuln['cve'],
|
|
89
|
+
severity=severity,
|
|
90
|
+
cvss_score=vuln['cvss_score'],
|
|
91
|
+
tags=[plugin_name],
|
|
92
|
+
reference=vuln['cve_link'],
|
|
93
|
+
extra_data=extra_data,
|
|
94
|
+
matched_at=url,
|
|
95
|
+
confidence='high'
|
|
96
|
+
)
|
secator/tasks/wpscan.py
CHANGED
|
@@ -16,10 +16,11 @@ from secator.tasks._categories import VulnHttp
|
|
|
16
16
|
@task()
|
|
17
17
|
class wpscan(VulnHttp):
|
|
18
18
|
"""Wordpress security scanner."""
|
|
19
|
-
cmd = 'wpscan --
|
|
19
|
+
cmd = 'wpscan --force --verbose'
|
|
20
|
+
tags = ['vuln', 'scan', 'wordpress']
|
|
20
21
|
file_flag = None
|
|
21
22
|
input_flag = '--url'
|
|
22
|
-
|
|
23
|
+
input_types = [URL]
|
|
23
24
|
json_flag = '-f json'
|
|
24
25
|
opt_prefix = '--'
|
|
25
26
|
opts = {
|
|
@@ -30,7 +31,9 @@ class wpscan(VulnHttp):
|
|
|
30
31
|
'passwords': {'type': str, 'help': 'List of passwords to use during the password attack.'},
|
|
31
32
|
'usernames': {'type': str, 'help': 'List of usernames to use during the password attack.'},
|
|
32
33
|
'login_uri': {'type': str, 'short': 'lu', 'help': 'URI of the login page if different from /wp-login.php'},
|
|
33
|
-
'detection_mode': {'type': str, 'short': 'dm', 'help': 'Detection mode between mixed, passive, and aggressive'}
|
|
34
|
+
'detection_mode': {'type': str, 'short': 'dm', 'help': 'Detection mode between mixed, passive, and aggressive'},
|
|
35
|
+
'random_user_agent': {'is_flag': True, 'short': 'rua', 'help': 'Random user agent'},
|
|
36
|
+
'disable_tls_checks': {'is_flag': True, 'short': 'dtc', 'help': 'Disable TLS checks'}
|
|
34
37
|
}
|
|
35
38
|
opt_key_map = {
|
|
36
39
|
HEADER: OPT_NOT_SUPPORTED,
|
|
@@ -72,7 +75,8 @@ class wpscan(VulnHttp):
|
|
|
72
75
|
'pacman': ['make', 'ruby-erb'],
|
|
73
76
|
'*': ['make']
|
|
74
77
|
}
|
|
75
|
-
|
|
78
|
+
install_version = '3.8.28'
|
|
79
|
+
install_cmd = f'gem install wpscan -v [install_version] --user-install -n {CONFIG.dirs.bin}'
|
|
76
80
|
install_post = {
|
|
77
81
|
'kali': (
|
|
78
82
|
f'gem uninstall nokogiri --user-install -n {CONFIG.dirs.bin} --force --executables && '
|
secator/template.py
CHANGED
|
@@ -8,85 +8,45 @@ from dotmap import DotMap
|
|
|
8
8
|
|
|
9
9
|
from secator.config import CONFIG, CONFIGS_FOLDER
|
|
10
10
|
from secator.rich import console
|
|
11
|
-
from secator.utils import convert_functions_to_strings
|
|
11
|
+
from secator.utils import convert_functions_to_strings, debug
|
|
12
|
+
from secator.output_types import Error
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
TEMPLATES_DIR_KEYS = ['workflow', 'scan', 'profile']
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def load_template(name):
|
|
18
|
-
"""Load a config by name.
|
|
19
|
-
|
|
20
|
-
Args:
|
|
21
|
-
name: Name of the config, for instances profiles/aggressive or workflows/domain_scan.
|
|
22
|
-
|
|
23
|
-
Returns:
|
|
24
|
-
dict: Loaded config.
|
|
25
|
-
"""
|
|
26
|
-
path = CONFIGS_FOLDER / f'{name}.yaml'
|
|
27
|
-
if not path.exists():
|
|
28
|
-
console.log(f'Config "{name}" could not be loaded.')
|
|
29
|
-
return
|
|
30
|
-
with path.open('r') as f:
|
|
31
|
-
return yaml.load(f.read(), Loader=yaml.Loader)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def find_templates():
|
|
35
|
-
results = {'scan': [], 'workflow': [], 'profile': []}
|
|
36
|
-
dirs_type = [CONFIGS_FOLDER]
|
|
37
|
-
if CONFIG.dirs.templates:
|
|
38
|
-
dirs_type.append(CONFIG.dirs.templates)
|
|
39
|
-
paths = []
|
|
40
|
-
for dir in dirs_type:
|
|
41
|
-
dir_paths = [
|
|
42
|
-
Path(path)
|
|
43
|
-
for path in glob.glob(str(dir).rstrip('/') + '/**/*.y*ml', recursive=True)
|
|
44
|
-
]
|
|
45
|
-
paths.extend(dir_paths)
|
|
46
|
-
for path in paths:
|
|
47
|
-
with path.open('r') as f:
|
|
48
|
-
try:
|
|
49
|
-
config = yaml.load(f.read(), yaml.Loader)
|
|
50
|
-
type = config.get('type')
|
|
51
|
-
if type:
|
|
52
|
-
results[type].append(path)
|
|
53
|
-
except yaml.YAMLError as exc:
|
|
54
|
-
console.log(f'Unable to load config at {path}')
|
|
55
|
-
console.log(str(exc))
|
|
56
|
-
return results
|
|
14
|
+
TEMPLATES = []
|
|
57
15
|
|
|
58
16
|
|
|
59
17
|
class TemplateLoader(DotMap):
|
|
60
18
|
|
|
61
19
|
def __init__(self, input={}, name=None, **kwargs):
|
|
62
20
|
if name:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
21
|
+
if '/' not in name:
|
|
22
|
+
console.print(Error(message=f'Cannot load {name}: you should specify a type for the template when loading by name (e.g. workflow/<workflow_name>)')) # noqa: E501
|
|
23
|
+
return
|
|
24
|
+
_type, name = name.split('/')
|
|
25
|
+
config = next((p for p in TEMPLATES if p['type'] == _type and p['name'] == name in str(p)), None)
|
|
26
|
+
if not config:
|
|
27
|
+
console.print(Error(message=f'Template {name} not found in loaded templates'))
|
|
28
|
+
config = {}
|
|
29
|
+
elif isinstance(input, dict):
|
|
68
30
|
config = input
|
|
69
|
-
|
|
31
|
+
elif isinstance(input, Path) or Path(input).exists():
|
|
32
|
+
config = self._load_from_path(input)
|
|
33
|
+
config['_path'] = str(input)
|
|
34
|
+
elif isinstance(input, str):
|
|
35
|
+
config = self._load(input)
|
|
36
|
+
super().__init__(config, **kwargs)
|
|
70
37
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
38
|
+
def add_to_templates(self):
|
|
39
|
+
TEMPLATES.append(self)
|
|
40
|
+
|
|
41
|
+
def _load_from_path(self, path):
|
|
74
42
|
if not path.exists():
|
|
75
|
-
console.
|
|
43
|
+
console.print(Error(message=f'Config path {path} does not exists'))
|
|
76
44
|
return
|
|
77
45
|
with path.open('r') as f:
|
|
78
|
-
return
|
|
79
|
-
|
|
80
|
-
def _load_from_name(self, name):
|
|
81
|
-
return load_template(name)
|
|
46
|
+
return self._load(f.read())
|
|
82
47
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
configs = find_templates()
|
|
86
|
-
return TemplateLoader({
|
|
87
|
-
key: [TemplateLoader(path) for path in configs[key]]
|
|
88
|
-
for key in TEMPLATES_DIR_KEYS
|
|
89
|
-
})
|
|
48
|
+
def _load(self, input):
|
|
49
|
+
return yaml.load(input, Loader=yaml.Loader)
|
|
90
50
|
|
|
91
51
|
@property
|
|
92
52
|
def supported_opts(self):
|
|
@@ -98,6 +58,17 @@ class TemplateLoader(DotMap):
|
|
|
98
58
|
"""Property to access tasks easily."""
|
|
99
59
|
return self._extract_tasks()
|
|
100
60
|
|
|
61
|
+
def print(self):
|
|
62
|
+
"""Print config as highlighted yaml."""
|
|
63
|
+
config = self.toDict()
|
|
64
|
+
_path = config.pop('_path', None)
|
|
65
|
+
if _path:
|
|
66
|
+
console.print(f'[italic green]{_path}[/]\n')
|
|
67
|
+
yaml_str = yaml.dump(config, indent=4, sort_keys=False)
|
|
68
|
+
from rich.syntax import Syntax
|
|
69
|
+
yaml_highlight = Syntax(yaml_str, 'yaml', line_numbers=True)
|
|
70
|
+
console.print(yaml_highlight)
|
|
71
|
+
|
|
101
72
|
def _collect_supported_opts(self):
|
|
102
73
|
"""Collect supported options from the tasks extracted from the config."""
|
|
103
74
|
tasks = self._extract_tasks()
|
|
@@ -141,7 +112,7 @@ class TemplateLoader(DotMap):
|
|
|
141
112
|
# For each workflow in the scan, load it and incorporate it with a unique prefix
|
|
142
113
|
for wf_name, _ in self.workflows.items():
|
|
143
114
|
name = wf_name.split('/')[0]
|
|
144
|
-
config = TemplateLoader(name=f'
|
|
115
|
+
config = TemplateLoader(name=f'workflow/{name}')
|
|
145
116
|
wf_tasks = config.flat_tasks
|
|
146
117
|
# Prefix tasks from this workflow with its name to prevent collision
|
|
147
118
|
for task_key, task_val in wf_tasks.items():
|
|
@@ -153,3 +124,26 @@ class TemplateLoader(DotMap):
|
|
|
153
124
|
parse_config(self.tasks)
|
|
154
125
|
|
|
155
126
|
return dict(tasks)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def find_templates():
|
|
130
|
+
results = []
|
|
131
|
+
dirs = [CONFIGS_FOLDER]
|
|
132
|
+
if CONFIG.dirs.templates:
|
|
133
|
+
dirs.append(CONFIG.dirs.templates)
|
|
134
|
+
paths = []
|
|
135
|
+
for dir in dirs:
|
|
136
|
+
config_paths = [
|
|
137
|
+
Path(path)
|
|
138
|
+
for path in glob.glob(str(dir).rstrip('/') + '/**/*.y*ml', recursive=True)
|
|
139
|
+
]
|
|
140
|
+
debug(f'Found {len(config_paths)} templates in {dir}', sub='template')
|
|
141
|
+
paths.extend(config_paths)
|
|
142
|
+
for path in paths:
|
|
143
|
+
config = TemplateLoader(input=path)
|
|
144
|
+
debug(f'Loaded template from {path}', sub='template')
|
|
145
|
+
results.append(config)
|
|
146
|
+
return results
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
TEMPLATES = find_templates()
|
secator/utils.py
CHANGED
|
@@ -164,7 +164,8 @@ def discover_internal_tasks():
|
|
|
164
164
|
# Sort task_classes by category
|
|
165
165
|
task_classes = sorted(
|
|
166
166
|
task_classes,
|
|
167
|
-
key=lambda x: (get_command_category(x), x.__name__)
|
|
167
|
+
# key=lambda x: (get_command_category(x), x.__name__)
|
|
168
|
+
key=lambda x: x.__name__)
|
|
168
169
|
|
|
169
170
|
return task_classes
|
|
170
171
|
|
|
@@ -262,9 +263,9 @@ def get_command_category(command):
|
|
|
262
263
|
Returns:
|
|
263
264
|
str: Command category.
|
|
264
265
|
"""
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return
|
|
266
|
+
if not command.tags:
|
|
267
|
+
return 'misc'
|
|
268
|
+
return '/'.join(command.tags)
|
|
268
269
|
|
|
269
270
|
|
|
270
271
|
def merge_opts(*options):
|
|
@@ -309,6 +310,8 @@ def pluralize(word):
|
|
|
309
310
|
"""
|
|
310
311
|
if word.endswith('y'):
|
|
311
312
|
return word.rstrip('y') + 'ies'
|
|
313
|
+
elif word.endswith('s'):
|
|
314
|
+
return word + 'es'
|
|
312
315
|
return f'{word}s'
|
|
313
316
|
|
|
314
317
|
|
|
@@ -373,11 +376,15 @@ def rich_to_ansi(text):
|
|
|
373
376
|
Returns:
|
|
374
377
|
str: Converted text (ANSI).
|
|
375
378
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
tmp_console.
|
|
380
|
-
|
|
379
|
+
try:
|
|
380
|
+
from rich.console import Console
|
|
381
|
+
tmp_console = Console(file=None, highlight=False)
|
|
382
|
+
with tmp_console.capture() as capture:
|
|
383
|
+
tmp_console.print(text, end='', soft_wrap=True)
|
|
384
|
+
return capture.get()
|
|
385
|
+
except Exception:
|
|
386
|
+
console.print(f'[bold red]Could not convert rich text to ansi: {text}[/]', highlight=False, markup=False)
|
|
387
|
+
return text
|
|
381
388
|
|
|
382
389
|
|
|
383
390
|
def rich_escape(obj):
|
|
@@ -414,15 +421,16 @@ def format_object(obj, obj_breaklines=False):
|
|
|
414
421
|
|
|
415
422
|
def debug(msg, sub='', id='', obj=None, lazy=None, obj_after=True, obj_breaklines=False, verbose=False):
|
|
416
423
|
"""Print debug log if DEBUG >= level."""
|
|
417
|
-
if not DEBUG_COMPONENT
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if sub:
|
|
421
|
-
if verbose and sub not in DEBUG_COMPONENT:
|
|
422
|
-
sub = f'debug.{sub}'
|
|
423
|
-
if not any(sub.startswith(s) for s in DEBUG_COMPONENT):
|
|
424
|
+
if not DEBUG_COMPONENT == ['all']:
|
|
425
|
+
if not DEBUG_COMPONENT or DEBUG_COMPONENT == [""]:
|
|
424
426
|
return
|
|
425
427
|
|
|
428
|
+
if sub:
|
|
429
|
+
if verbose and sub not in DEBUG_COMPONENT:
|
|
430
|
+
sub = f'debug.{sub}'
|
|
431
|
+
if not any(sub.startswith(s) for s in DEBUG_COMPONENT):
|
|
432
|
+
return
|
|
433
|
+
|
|
426
434
|
if lazy:
|
|
427
435
|
msg = lazy(msg)
|
|
428
436
|
|
|
@@ -459,6 +467,10 @@ def escape_mongodb_url(url):
|
|
|
459
467
|
return url
|
|
460
468
|
|
|
461
469
|
|
|
470
|
+
def caml_to_snake(s):
|
|
471
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '_', s).lower()
|
|
472
|
+
|
|
473
|
+
|
|
462
474
|
def print_version():
|
|
463
475
|
"""Print secator version information."""
|
|
464
476
|
from secator.installer import get_version_info
|
|
@@ -773,8 +785,9 @@ def process_wordlist(val):
|
|
|
773
785
|
val = default_wordlist
|
|
774
786
|
template_wordlist = getattr(CONFIG.wordlists.templates, val)
|
|
775
787
|
if template_wordlist:
|
|
776
|
-
|
|
777
|
-
|
|
788
|
+
val = template_wordlist
|
|
789
|
+
|
|
790
|
+
if Path(val).exists():
|
|
778
791
|
return val
|
|
779
792
|
else:
|
|
780
793
|
return download_file(
|