secator 0.15.0__py3-none-any.whl → 0.16.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of secator might be problematic. Click here for more details.
- secator/celery.py +40 -24
- secator/celery_signals.py +71 -68
- secator/celery_utils.py +43 -27
- secator/cli.py +520 -280
- secator/cli_helper.py +394 -0
- secator/click.py +87 -0
- secator/config.py +67 -39
- secator/configs/profiles/http_headless.yaml +6 -0
- secator/configs/profiles/http_record.yaml +6 -0
- secator/configs/profiles/tor.yaml +1 -1
- secator/configs/scans/domain.yaml +4 -2
- secator/configs/scans/host.yaml +1 -1
- secator/configs/scans/network.yaml +1 -4
- secator/configs/scans/subdomain.yaml +13 -1
- secator/configs/scans/url.yaml +1 -2
- secator/configs/workflows/cidr_recon.yaml +6 -4
- secator/configs/workflows/code_scan.yaml +1 -1
- secator/configs/workflows/host_recon.yaml +29 -3
- secator/configs/workflows/subdomain_recon.yaml +67 -16
- secator/configs/workflows/url_crawl.yaml +44 -15
- secator/configs/workflows/url_dirsearch.yaml +4 -4
- secator/configs/workflows/url_fuzz.yaml +25 -17
- secator/configs/workflows/url_params_fuzz.yaml +7 -0
- secator/configs/workflows/url_vuln.yaml +33 -8
- secator/configs/workflows/user_hunt.yaml +2 -1
- secator/configs/workflows/wordpress.yaml +5 -3
- secator/cve.py +718 -0
- secator/decorators.py +0 -454
- secator/definitions.py +49 -30
- secator/exporters/_base.py +2 -2
- secator/exporters/console.py +2 -2
- secator/exporters/table.py +4 -3
- secator/exporters/txt.py +1 -1
- secator/hooks/mongodb.py +2 -4
- secator/installer.py +77 -49
- secator/loader.py +116 -0
- secator/output_types/_base.py +3 -0
- secator/output_types/certificate.py +63 -63
- secator/output_types/error.py +4 -5
- secator/output_types/info.py +2 -2
- secator/output_types/ip.py +3 -1
- secator/output_types/progress.py +5 -9
- secator/output_types/state.py +17 -17
- secator/output_types/tag.py +3 -0
- secator/output_types/target.py +10 -2
- secator/output_types/url.py +19 -7
- secator/output_types/vulnerability.py +11 -7
- secator/output_types/warning.py +2 -2
- secator/report.py +27 -15
- secator/rich.py +18 -10
- secator/runners/_base.py +447 -234
- secator/runners/_helpers.py +133 -24
- secator/runners/command.py +182 -102
- secator/runners/scan.py +33 -5
- secator/runners/task.py +13 -7
- secator/runners/workflow.py +105 -72
- secator/scans/__init__.py +2 -2
- secator/serializers/dataclass.py +20 -20
- secator/tasks/__init__.py +4 -4
- secator/tasks/_categories.py +39 -27
- secator/tasks/arjun.py +9 -5
- secator/tasks/bbot.py +53 -21
- secator/tasks/bup.py +19 -5
- secator/tasks/cariddi.py +24 -3
- secator/tasks/dalfox.py +26 -7
- secator/tasks/dirsearch.py +10 -4
- secator/tasks/dnsx.py +70 -25
- secator/tasks/feroxbuster.py +11 -3
- secator/tasks/ffuf.py +42 -6
- secator/tasks/fping.py +20 -8
- secator/tasks/gau.py +3 -1
- secator/tasks/gf.py +5 -4
- secator/tasks/gitleaks.py +2 -2
- secator/tasks/gospider.py +7 -1
- secator/tasks/grype.py +5 -4
- secator/tasks/h8mail.py +2 -1
- secator/tasks/httpx.py +18 -5
- secator/tasks/katana.py +35 -15
- secator/tasks/maigret.py +4 -4
- secator/tasks/mapcidr.py +3 -3
- secator/tasks/msfconsole.py +4 -4
- secator/tasks/naabu.py +5 -4
- secator/tasks/nmap.py +12 -14
- secator/tasks/nuclei.py +3 -3
- secator/tasks/searchsploit.py +6 -5
- secator/tasks/subfinder.py +2 -2
- secator/tasks/testssl.py +264 -263
- secator/tasks/trivy.py +5 -5
- secator/tasks/wafw00f.py +21 -3
- secator/tasks/wpprobe.py +90 -83
- secator/tasks/wpscan.py +6 -5
- secator/template.py +218 -104
- secator/thread.py +15 -15
- secator/tree.py +196 -0
- secator/utils.py +131 -123
- secator/utils_test.py +60 -19
- secator/workflows/__init__.py +2 -2
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/METADATA +37 -36
- secator-0.16.0.dist-info/RECORD +132 -0
- secator/configs/profiles/default.yaml +0 -8
- secator/configs/workflows/url_nuclei.yaml +0 -11
- secator/tasks/dnsxbrute.py +0 -42
- secator-0.15.0.dist-info/RECORD +0 -128
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
- {secator-0.15.0.dist-info → secator-0.16.0.dist-info}/licenses/LICENSE +0 -0
secator/tasks/wpprobe.py
CHANGED
|
@@ -1,96 +1,103 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
1
3
|
import click
|
|
2
4
|
import yaml
|
|
3
5
|
|
|
4
6
|
from secator.decorators import task
|
|
5
7
|
from secator.runners import Command
|
|
6
8
|
from secator.definitions import OUTPUT_PATH, THREADS, URL
|
|
7
|
-
from secator.output_types import Vulnerability, Tag, Info, Warning
|
|
9
|
+
from secator.output_types import Vulnerability, Tag, Info, Warning, Error
|
|
8
10
|
from secator.tasks._categories import OPTS
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
@task()
|
|
12
14
|
class wpprobe(Command):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
15
|
+
"""Fast wordpress plugin enumeration tool."""
|
|
16
|
+
cmd = 'wpprobe'
|
|
17
|
+
input_types = [URL]
|
|
18
|
+
output_types = [Vulnerability, Tag]
|
|
19
|
+
tags = ['vuln', 'scan', 'wordpress']
|
|
20
|
+
file_flag = '-f'
|
|
21
|
+
input_flag = '-u'
|
|
22
|
+
opt_prefix = '-'
|
|
23
|
+
opts = {
|
|
24
|
+
'mode': {'type': click.Choice(['scan', 'update', 'update-db']), 'default': 'scan', 'help': 'WPProbe mode', 'required': True, 'internal': True}, # noqa: E501
|
|
25
|
+
'output_path': {'type': str, 'default': None, 'help': 'Output JSON file path', 'internal': True, 'display': False}, # noqa: E501
|
|
26
|
+
}
|
|
27
|
+
meta_opts = {
|
|
28
|
+
THREADS: OPTS[THREADS]
|
|
29
|
+
}
|
|
30
|
+
opt_key_map = {
|
|
31
|
+
THREADS: 't'
|
|
32
|
+
}
|
|
33
|
+
install_version = 'v0.5.6'
|
|
34
|
+
install_cmd = 'go install github.com/Chocapikk/wpprobe@[install_version]'
|
|
35
|
+
install_github_handle = 'Chocapikk/wpprobe'
|
|
36
|
+
install_post = {
|
|
37
|
+
'*': 'wpprobe update && wpprobe update-db'
|
|
38
|
+
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
40
|
+
@staticmethod
|
|
41
|
+
def on_cmd(self):
|
|
42
|
+
mode = self.get_opt_value('mode')
|
|
43
|
+
if mode == 'update' or mode == 'update-db':
|
|
44
|
+
self.cmd = f'{wpprobe.cmd} {mode}'
|
|
45
|
+
return
|
|
46
|
+
self.cmd = re.sub(wpprobe.cmd, f'{wpprobe.cmd} {mode}', self.cmd, 1)
|
|
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 += f' -o {self.output_path}'
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
53
|
+
@staticmethod
|
|
54
|
+
def on_cmd_done(self):
|
|
55
|
+
if not self.get_opt_value('mode') == 'scan':
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if not os.path.exists(self.output_path):
|
|
59
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
63
|
+
with open(self.output_path, 'r') as f:
|
|
64
|
+
results = yaml.safe_load(f.read())
|
|
65
|
+
if not results or 'url' not in results:
|
|
66
|
+
yield Warning(message='No results found !')
|
|
67
|
+
return
|
|
68
|
+
url = results['url']
|
|
69
|
+
for plugin_name, plugin_data in results['plugins'].items():
|
|
70
|
+
for plugin_data_version in plugin_data:
|
|
71
|
+
plugin_version = plugin_data_version['version']
|
|
72
|
+
yield Tag(
|
|
73
|
+
name=f'Wordpress plugin - {plugin_name} {plugin_version}',
|
|
74
|
+
match=url,
|
|
75
|
+
extra_data={
|
|
76
|
+
'name': plugin_name,
|
|
77
|
+
'version': plugin_version
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
severities = plugin_data_version.get('severities', {})
|
|
81
|
+
for severity, severity_data in severities.items():
|
|
82
|
+
if severity == 'None':
|
|
83
|
+
severity = 'unknown'
|
|
84
|
+
for item in severity_data:
|
|
85
|
+
for vuln in item['vulnerabilities']:
|
|
86
|
+
auth_type = item.get('auth_type')
|
|
87
|
+
extra_data = {
|
|
88
|
+
'plugin_name': plugin_name,
|
|
89
|
+
'plugin_version': plugin_version,
|
|
90
|
+
}
|
|
91
|
+
if auth_type:
|
|
92
|
+
extra_data['auth_type'] = auth_type
|
|
93
|
+
yield Vulnerability(
|
|
94
|
+
name=vuln['title'],
|
|
95
|
+
id=vuln['cve'],
|
|
96
|
+
severity=severity,
|
|
97
|
+
cvss_score=vuln['cvss_score'],
|
|
98
|
+
tags=[plugin_name],
|
|
99
|
+
reference=vuln['cve_link'],
|
|
100
|
+
extra_data=extra_data,
|
|
101
|
+
matched_at=url,
|
|
102
|
+
confidence='high'
|
|
103
|
+
)
|
secator/tasks/wpscan.py
CHANGED
|
@@ -17,10 +17,11 @@ from secator.tasks._categories import VulnHttp
|
|
|
17
17
|
class wpscan(VulnHttp):
|
|
18
18
|
"""Wordpress security scanner."""
|
|
19
19
|
cmd = 'wpscan --force --verbose'
|
|
20
|
+
input_types = [URL]
|
|
21
|
+
output_types = [Vulnerability, Tag]
|
|
20
22
|
tags = ['vuln', 'scan', 'wordpress']
|
|
21
|
-
file_flag = None
|
|
22
23
|
input_flag = '--url'
|
|
23
|
-
|
|
24
|
+
input_chunk_size = 1
|
|
24
25
|
json_flag = '-f json'
|
|
25
26
|
opt_prefix = '--'
|
|
26
27
|
opts = {
|
|
@@ -69,14 +70,14 @@ class wpscan(VulnHttp):
|
|
|
69
70
|
PROVIDER: 'wpscan',
|
|
70
71
|
},
|
|
71
72
|
}
|
|
72
|
-
output_types = [Vulnerability, Tag]
|
|
73
73
|
install_pre = {
|
|
74
74
|
'apt': ['make', 'kali:libcurl4t64', 'libffi-dev'],
|
|
75
75
|
'pacman': ['make', 'ruby-erb'],
|
|
76
76
|
'*': ['make']
|
|
77
77
|
}
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
install_github_handle = 'wpscanteam/wpscan'
|
|
79
|
+
install_version = 'v3.8.28'
|
|
80
|
+
install_cmd = f'gem install wpscan -v [install_version_strip] --user-install -n {CONFIG.dirs.bin}'
|
|
80
81
|
install_post = {
|
|
81
82
|
'kali': (
|
|
82
83
|
f'gem uninstall nokogiri --user-install -n {CONFIG.dirs.bin} --force --executables && '
|
secator/template.py
CHANGED
|
@@ -1,30 +1,28 @@
|
|
|
1
|
-
import
|
|
1
|
+
import yaml
|
|
2
2
|
|
|
3
3
|
from collections import OrderedDict
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
import yaml
|
|
7
4
|
from dotmap import DotMap
|
|
5
|
+
from pathlib import Path
|
|
8
6
|
|
|
9
|
-
from secator.config import CONFIG, CONFIGS_FOLDER
|
|
10
|
-
from secator.rich import console
|
|
11
|
-
from secator.utils import convert_functions_to_strings, debug
|
|
12
7
|
from secator.output_types import Error
|
|
13
|
-
|
|
14
|
-
TEMPLATES = []
|
|
8
|
+
from secator.rich import console
|
|
15
9
|
|
|
16
10
|
|
|
17
11
|
class TemplateLoader(DotMap):
|
|
18
12
|
|
|
19
13
|
def __init__(self, input={}, name=None, **kwargs):
|
|
20
14
|
if name:
|
|
21
|
-
|
|
15
|
+
split = name.split('/')
|
|
16
|
+
if len(split) != 2:
|
|
22
17
|
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
18
|
return
|
|
24
|
-
_type,
|
|
25
|
-
|
|
19
|
+
_type, _name = tuple(split)
|
|
20
|
+
if _type.endswith('s'):
|
|
21
|
+
_type = _type[:-1]
|
|
22
|
+
from secator.loader import find_templates
|
|
23
|
+
config = next((p for p in find_templates() if p['type'] == _type and p['name'] == _name), None)
|
|
26
24
|
if not config:
|
|
27
|
-
console.print(Error(message=f'Template {
|
|
25
|
+
console.print(Error(message=f'Template {_type}/{_name} not found in loaded templates'))
|
|
28
26
|
config = {}
|
|
29
27
|
elif isinstance(input, dict):
|
|
30
28
|
config = input
|
|
@@ -35,9 +33,6 @@ class TemplateLoader(DotMap):
|
|
|
35
33
|
config = self._load(input)
|
|
36
34
|
super().__init__(config, **kwargs)
|
|
37
35
|
|
|
38
|
-
def add_to_templates(self):
|
|
39
|
-
TEMPLATES.append(self)
|
|
40
|
-
|
|
41
36
|
def _load_from_path(self, path):
|
|
42
37
|
if not path.exists():
|
|
43
38
|
console.print(Error(message=f'Config path {path} does not exists'))
|
|
@@ -48,16 +43,6 @@ class TemplateLoader(DotMap):
|
|
|
48
43
|
def _load(self, input):
|
|
49
44
|
return yaml.load(input, Loader=yaml.Loader)
|
|
50
45
|
|
|
51
|
-
@property
|
|
52
|
-
def supported_opts(self):
|
|
53
|
-
"""Property to access supported options easily."""
|
|
54
|
-
return self._collect_supported_opts()
|
|
55
|
-
|
|
56
|
-
@property
|
|
57
|
-
def flat_tasks(self):
|
|
58
|
-
"""Property to access tasks easily."""
|
|
59
|
-
return self._extract_tasks()
|
|
60
|
-
|
|
61
46
|
def print(self):
|
|
62
47
|
"""Print config as highlighted yaml."""
|
|
63
48
|
config = self.toDict()
|
|
@@ -69,81 +54,210 @@ class TemplateLoader(DotMap):
|
|
|
69
54
|
yaml_highlight = Syntax(yaml_str, 'yaml', line_numbers=True)
|
|
70
55
|
console.print(yaml_highlight)
|
|
71
56
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
tasks
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
57
|
+
|
|
58
|
+
def get_short_id(id_str, config_name):
|
|
59
|
+
"""Remove config name prefix from ID string if present.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
id_str: The ID string to process
|
|
63
|
+
config_name: The config name prefix to remove
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
str: ID string with prefix removed, or original string if no prefix found
|
|
67
|
+
"""
|
|
68
|
+
if id_str.startswith(config_name):
|
|
69
|
+
return id_str.replace(config_name + '.', '')
|
|
70
|
+
return id_str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_config_options(config, exec_opts=None, output_opts=None, type_mapping=None):
|
|
74
|
+
"""Extract and normalize command-line options from configuration.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
config: Configuration object (task, workflow, or scan)
|
|
78
|
+
exec_opts: Execution options dictionary (optional)
|
|
79
|
+
output_opts: Output options dictionary (optional)
|
|
80
|
+
type_mapping: Type mapping for option types (optional)
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
OrderedDict: Normalized options with metadata
|
|
84
|
+
"""
|
|
85
|
+
from secator.tree import build_runner_tree, walk_runner_tree, get_flat_node_list
|
|
86
|
+
from secator.utils import debug
|
|
87
|
+
from secator.runners.task import Task
|
|
88
|
+
|
|
89
|
+
# Task config created on-the-fly
|
|
90
|
+
if config.type == 'task':
|
|
91
|
+
config = TemplateLoader({
|
|
92
|
+
'name': config.name,
|
|
93
|
+
'type': 'workflow',
|
|
94
|
+
'tasks': {config.name: {}}
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
# Get main info
|
|
98
|
+
tree = build_runner_tree(config)
|
|
99
|
+
nodes = get_flat_node_list(tree)
|
|
100
|
+
exec_opts = exec_opts or {}
|
|
101
|
+
output_opts = output_opts or {}
|
|
102
|
+
type_mapping = type_mapping or {}
|
|
103
|
+
all_opts = OrderedDict({})
|
|
104
|
+
|
|
105
|
+
# Log current config and tree
|
|
106
|
+
debug(f'[magenta]{config.name}[/]', sub=f'cli.{config.name}')
|
|
107
|
+
debug(f'{tree.render_tree()}', sub=f'cli.{config.name}')
|
|
108
|
+
|
|
109
|
+
# Process global execution options
|
|
110
|
+
for opt in exec_opts:
|
|
111
|
+
opt_conf = exec_opts[opt].copy()
|
|
112
|
+
opt_conf['prefix'] = 'Execution'
|
|
113
|
+
all_opts[opt] = opt_conf
|
|
114
|
+
|
|
115
|
+
# Process global output options
|
|
116
|
+
for opt in output_opts:
|
|
117
|
+
opt_conf = output_opts[opt].copy()
|
|
118
|
+
opt_conf['prefix'] = 'Output'
|
|
119
|
+
all_opts[opt] = opt_conf
|
|
120
|
+
|
|
121
|
+
# Process config options
|
|
122
|
+
# a.k.a:
|
|
123
|
+
# - default YAML config options, defined in default_options: key in the runner YAML config
|
|
124
|
+
# - new options defined in options: key in the runner YAML config
|
|
125
|
+
config_opts_defaults = config.default_options.toDict()
|
|
126
|
+
config_opts = config.options.toDict()
|
|
127
|
+
for k, v in config_opts.items():
|
|
128
|
+
all_opts[k] = v
|
|
129
|
+
all_opts[k]['prefix'] = f'{config.type}'
|
|
130
|
+
|
|
131
|
+
def find_same_opts(node, nodes, opt_name, check_class_opts=False):
|
|
132
|
+
"""Find options with the same name that are defined in other nodes of the same type."""
|
|
133
|
+
same_opts = []
|
|
134
|
+
for _ in nodes:
|
|
135
|
+
if _.id == node.id or _.type != node.type:
|
|
136
|
+
continue
|
|
137
|
+
node_task = None
|
|
138
|
+
if check_class_opts:
|
|
139
|
+
node_task = Task.get_task_class(_.name)
|
|
140
|
+
if opt_name not in node_task.opts:
|
|
141
|
+
continue
|
|
142
|
+
opts_value = node_task.opts[opt_name]
|
|
143
|
+
else:
|
|
144
|
+
if opt_name not in _.opts:
|
|
145
|
+
continue
|
|
146
|
+
opts_value = _.opts[opt_name]
|
|
147
|
+
name_str = 'nodes' if not check_class_opts else 'tasks'
|
|
148
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] -> [bold green]{opt_name}[/] found in other {name_str} [bold blue]{_.id}[/]', sub=f'cli.{config.name}.same', verbose=True) # noqa: E501
|
|
149
|
+
same_opts.append({
|
|
150
|
+
'id': _.id,
|
|
151
|
+
'task_name': node_task.__name__ if node_task else None,
|
|
152
|
+
'name': _.name,
|
|
153
|
+
'value': opts_value,
|
|
154
|
+
})
|
|
155
|
+
if same_opts:
|
|
156
|
+
other_tasks = ", ".join([f'[bold yellow]{_["id"]}[/]' for _ in same_opts])
|
|
157
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] -> [bold green]{opt_name}[/] found in {len(same_opts)} other {name_str}: {other_tasks}', sub=f'cli.{config.name}.same', verbose=True) # noqa: E501
|
|
158
|
+
return same_opts
|
|
159
|
+
|
|
160
|
+
def process_node(node):
|
|
161
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] ({node.type})', sub=f'cli.{config.name}')
|
|
162
|
+
|
|
163
|
+
if node.type not in ['task', 'workflow']:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Process workflow options
|
|
167
|
+
# a.k.a the new options defined in options: key in the workflow YAML config;
|
|
168
|
+
if node.type == 'workflow':
|
|
169
|
+
for k, v in node.opts.items():
|
|
170
|
+
same_opts = find_same_opts(node, nodes, k)
|
|
171
|
+
conf = v.copy()
|
|
172
|
+
opt_name = k
|
|
173
|
+
conf['prefix'] = f'{node.type.capitalize()} {node.name}'
|
|
174
|
+
if len(same_opts) > 0: # opt name conflict, change opt name
|
|
175
|
+
opt_name = f'{node.name}.{k}'
|
|
176
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] -> [bold green]{k}[/] renamed to [bold green]{opt_name}[/] [dim red](duplicated)[/]', sub=f'cli.{config.name}') # noqa: E501
|
|
177
|
+
all_opts[opt_name] = conf
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
# Process task options
|
|
181
|
+
# a.k.a task options defined in their respective task classes
|
|
182
|
+
cls = Task.get_task_class(node.name)
|
|
183
|
+
task_opts = cls.opts.copy()
|
|
184
|
+
task_opts_meta = cls.meta_opts.copy()
|
|
185
|
+
task_opts_all = {**task_opts, **task_opts_meta}
|
|
186
|
+
node_opts = node.opts or {}
|
|
187
|
+
ancestor_opts_defaults = node.ancestor.default_opts or {}
|
|
188
|
+
node_id_str = get_short_id(node.id, config.name)
|
|
189
|
+
|
|
190
|
+
for k, v in task_opts_all.items():
|
|
191
|
+
conf = v.copy()
|
|
192
|
+
conf['prefix'] = f'Task {node.name}'
|
|
193
|
+
default_from_config = node_opts.get(k) or ancestor_opts_defaults.get(k) or config_opts_defaults.get(k)
|
|
194
|
+
opt_name = k
|
|
195
|
+
same_opts = find_same_opts(node, nodes, k)
|
|
196
|
+
|
|
197
|
+
# Found a default in YAML config, either in task options, or workflow options, or config options
|
|
198
|
+
if default_from_config:
|
|
199
|
+
conf['required'] = False
|
|
200
|
+
conf['default'] = default_from_config
|
|
201
|
+
conf['default_from'] = node_id_str
|
|
202
|
+
if node_opts.get(k):
|
|
203
|
+
conf['default_from'] = node_id_str
|
|
204
|
+
conf['prefix'] = 'Config'
|
|
205
|
+
elif ancestor_opts_defaults.get(k):
|
|
206
|
+
conf['default_from'] = get_short_id(node.ancestor.id, config.name)
|
|
207
|
+
conf['prefix'] = f'{node.ancestor.type.capitalize()} {node.ancestor.name}'
|
|
208
|
+
elif config_opts_defaults.get(k):
|
|
209
|
+
conf['default_from'] = config.name
|
|
210
|
+
conf['prefix'] = 'Config'
|
|
211
|
+
mapped_value = cls.opt_value_map.get(opt_name)
|
|
212
|
+
if mapped_value:
|
|
213
|
+
if callable(mapped_value):
|
|
214
|
+
default_from_config = mapped_value(default_from_config)
|
|
215
|
+
else:
|
|
216
|
+
default_from_config = mapped_value
|
|
217
|
+
conf['default'] = default_from_config
|
|
218
|
+
if len(same_opts) > 0: # change opt name to avoid conflict
|
|
219
|
+
conf['prefix'] = 'Config'
|
|
220
|
+
opt_name = f'{conf["default_from"]}.{k}'
|
|
221
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] -> [bold green]{k}[/] renamed to [bold green]{opt_name}[/] [dim red](default set in config)[/]', sub=f'cli.{config.name}') # noqa: E501
|
|
222
|
+
|
|
223
|
+
# Standard meta options like rate_limit, delay, proxy, etc...
|
|
224
|
+
elif k in task_opts_meta:
|
|
225
|
+
conf['prefix'] = 'Meta'
|
|
226
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] -> [bold green]{k}[/] changed prefix to [bold cyan]Meta[/]', sub=f'cli.{config.name}') # noqa: E501
|
|
227
|
+
|
|
228
|
+
# Task-specific options
|
|
229
|
+
elif k in task_opts:
|
|
230
|
+
same_opts = find_same_opts(node, nodes, k, check_class_opts=True)
|
|
231
|
+
if len(same_opts) > 0:
|
|
232
|
+
applies_to = set([node.name] + [_['name'] for _ in same_opts])
|
|
233
|
+
conf['applies_to'] = applies_to
|
|
234
|
+
conf['prefix'] = 'Shared task'
|
|
235
|
+
debug(f'[bold]{config.name}[/] -> [bold blue]{node.id}[/] -> [bold green]{k}[/] changed prefix to [bold cyan]Common[/] [dim red](duplicated {len(same_opts)} times)[/]', sub=f'cli.{config.name}') # noqa: E501
|
|
236
|
+
else:
|
|
237
|
+
raise ValueError(f'Unknown option {k} for task {node.id}')
|
|
238
|
+
all_opts[opt_name] = conf
|
|
239
|
+
|
|
240
|
+
walk_runner_tree(tree, process_node)
|
|
241
|
+
|
|
242
|
+
# Normalize all options
|
|
243
|
+
debug('[bold yellow3]All opts processed. Showing defaults:[/]', sub=f'cli.{config.name}')
|
|
244
|
+
normalized_opts = OrderedDict({})
|
|
245
|
+
for k, v in all_opts.items():
|
|
246
|
+
v['reverse'] = False
|
|
247
|
+
v['show_default'] = True
|
|
248
|
+
default_from = v.get('default_from')
|
|
249
|
+
default = v.get('default', False)
|
|
250
|
+
if isinstance(default, bool) and default is True:
|
|
251
|
+
v['reverse'] = True
|
|
252
|
+
if type_mapping and 'type' in v:
|
|
253
|
+
v['type'] = type_mapping.get(v['type'], str)
|
|
254
|
+
short = v.get('short')
|
|
255
|
+
k = k.replace('.', '-').replace('_', '-').replace('/', '-')
|
|
256
|
+
from_str = default_from.replace('.', '-').replace('_', '-').replace('/', '-') if default_from else None
|
|
257
|
+
if not default_from or from_str not in k:
|
|
258
|
+
v['short'] = short if short else None
|
|
259
|
+
else:
|
|
260
|
+
v['short'] = f'{from_str}-{short}' if short else None
|
|
261
|
+
debug(f'\t[bold]{k}[/] -> [bold green]{v.get("default", "N/A")}[/] [dim red](default from {v.get("default_from", "N/A")})[/]', sub=f'cli.{config.name}') # noqa: E501
|
|
262
|
+
normalized_opts[k] = v
|
|
263
|
+
return normalized_opts
|
secator/thread.py
CHANGED
|
@@ -4,21 +4,21 @@ from secator.output_types import Error
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class Thread(threading.Thread):
|
|
7
|
-
|
|
7
|
+
"""A thread that returns errors in their join() method as secator.output_types.Error."""
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
def __init__(self, *args, **kwargs):
|
|
10
|
+
super().__init__(*args, **kwargs)
|
|
11
|
+
self.error = None
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
def run(self):
|
|
14
|
+
try:
|
|
15
|
+
if hasattr(self, '_target'):
|
|
16
|
+
self._target(*self._args, **self._kwargs)
|
|
17
|
+
except Exception as e:
|
|
18
|
+
self.error = Error.from_exception(e)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
def join(self, *args, **kwargs):
|
|
21
|
+
super().join(*args, **kwargs)
|
|
22
|
+
if self.error:
|
|
23
|
+
return self.error
|
|
24
|
+
return None
|