secator 0.15.1__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 +446 -233
- 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 +3 -3
- 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 +2 -2
- secator/tasks/nmap.py +12 -14
- secator/tasks/nuclei.py +3 -3
- secator/tasks/searchsploit.py +4 -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.1.dist-info → secator-0.16.0.dist-info}/METADATA +36 -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.1.dist-info/RECORD +0 -128
- {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
- {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
- {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/licenses/LICENSE +0 -0
secator/runners/_base.py
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
|
+
import gc
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import sys
|
|
5
|
+
import textwrap
|
|
4
6
|
import uuid
|
|
7
|
+
|
|
5
8
|
from datetime import datetime
|
|
6
9
|
from pathlib import Path
|
|
7
10
|
from time import time
|
|
8
11
|
|
|
12
|
+
from dotmap import DotMap
|
|
9
13
|
import humanize
|
|
10
14
|
|
|
11
|
-
from secator.definitions import ADDONS_ENABLED
|
|
15
|
+
from secator.definitions import ADDONS_ENABLED, STATE_COLORS
|
|
12
16
|
from secator.celery_utils import CeleryData
|
|
13
17
|
from secator.config import CONFIG
|
|
14
18
|
from secator.output_types import FINDING_TYPES, OUTPUT_TYPES, OutputType, Progress, Info, Warning, Error, Target, State
|
|
15
19
|
from secator.report import Report
|
|
16
20
|
from secator.rich import console, console_stdout
|
|
17
21
|
from secator.runners._helpers import (get_task_folder_id, run_extractors)
|
|
18
|
-
from secator.utils import (debug, import_dynamic, rich_to_ansi, should_update)
|
|
22
|
+
from secator.utils import (debug, import_dynamic, rich_to_ansi, should_update, autodetect_type)
|
|
23
|
+
from secator.tree import build_runner_tree
|
|
24
|
+
from secator.loader import get_configs_by_type
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
logger = logging.getLogger(__name__)
|
|
21
28
|
|
|
@@ -36,6 +43,16 @@ VALIDATORS = [
|
|
|
36
43
|
]
|
|
37
44
|
|
|
38
45
|
|
|
46
|
+
def format_runner_name(runner):
|
|
47
|
+
"""Format runner name."""
|
|
48
|
+
colors = {
|
|
49
|
+
'task': 'bold gold3',
|
|
50
|
+
'workflow': 'bold dark_orange3',
|
|
51
|
+
'scan': 'bold red',
|
|
52
|
+
}
|
|
53
|
+
return f'[{colors[runner.config.type]}]{runner.unique_name}[/]'
|
|
54
|
+
|
|
55
|
+
|
|
39
56
|
class Runner:
|
|
40
57
|
"""Runner class.
|
|
41
58
|
|
|
@@ -61,118 +78,171 @@ class Runner:
|
|
|
61
78
|
# Default exporters
|
|
62
79
|
default_exporters = []
|
|
63
80
|
|
|
81
|
+
# Profiles
|
|
82
|
+
profiles = []
|
|
83
|
+
|
|
64
84
|
# Run hooks
|
|
65
85
|
enable_hooks = True
|
|
66
86
|
|
|
67
87
|
def __init__(self, config, inputs=[], results=[], run_opts={}, hooks={}, validators={}, context={}):
|
|
68
|
-
self.uuids = []
|
|
69
|
-
self.results = []
|
|
70
|
-
self.output = ''
|
|
71
|
-
|
|
72
88
|
# Runner config
|
|
73
|
-
self.config = config
|
|
89
|
+
self.config = DotMap(config.toDict())
|
|
74
90
|
self.name = run_opts.get('name', config.name)
|
|
75
|
-
self.description = run_opts.get('description', config.description)
|
|
91
|
+
self.description = run_opts.get('description', config.description or '')
|
|
76
92
|
self.workspace_name = context.get('workspace_name', 'default')
|
|
77
93
|
self.run_opts = run_opts.copy()
|
|
78
94
|
self.sync = run_opts.get('sync', True)
|
|
95
|
+
self.context = context
|
|
96
|
+
|
|
97
|
+
# Runner state
|
|
98
|
+
self.uuids = set()
|
|
99
|
+
self.results = []
|
|
100
|
+
self.results_count = 0
|
|
101
|
+
self.threads = []
|
|
102
|
+
self.output = ''
|
|
103
|
+
self.started = False
|
|
79
104
|
self.done = False
|
|
80
105
|
self.start_time = datetime.fromtimestamp(time())
|
|
106
|
+
self.end_time = None
|
|
81
107
|
self.last_updated_db = None
|
|
82
108
|
self.last_updated_celery = None
|
|
83
109
|
self.last_updated_progress = None
|
|
84
|
-
self.end_time = None
|
|
85
|
-
self._hooks = hooks
|
|
86
110
|
self.progress = 0
|
|
87
|
-
self.context = context
|
|
88
|
-
self.delay = run_opts.get('delay', False)
|
|
89
111
|
self.celery_result = None
|
|
90
112
|
self.celery_ids = []
|
|
91
113
|
self.celery_ids_map = {}
|
|
92
|
-
self.
|
|
93
|
-
self.
|
|
94
|
-
self.
|
|
95
|
-
self.quiet = self.run_opts.get('quiet', False)
|
|
96
|
-
self.started = False
|
|
97
|
-
self.enable_reports = self.run_opts.get('enable_reports', not self.sync)
|
|
98
|
-
self._reports_folder = self.run_opts.get('reports_folder', None)
|
|
114
|
+
self.revoked = False
|
|
115
|
+
self.results_buffer = []
|
|
116
|
+
self._hooks = hooks
|
|
99
117
|
|
|
100
118
|
# Runner process options
|
|
101
|
-
self.
|
|
119
|
+
self.no_poll = self.run_opts.get('no_poll', False)
|
|
120
|
+
self.no_process = not self.run_opts.get('process', True)
|
|
102
121
|
self.piped_input = self.run_opts.get('piped_input', False)
|
|
103
122
|
self.piped_output = self.run_opts.get('piped_output', False)
|
|
104
|
-
self.enable_duplicate_check = self.run_opts.get('enable_duplicate_check', True)
|
|
105
123
|
self.dry_run = self.run_opts.get('dry_run', False)
|
|
124
|
+
self.has_parent = self.run_opts.get('has_parent', False)
|
|
125
|
+
self.has_children = self.run_opts.get('has_children', False)
|
|
126
|
+
self.caller = self.run_opts.get('caller', None)
|
|
127
|
+
self.quiet = self.run_opts.get('quiet', False)
|
|
128
|
+
self._reports_folder = self.run_opts.get('reports_folder', None)
|
|
129
|
+
self.raise_on_error = self.run_opts.get('raise_on_error', False)
|
|
130
|
+
|
|
131
|
+
# Runner toggles
|
|
132
|
+
self.enable_duplicate_check = self.run_opts.get('enable_duplicate_check', True)
|
|
133
|
+
self.enable_profiles = self.run_opts.get('enable_profiles', True)
|
|
134
|
+
self.enable_reports = self.run_opts.get('enable_reports', not self.sync) and not self.dry_run and not self.no_process and not self.no_poll # noqa: E501
|
|
135
|
+
self.enable_hooks = self.run_opts.get('enable_hooks', True) and not self.dry_run and not self.no_process and not self.no_poll # noqa: E501
|
|
106
136
|
|
|
107
137
|
# Runner print opts
|
|
108
|
-
self.print_item = self.run_opts.get('print_item', False)
|
|
138
|
+
self.print_item = self.run_opts.get('print_item', False) and not self.dry_run
|
|
109
139
|
self.print_line = self.run_opts.get('print_line', False) and not self.quiet
|
|
110
140
|
self.print_remote_info = self.run_opts.get('print_remote_info', False) and not self.piped_input and not self.piped_output # noqa: E501
|
|
141
|
+
self.print_start = self.run_opts.get('print_start', False) and not self.dry_run # noqa: E501
|
|
142
|
+
self.print_end = self.run_opts.get('print_end', False) and not self.dry_run # noqa: E501
|
|
143
|
+
self.print_target = self.run_opts.get('print_target', False) and not self.dry_run and not self.has_parent
|
|
111
144
|
self.print_json = self.run_opts.get('print_json', False)
|
|
112
|
-
self.print_raw = self.run_opts.get('print_raw', False) or self.piped_output
|
|
145
|
+
self.print_raw = self.run_opts.get('print_raw', False) or (self.piped_output and not self.print_json)
|
|
113
146
|
self.print_fmt = self.run_opts.get('fmt', '')
|
|
114
|
-
self.
|
|
115
|
-
self.
|
|
116
|
-
self.print_stat = self.run_opts.get('print_stat', False) and not self.quiet and not self.print_raw
|
|
117
|
-
self.raise_on_error = self.run_opts.get('raise_on_error', False)
|
|
118
|
-
self.print_opts = {k: v for k, v in self.__dict__.items() if k.startswith('print_') if v}
|
|
147
|
+
self.print_stat = self.run_opts.get('print_stat', False)
|
|
148
|
+
self.print_profiles = self.run_opts.get('print_profiles', False)
|
|
119
149
|
|
|
120
150
|
# Chunks
|
|
121
|
-
self.has_parent = self.run_opts.get('has_parent', False)
|
|
122
|
-
self.has_children = self.run_opts.get('has_children', False)
|
|
123
151
|
self.chunk = self.run_opts.get('chunk', None)
|
|
124
152
|
self.chunk_count = self.run_opts.get('chunk_count', None)
|
|
125
153
|
self.unique_name = self.name.replace('/', '_')
|
|
126
154
|
self.unique_name = f'{self.unique_name}_{self.chunk}' if self.chunk else self.unique_name
|
|
127
155
|
|
|
156
|
+
# Opt aliases
|
|
157
|
+
self.opt_aliases = []
|
|
158
|
+
if self.config.node_id:
|
|
159
|
+
self.opt_aliases.append(self.config.node_id.replace('.', '_'))
|
|
160
|
+
if self.config.node_name:
|
|
161
|
+
self.opt_aliases.append(self.config.node_name)
|
|
162
|
+
self.opt_aliases.append(self.unique_name)
|
|
163
|
+
|
|
164
|
+
# Begin initialization
|
|
165
|
+
self.debug(f'begin initialization of {self.unique_name}', sub='init')
|
|
166
|
+
|
|
167
|
+
# Hooks
|
|
168
|
+
self.resolved_hooks = {name: [] for name in HOOKS + getattr(self, 'hooks', [])}
|
|
169
|
+
self.debug('registering hooks', obj=list(self.resolved_hooks.keys()), sub='init')
|
|
170
|
+
self.register_hooks(hooks)
|
|
171
|
+
|
|
172
|
+
# Validators
|
|
173
|
+
self.resolved_validators = {name: [] for name in VALIDATORS + getattr(self, 'validators', [])}
|
|
174
|
+
self.debug('registering validators', obj={'validators': list(self.resolved_validators.keys())}, sub='init')
|
|
175
|
+
self.resolved_validators['validate_input'].append(self._validate_inputs)
|
|
176
|
+
self.register_validators(validators)
|
|
177
|
+
|
|
128
178
|
# Add prior results to runner results
|
|
129
|
-
|
|
179
|
+
self.debug(f'adding {len(results)} prior results to runner', sub='init')
|
|
180
|
+
for result in results:
|
|
181
|
+
self.add_result(result, print=False, output=False, hooks=False, queue=not self.has_parent)
|
|
130
182
|
|
|
131
183
|
# Determine inputs
|
|
184
|
+
self.debug(f'resolving inputs with dynamic opts ({len(self.dynamic_opts)})', obj=self.dynamic_opts, sub='init')
|
|
132
185
|
self.inputs = [inputs] if not isinstance(inputs, list) else inputs
|
|
186
|
+
self.inputs = list(set(self.inputs))
|
|
133
187
|
targets = [Target(name=target) for target in self.inputs]
|
|
134
|
-
|
|
188
|
+
for target in targets:
|
|
189
|
+
self.add_result(target, print=False, output=False)
|
|
135
190
|
|
|
136
|
-
#
|
|
137
|
-
self.
|
|
138
|
-
self.debug('
|
|
139
|
-
self.debug('
|
|
191
|
+
# Run extractors on results and targets
|
|
192
|
+
self._run_extractors(results + targets)
|
|
193
|
+
self.debug(f'inputs ({len(self.inputs)})', obj=self.inputs, sub='init')
|
|
194
|
+
self.debug(f'run opts ({len(self.resolved_opts)})', obj=self.resolved_opts, sub='init')
|
|
195
|
+
self.debug(f'print opts ({len(self.resolved_print_opts)})', obj=self.resolved_print_opts, sub='init')
|
|
140
196
|
|
|
141
197
|
# Load profiles
|
|
142
|
-
profiles_str = run_opts.get('profiles'
|
|
143
|
-
self.
|
|
198
|
+
profiles_str = run_opts.get('profiles') or []
|
|
199
|
+
self.debug('resolving profiles', obj={'profiles': profiles_str}, sub='init')
|
|
200
|
+
self.profiles = self.resolve_profiles(profiles_str)
|
|
144
201
|
|
|
145
202
|
# Determine exporters
|
|
146
203
|
exporters_str = self.run_opts.get('output') or self.default_exporters
|
|
204
|
+
self.debug('resolving exporters', obj={'exporters': exporters_str}, sub='init')
|
|
147
205
|
self.exporters = self.resolve_exporters(exporters_str)
|
|
148
206
|
|
|
149
207
|
# Profiler
|
|
150
|
-
self.
|
|
151
|
-
if self.
|
|
208
|
+
self.enable_pyinstrument = self.run_opts.get('enable_pyinstrument', False) and ADDONS_ENABLED['trace']
|
|
209
|
+
if self.enable_pyinstrument:
|
|
210
|
+
self.debug('enabling profiler', sub='init')
|
|
152
211
|
from pyinstrument import Profiler
|
|
153
212
|
self.profiler = Profiler(async_mode=False, interval=0.0001)
|
|
154
213
|
try:
|
|
155
214
|
self.profiler.start()
|
|
156
215
|
except RuntimeError:
|
|
157
|
-
self.
|
|
216
|
+
self.enable_pyinstrument = False
|
|
158
217
|
pass
|
|
159
218
|
|
|
160
|
-
# Hooks
|
|
161
|
-
self.hooks = {name: [] for name in HOOKS + getattr(self, 'hooks', [])}
|
|
162
|
-
self.register_hooks(hooks)
|
|
163
|
-
|
|
164
|
-
# Validators
|
|
165
|
-
self.validators = {name: [] for name in VALIDATORS + getattr(self, 'validators', [])}
|
|
166
|
-
self.register_validators(validators)
|
|
167
|
-
|
|
168
219
|
# Input post-process
|
|
169
|
-
self.run_hooks('before_init')
|
|
220
|
+
self.run_hooks('before_init', sub='init')
|
|
170
221
|
|
|
171
222
|
# Check if input is valid
|
|
172
|
-
self.inputs_valid = self.run_validators('validate_input', self.inputs)
|
|
223
|
+
self.inputs_valid = self.run_validators('validate_input', self.inputs, sub='init')
|
|
224
|
+
|
|
225
|
+
# Print targets
|
|
226
|
+
if self.print_target:
|
|
227
|
+
pluralize = 'targets' if len(self.self_targets) > 1 else 'target'
|
|
228
|
+
self._print(Info(message=f'Loaded {len(self.self_targets)} {pluralize} for {format_runner_name(self)}:'), rich=True)
|
|
229
|
+
for target in self.self_targets:
|
|
230
|
+
self._print(f' {repr(target)}', rich=True)
|
|
173
231
|
|
|
174
232
|
# Run hooks
|
|
175
|
-
self.run_hooks('on_init')
|
|
233
|
+
self.run_hooks('on_init', sub='init')
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def resolved_opts(self):
|
|
237
|
+
return {k: v for k, v in self.run_opts.items() if v is not None and not k.startswith('print_') and not k.endswith('_')} # noqa: E501
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def resolved_print_opts(self):
|
|
241
|
+
return {k: v for k, v in self.__dict__.items() if k.startswith('print_') if v}
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def dynamic_opts(self):
|
|
245
|
+
return {k: v for k, v in self.run_opts.items() if k.endswith('_')}
|
|
176
246
|
|
|
177
247
|
@property
|
|
178
248
|
def elapsed(self):
|
|
@@ -188,6 +258,10 @@ class Runner:
|
|
|
188
258
|
def targets(self):
|
|
189
259
|
return [r for r in self.results if isinstance(r, Target)]
|
|
190
260
|
|
|
261
|
+
@property
|
|
262
|
+
def self_targets(self):
|
|
263
|
+
return [r for r in self.results if isinstance(r, Target) and r._source.startswith(self.unique_name)]
|
|
264
|
+
|
|
191
265
|
@property
|
|
192
266
|
def infos(self):
|
|
193
267
|
return [r for r in self.results if isinstance(r, Info)]
|
|
@@ -230,6 +304,8 @@ class Runner:
|
|
|
230
304
|
def status(self):
|
|
231
305
|
if not self.started:
|
|
232
306
|
return 'PENDING'
|
|
307
|
+
if self.revoked:
|
|
308
|
+
return 'REVOKED'
|
|
233
309
|
if not self.done:
|
|
234
310
|
return 'RUNNING'
|
|
235
311
|
return 'FAILURE' if len(self.self_errors) > 0 else 'SUCCESS'
|
|
@@ -247,7 +323,7 @@ class Runner:
|
|
|
247
323
|
'chunk_info': f'{self.chunk}/{self.chunk_count}' if self.chunk and self.chunk_count else '',
|
|
248
324
|
'celery_id': self.context['celery_id'],
|
|
249
325
|
'count': self.self_findings_count,
|
|
250
|
-
'descr': self.
|
|
326
|
+
'descr': self.description
|
|
251
327
|
}
|
|
252
328
|
|
|
253
329
|
@property
|
|
@@ -260,13 +336,31 @@ class Runner:
|
|
|
260
336
|
path_inputs = path / '.inputs'
|
|
261
337
|
path_outputs = path / '.outputs'
|
|
262
338
|
if not path.exists():
|
|
263
|
-
self.debug(f'
|
|
339
|
+
self.debug(f'creating reports folder {path}', sub='start')
|
|
264
340
|
path.mkdir(parents=True, exist_ok=True)
|
|
265
341
|
path_inputs.mkdir(exist_ok=True)
|
|
266
342
|
path_outputs.mkdir(exist_ok=True)
|
|
267
343
|
self._reports_folder = path.resolve()
|
|
268
344
|
return self._reports_folder
|
|
269
345
|
|
|
346
|
+
@property
|
|
347
|
+
def id(self):
|
|
348
|
+
"""Get id from context.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
str: Id.
|
|
352
|
+
"""
|
|
353
|
+
return self.context.get('task_id', '') or self.context.get('workflow_id', '') or self.context.get('scan_id', '')
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def ancestor_id(self):
|
|
357
|
+
"""Get ancestor id from context.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
str: Ancestor id.
|
|
361
|
+
"""
|
|
362
|
+
return self.context.get('ancestor_id')
|
|
363
|
+
|
|
270
364
|
def run(self):
|
|
271
365
|
"""Run method.
|
|
272
366
|
|
|
@@ -286,75 +380,143 @@ class Runner:
|
|
|
286
380
|
if self.sync:
|
|
287
381
|
self.mark_started()
|
|
288
382
|
|
|
383
|
+
# Yield results buffer
|
|
384
|
+
yield from self.results_buffer
|
|
385
|
+
self.results_buffer = []
|
|
386
|
+
|
|
289
387
|
# If any errors happened during validation, exit
|
|
290
|
-
if self.
|
|
291
|
-
|
|
292
|
-
self.mark_completed()
|
|
388
|
+
if self.self_errors:
|
|
389
|
+
self._finalize()
|
|
293
390
|
return
|
|
294
391
|
|
|
295
392
|
# Loop and process items
|
|
296
393
|
for item in self.yielder():
|
|
297
394
|
yield from self._process_item(item)
|
|
298
|
-
self.run_hooks('on_interval')
|
|
299
|
-
|
|
300
|
-
# Wait for threads to finish
|
|
301
|
-
yield from self.join_threads()
|
|
395
|
+
self.run_hooks('on_interval', sub='item')
|
|
302
396
|
|
|
303
397
|
except BaseException as e:
|
|
304
|
-
self.debug(f'encountered exception {type(e).__name__}. Stopping remote tasks.', sub='
|
|
398
|
+
self.debug(f'encountered exception {type(e).__name__}. Stopping remote tasks.', sub='run')
|
|
305
399
|
error = Error.from_exception(e)
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
self.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
400
|
+
self.add_result(error)
|
|
401
|
+
self.revoked = True
|
|
402
|
+
if not self.sync: # yield latest results from Celery
|
|
403
|
+
self.stop_celery_tasks()
|
|
404
|
+
for item in self.yielder():
|
|
405
|
+
yield from self._process_item(item)
|
|
406
|
+
self.run_hooks('on_interval', sub='item')
|
|
313
407
|
|
|
314
408
|
finally:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
409
|
+
yield from self.results_buffer
|
|
410
|
+
self.results_buffer = []
|
|
411
|
+
self._finalize()
|
|
412
|
+
|
|
413
|
+
def _finalize(self):
|
|
414
|
+
"""Finalize the runner."""
|
|
415
|
+
self.join_threads()
|
|
416
|
+
gc.collect()
|
|
417
|
+
if self.sync:
|
|
418
|
+
self.mark_completed()
|
|
419
|
+
if self.enable_reports:
|
|
420
|
+
self.export_reports()
|
|
321
421
|
|
|
322
422
|
def join_threads(self):
|
|
323
423
|
"""Wait for all running threads to complete."""
|
|
324
424
|
if not self.threads:
|
|
325
425
|
return
|
|
326
|
-
self.debug(f'waiting for {len(self.threads)} threads to complete')
|
|
426
|
+
self.debug(f'waiting for {len(self.threads)} threads to complete', sub='end')
|
|
327
427
|
for thread in self.threads:
|
|
328
428
|
error = thread.join()
|
|
329
429
|
if error:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
self.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
430
|
+
self.add_result(error)
|
|
431
|
+
|
|
432
|
+
def _run_extractors(self, results):
|
|
433
|
+
"""Run extractors on results and targets."""
|
|
434
|
+
self.debug('running extractors', sub='init')
|
|
435
|
+
ctx = {'opts': DotMap(self.run_opts), 'targets': self.inputs, 'ancestor_id': self.ancestor_id}
|
|
436
|
+
inputs, run_opts, errors = run_extractors(
|
|
437
|
+
results,
|
|
438
|
+
self.run_opts,
|
|
439
|
+
self.inputs,
|
|
440
|
+
ctx=ctx,
|
|
441
|
+
dry_run=self.dry_run)
|
|
442
|
+
for error in errors:
|
|
443
|
+
self.add_result(error)
|
|
444
|
+
self.inputs = sorted(list(set(inputs)))
|
|
445
|
+
self.debug(f'extracted {len(self.inputs)} inputs', sub='init')
|
|
446
|
+
self.run_opts = run_opts
|
|
447
|
+
|
|
448
|
+
def add_result(self, item, print=True, output=True, hooks=True, queue=True):
|
|
345
449
|
"""Add item to runner results.
|
|
346
450
|
|
|
347
451
|
Args:
|
|
348
452
|
item (OutputType): Item.
|
|
349
453
|
print (bool): Whether to print it or not.
|
|
350
454
|
output (bool): Whether to add it to the output or not.
|
|
455
|
+
hooks (bool): Whether to run hooks on the item.
|
|
456
|
+
queue (bool): Whether to queue the item for later processing.
|
|
351
457
|
"""
|
|
352
|
-
|
|
458
|
+
if item._uuid and item._uuid in self.uuids:
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
# Keep existing ancestor id in context
|
|
462
|
+
ancestor_id = item._context.get('ancestor_id', None)
|
|
463
|
+
|
|
464
|
+
# Set context
|
|
465
|
+
item._context.update(self.context)
|
|
466
|
+
item._context['ancestor_id'] = ancestor_id or self.ancestor_id
|
|
467
|
+
|
|
468
|
+
# Set uuid
|
|
469
|
+
if not item._uuid:
|
|
470
|
+
item._uuid = str(uuid.uuid4())
|
|
471
|
+
|
|
472
|
+
# Set source
|
|
473
|
+
if not item._source:
|
|
474
|
+
item._source = self.unique_name
|
|
475
|
+
|
|
476
|
+
# Check for state updates
|
|
477
|
+
if isinstance(item, State) and self.celery_result and item.task_id == self.celery_result.id:
|
|
478
|
+
self.debug(f'update runner state from remote state: {item.state}', sub='item')
|
|
479
|
+
if item.state in ['FAILURE', 'SUCCESS', 'REVOKED']:
|
|
480
|
+
self.started = True
|
|
481
|
+
self.done = True
|
|
482
|
+
self.progress = 100
|
|
483
|
+
self.end_time = datetime.fromtimestamp(time())
|
|
484
|
+
elif item.state in ['RUNNING']:
|
|
485
|
+
self.started = True
|
|
486
|
+
self.start_time = datetime.fromtimestamp(time())
|
|
487
|
+
self.end_time = None
|
|
488
|
+
self.last_updated_celery = item._timestamp
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
# If progress item, update runner progress
|
|
492
|
+
elif isinstance(item, Progress) and item._source == self.unique_name:
|
|
493
|
+
self.debug(f'update runner progress: {item.percent}', sub='item', verbose=True)
|
|
494
|
+
if not should_update(CONFIG.runners.progress_update_frequency, self.last_updated_progress, item._timestamp):
|
|
495
|
+
return
|
|
496
|
+
self.progress = item.percent
|
|
497
|
+
self.last_updated_progress = item._timestamp
|
|
498
|
+
|
|
499
|
+
# If info item and task_id is defined, update runner celery_ids
|
|
500
|
+
elif isinstance(item, Info) and item.task_id and item.task_id not in self.celery_ids:
|
|
501
|
+
self.debug(f'update runner celery_ids from remote: {item.task_id}', sub='item')
|
|
502
|
+
self.celery_ids.append(item.task_id)
|
|
503
|
+
|
|
504
|
+
# If output type, run on_item hooks
|
|
505
|
+
elif isinstance(item, tuple(OUTPUT_TYPES)) and hooks:
|
|
506
|
+
item = self.run_hooks('on_item', item, sub='item')
|
|
507
|
+
if not item:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
# Add item to results
|
|
511
|
+
self.uuids.add(item._uuid)
|
|
353
512
|
self.results.append(item)
|
|
513
|
+
self.results_count += 1
|
|
354
514
|
if output:
|
|
355
515
|
self.output += repr(item) + '\n'
|
|
356
516
|
if print:
|
|
357
517
|
self._print_item(item)
|
|
518
|
+
if queue:
|
|
519
|
+
self.results_buffer.append(item)
|
|
358
520
|
|
|
359
521
|
def add_subtask(self, task_id, task_name, task_description):
|
|
360
522
|
"""Add a Celery subtask to the current runner for tracking purposes.
|
|
@@ -386,9 +548,9 @@ class Runner:
|
|
|
386
548
|
|
|
387
549
|
# Item is an output type
|
|
388
550
|
if isinstance(item, OutputType):
|
|
389
|
-
self.debug(item, lazy=lambda x: repr(x), sub='item', allow_no_process=False, verbose=True)
|
|
390
551
|
_type = item._type
|
|
391
552
|
print_this_type = getattr(self, f'print_{_type}', True)
|
|
553
|
+
self.debug(item, lazy=lambda x: repr(x), sub='item', allow_no_process=False, verbose=print_this_type)
|
|
392
554
|
if not print_this_type:
|
|
393
555
|
return
|
|
394
556
|
|
|
@@ -421,13 +583,14 @@ class Runner:
|
|
|
421
583
|
# Repr output
|
|
422
584
|
if item_out:
|
|
423
585
|
item_repr = repr(item)
|
|
424
|
-
if
|
|
586
|
+
if self.print_remote_info and item._source:
|
|
425
587
|
item_repr += rich_to_ansi(rf' \[[dim]{item._source}[/]]')
|
|
588
|
+
# item_repr += f' ({self.__class__.__name__}) ({item._uuid}) ({item._context.get("ancestor_id")})' # for debugging
|
|
426
589
|
self._print(item_repr, out=item_out)
|
|
427
590
|
|
|
428
591
|
# Item is a line
|
|
429
592
|
elif isinstance(item, str):
|
|
430
|
-
self.debug(item, sub='line', allow_no_process=False, verbose=True)
|
|
593
|
+
self.debug(item, sub='line.print', allow_no_process=False, verbose=True)
|
|
431
594
|
if self.print_line or force:
|
|
432
595
|
self._print(item, out=sys.stderr, end='\n', rich=False)
|
|
433
596
|
|
|
@@ -446,13 +609,15 @@ class Runner:
|
|
|
446
609
|
if sub:
|
|
447
610
|
new_sub += f'.{sub}'
|
|
448
611
|
kwargs['sub'] = new_sub
|
|
612
|
+
if self.id and not self.sync:
|
|
613
|
+
kwargs['id'] = self.id
|
|
449
614
|
debug(*args, **kwargs)
|
|
450
615
|
|
|
451
616
|
def mark_duplicates(self):
|
|
452
617
|
"""Check for duplicates and mark items as duplicates."""
|
|
453
618
|
if not self.enable_duplicate_check:
|
|
454
619
|
return
|
|
455
|
-
self.debug('running duplicate check',
|
|
620
|
+
self.debug('running duplicate check', sub='end')
|
|
456
621
|
# dupe_count = 0
|
|
457
622
|
import concurrent.futures
|
|
458
623
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=100)
|
|
@@ -463,7 +628,7 @@ class Runner:
|
|
|
463
628
|
# if duplicates:
|
|
464
629
|
# duplicates_str = '\n\t'.join(duplicates)
|
|
465
630
|
# self.debug(f'Duplicates ({dupe_count}):\n\t{duplicates_str}', sub='duplicates', verbose=True)
|
|
466
|
-
# self.debug(f'duplicate check completed: {dupe_count} found',
|
|
631
|
+
# self.debug(f'duplicate check completed: {dupe_count} found', sub='duplicates')
|
|
467
632
|
|
|
468
633
|
def check_duplicate(self, item):
|
|
469
634
|
"""Check if an item is a duplicate in the list of results and mark it like so.
|
|
@@ -471,7 +636,7 @@ class Runner:
|
|
|
471
636
|
Args:
|
|
472
637
|
item (OutputType): Secator output type.
|
|
473
638
|
"""
|
|
474
|
-
self.debug('running duplicate check for item', obj=item.toDict(), obj_breaklines=True, sub='
|
|
639
|
+
self.debug('running duplicate check for item', obj=item.toDict(), obj_breaklines=True, sub='item.duplicate', verbose=True) # noqa: E501
|
|
475
640
|
others = [f for f in self.results if f == item and f._uuid != item._uuid]
|
|
476
641
|
if others:
|
|
477
642
|
main = max(item, *others)
|
|
@@ -480,22 +645,21 @@ class Runner:
|
|
|
480
645
|
main._related.extend([dupe._uuid for dupe in dupes])
|
|
481
646
|
main._related = list(dict.fromkeys(main._related))
|
|
482
647
|
if main._uuid != item._uuid:
|
|
483
|
-
self.debug(f'found {len(others)} duplicates for', obj=item.toDict(), obj_breaklines=True, sub='
|
|
648
|
+
self.debug(f'found {len(others)} duplicates for', obj=item.toDict(), obj_breaklines=True, sub='item.duplicate', verbose=True) # noqa: E501
|
|
484
649
|
item._duplicate = True
|
|
485
|
-
item = self.run_hooks('on_item', item)
|
|
650
|
+
item = self.run_hooks('on_item', item, sub='item.duplicate')
|
|
486
651
|
if item._uuid not in main._related:
|
|
487
652
|
main._related.append(item._uuid)
|
|
488
|
-
main = self.run_hooks('on_duplicate', main)
|
|
489
|
-
item = self.run_hooks('on_duplicate', item)
|
|
653
|
+
main = self.run_hooks('on_duplicate', main, sub='item.duplicate')
|
|
654
|
+
item = self.run_hooks('on_duplicate', item, sub='item.duplicate')
|
|
490
655
|
|
|
491
656
|
for dupe in dupes:
|
|
492
657
|
if not dupe._duplicate:
|
|
493
658
|
self.debug(
|
|
494
659
|
'found new duplicate', obj=dupe.toDict(), obj_breaklines=True,
|
|
495
|
-
sub='
|
|
496
|
-
# dupe_count += 1
|
|
660
|
+
sub='item.duplicate', verbose=True)
|
|
497
661
|
dupe._duplicate = True
|
|
498
|
-
dupe = self.run_hooks('on_duplicate', dupe)
|
|
662
|
+
dupe = self.run_hooks('on_duplicate', dupe, sub='item.duplicate')
|
|
499
663
|
|
|
500
664
|
def yielder(self):
|
|
501
665
|
"""Base yielder implementation.
|
|
@@ -506,15 +670,30 @@ class Runner:
|
|
|
506
670
|
Yields:
|
|
507
671
|
secator.output_types.OutputType: Secator output type.
|
|
508
672
|
"""
|
|
673
|
+
# If existing celery result, yield from it
|
|
674
|
+
if self.celery_result:
|
|
675
|
+
yield from CeleryData.iter_results(
|
|
676
|
+
self.celery_result,
|
|
677
|
+
ids_map=self.celery_ids_map,
|
|
678
|
+
description=True,
|
|
679
|
+
revoked=self.revoked,
|
|
680
|
+
print_remote_info=self.print_remote_info,
|
|
681
|
+
print_remote_title=f'[bold gold3]{self.__class__.__name__.capitalize()}[/] [bold magenta]{self.name}[/] results'
|
|
682
|
+
)
|
|
683
|
+
return
|
|
684
|
+
|
|
509
685
|
# Build Celery workflow
|
|
686
|
+
self.debug('building celery workflow', sub='start')
|
|
510
687
|
workflow = self.build_celery_workflow()
|
|
688
|
+
self.print_target = False
|
|
511
689
|
|
|
512
690
|
# Run workflow and get results
|
|
513
691
|
if self.sync:
|
|
514
692
|
self.print_item = False
|
|
693
|
+
self.debug('running workflow in sync mode', sub='start')
|
|
515
694
|
results = workflow.apply().get()
|
|
516
|
-
yield from results
|
|
517
695
|
else:
|
|
696
|
+
self.debug('running workflow in async mode', sub='start')
|
|
518
697
|
self.celery_result = workflow()
|
|
519
698
|
self.celery_ids.append(str(self.celery_result.id))
|
|
520
699
|
yield Info(
|
|
@@ -522,7 +701,6 @@ class Runner:
|
|
|
522
701
|
task_id=self.celery_result.id
|
|
523
702
|
)
|
|
524
703
|
if self.no_poll:
|
|
525
|
-
self.enable_hooks = False
|
|
526
704
|
self.enable_reports = False
|
|
527
705
|
self.no_process = True
|
|
528
706
|
return
|
|
@@ -557,7 +735,7 @@ class Runner:
|
|
|
557
735
|
'end_time': self.end_time,
|
|
558
736
|
'elapsed': self.elapsed.total_seconds(),
|
|
559
737
|
'elapsed_human': self.elapsed_human,
|
|
560
|
-
'run_opts':
|
|
738
|
+
'run_opts': self.resolved_opts,
|
|
561
739
|
}
|
|
562
740
|
data.update({
|
|
563
741
|
'config': self.config.toDict(),
|
|
@@ -576,74 +754,76 @@ class Runner:
|
|
|
576
754
|
})
|
|
577
755
|
return data
|
|
578
756
|
|
|
579
|
-
def run_hooks(self, hook_type, *args):
|
|
757
|
+
def run_hooks(self, hook_type, *args, sub='hooks'):
|
|
580
758
|
""""Run hooks of a certain type.
|
|
581
759
|
|
|
582
760
|
Args:
|
|
583
761
|
hook_type (str): Hook type.
|
|
584
762
|
args (list): List of arguments to pass to the hook.
|
|
763
|
+
sub (str): Debug id.
|
|
585
764
|
|
|
586
765
|
Returns:
|
|
587
766
|
any: Hook return value.
|
|
588
767
|
"""
|
|
589
768
|
result = args[0] if len(args) > 0 else None
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
769
|
+
if self.no_process:
|
|
770
|
+
self.debug('hook skipped (no_process)', obj={'name': hook_type}, sub=sub, verbose=True) # noqa: E501
|
|
771
|
+
return result
|
|
772
|
+
if self.dry_run:
|
|
773
|
+
self.debug('hook skipped (dry_run)', obj={'name': hook_type}, sub=sub, verbose=True) # noqa: E501
|
|
774
|
+
return result
|
|
775
|
+
for hook in self.resolved_hooks[hook_type]:
|
|
593
776
|
fun = self.get_func_path(hook)
|
|
594
777
|
try:
|
|
595
778
|
if hook_type == 'on_interval' and not should_update(CONFIG.runners.backend_update_frequency, self.last_updated_db):
|
|
596
|
-
self.debug('', obj={
|
|
779
|
+
self.debug('hook skipped (backend update frequency)', obj={'name': hook_type, 'fun': fun}, sub=sub, verbose=True) # noqa: E501
|
|
597
780
|
return
|
|
598
781
|
if not self.enable_hooks or self.no_process:
|
|
599
|
-
self.debug('', obj={
|
|
782
|
+
self.debug('hook skipped (disabled hooks or no_process)', obj={'name': hook_type, 'fun': fun}, sub=sub, verbose=True) # noqa: E501
|
|
600
783
|
continue
|
|
601
|
-
# self.debug('', obj={f'{name} [dim yellow]->[/] {fun}': '[dim yellow]started[/]'}, id=_id, sub='hooks', verbose=True) # noqa: E501
|
|
602
784
|
result = hook(self, *args)
|
|
603
|
-
self.debug('', obj={
|
|
785
|
+
self.debug('hook success', obj={'name': hook_type, 'fun': fun}, sub=sub, verbose='item' in sub) # noqa: E501
|
|
786
|
+
if isinstance(result, Error):
|
|
787
|
+
self.add_result(result, hooks=False)
|
|
604
788
|
except Exception as e:
|
|
605
|
-
self.debug('', obj={
|
|
606
|
-
error = Error.from_exception(e)
|
|
607
|
-
error.message = f'Hook "{fun}" execution failed.'
|
|
608
|
-
error._source = self.unique_name
|
|
609
|
-
error._uuid = str(uuid.uuid4())
|
|
610
|
-
self.add_result(error, print=True)
|
|
789
|
+
self.debug('hook failed', obj={'name': hook_type, 'fun': fun}, sub=sub) # noqa: E501
|
|
790
|
+
error = Error.from_exception(e, message=f'Hook "{fun}" execution failed')
|
|
611
791
|
if self.raise_on_error:
|
|
612
792
|
raise e
|
|
793
|
+
self.add_result(error, hooks=False)
|
|
613
794
|
return result
|
|
614
795
|
|
|
615
|
-
def run_validators(self, validator_type, *args, error=True):
|
|
796
|
+
def run_validators(self, validator_type, *args, error=True, sub='validators'):
|
|
616
797
|
"""Run validators of a certain type.
|
|
617
798
|
|
|
618
799
|
Args:
|
|
619
800
|
validator_type (str): Validator type. E.g: on_start.
|
|
620
801
|
args (list): List of arguments to pass to the validator.
|
|
621
802
|
error (bool): Whether to add an error to runner results if the validator failed.
|
|
803
|
+
sub (str): Debug id.
|
|
622
804
|
|
|
623
805
|
Returns:
|
|
624
806
|
bool: Validator return value.
|
|
625
807
|
"""
|
|
626
808
|
if self.no_process:
|
|
809
|
+
self.debug('validator skipped (no_process)', obj={'name': validator_type}, sub=sub, verbose=True) # noqa: E501
|
|
810
|
+
return True
|
|
811
|
+
if self.dry_run:
|
|
812
|
+
self.debug('validator skipped (dry_run)', obj={'name': validator_type}, sub=sub, verbose=True) # noqa: E501
|
|
627
813
|
return True
|
|
628
|
-
|
|
629
|
-
for validator in self.validators[validator_type]:
|
|
630
|
-
name = f'{self.__class__.__name__}.{validator_type}'
|
|
814
|
+
for validator in self.resolved_validators[validator_type]:
|
|
631
815
|
fun = self.get_func_path(validator)
|
|
632
816
|
if not validator(self, *args):
|
|
633
|
-
self.debug('', obj={name
|
|
817
|
+
self.debug('validator failed', obj={'name': validator_type, 'fun': fun}, sub=sub) # noqa: E501
|
|
634
818
|
doc = validator.__doc__
|
|
635
819
|
if error:
|
|
636
820
|
message = 'Validator failed'
|
|
637
821
|
if doc:
|
|
638
822
|
message += f': {doc}'
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
_source=self.unique_name,
|
|
642
|
-
_uuid=str(uuid.uuid4())
|
|
643
|
-
)
|
|
644
|
-
self.add_result(error, print=True)
|
|
823
|
+
err = Error(message=message)
|
|
824
|
+
self.add_result(err)
|
|
645
825
|
return False
|
|
646
|
-
self.debug('', obj={name
|
|
826
|
+
self.debug('validator success', obj={'name': validator_type, 'fun': fun}, sub=sub) # noqa: E501
|
|
647
827
|
return True
|
|
648
828
|
|
|
649
829
|
def register_hooks(self, hooks):
|
|
@@ -652,23 +832,21 @@ class Runner:
|
|
|
652
832
|
Args:
|
|
653
833
|
hooks (dict[str, List[Callable]]): List of hooks to register.
|
|
654
834
|
"""
|
|
655
|
-
for key in self.
|
|
835
|
+
for key in self.resolved_hooks:
|
|
656
836
|
# Register class + derived class hooks
|
|
657
837
|
class_hook = getattr(self, key, None)
|
|
658
838
|
if class_hook:
|
|
659
|
-
name = f'{self.__class__.__name__}.{key}'
|
|
660
839
|
fun = self.get_func_path(class_hook)
|
|
661
|
-
self.debug('', obj={name
|
|
662
|
-
self.
|
|
840
|
+
self.debug('hook registered', obj={'name': key, 'fun': fun}, sub='init')
|
|
841
|
+
self.resolved_hooks[key].append(class_hook)
|
|
663
842
|
|
|
664
843
|
# Register user hooks
|
|
665
844
|
user_hooks = hooks.get(self.__class__, {}).get(key, [])
|
|
666
845
|
user_hooks.extend(hooks.get(key, []))
|
|
667
846
|
for hook in user_hooks:
|
|
668
|
-
name = f'{self.__class__.__name__}.{key}'
|
|
669
847
|
fun = self.get_func_path(hook)
|
|
670
|
-
self.debug('', obj={name
|
|
671
|
-
self.
|
|
848
|
+
self.debug('hook registered', obj={'name': key, 'fun': fun}, sub='init')
|
|
849
|
+
self.resolved_hooks[key].extend(user_hooks)
|
|
672
850
|
|
|
673
851
|
def register_validators(self, validators):
|
|
674
852
|
"""Register validators.
|
|
@@ -677,21 +855,19 @@ class Runner:
|
|
|
677
855
|
validators (dict[str, List[Callable]]): Validators to register.
|
|
678
856
|
"""
|
|
679
857
|
# Register class + derived class hooks
|
|
680
|
-
for key in self.
|
|
858
|
+
for key in self.resolved_validators:
|
|
681
859
|
class_validator = getattr(self, key, None)
|
|
682
860
|
if class_validator:
|
|
683
|
-
name = f'{self.__class__.__name__}.{key}'
|
|
684
861
|
fun = self.get_func_path(class_validator)
|
|
685
|
-
self.
|
|
686
|
-
self.debug('', obj={name
|
|
862
|
+
self.resolved_validators[key].append(class_validator)
|
|
863
|
+
self.debug('validator registered', obj={'name': key, 'fun': fun}, sub='init')
|
|
687
864
|
|
|
688
865
|
# Register user hooks
|
|
689
866
|
user_validators = validators.get(key, [])
|
|
690
867
|
for validator in user_validators:
|
|
691
|
-
name = f'{self.__class__.__name__}.{key}'
|
|
692
868
|
fun = self.get_func_path(validator)
|
|
693
|
-
self.debug('', obj={name
|
|
694
|
-
self.
|
|
869
|
+
self.debug('validator registered', obj={'name': key, 'fun': fun}, sub='init')
|
|
870
|
+
self.resolved_validators[key].extend(user_validators)
|
|
695
871
|
|
|
696
872
|
def mark_started(self):
|
|
697
873
|
"""Mark runner as started."""
|
|
@@ -699,9 +875,9 @@ class Runner:
|
|
|
699
875
|
return
|
|
700
876
|
self.started = True
|
|
701
877
|
self.start_time = datetime.fromtimestamp(time())
|
|
702
|
-
self.debug(f'started (sync: {self.sync}, hooks: {self.enable_hooks})')
|
|
878
|
+
self.debug(f'started (sync: {self.sync}, hooks: {self.enable_hooks}), chunk: {self.chunk}, chunk_count: {self.chunk_count}', sub='start') # noqa: E501
|
|
703
879
|
self.log_start()
|
|
704
|
-
self.run_hooks('on_start')
|
|
880
|
+
self.run_hooks('on_start', sub='start')
|
|
705
881
|
|
|
706
882
|
def mark_completed(self):
|
|
707
883
|
"""Mark runner as completed."""
|
|
@@ -711,28 +887,50 @@ class Runner:
|
|
|
711
887
|
self.done = True
|
|
712
888
|
self.progress = 100
|
|
713
889
|
self.end_time = datetime.fromtimestamp(time())
|
|
714
|
-
self.debug(f'completed (sync: {self.sync}, reports: {self.enable_reports}, hooks: {self.enable_hooks})')
|
|
890
|
+
self.debug(f'completed (status: {self.status}, sync: {self.sync}, reports: {self.enable_reports}, hooks: {self.enable_hooks})', sub='end') # noqa: E501
|
|
715
891
|
self.mark_duplicates()
|
|
716
|
-
self.run_hooks('on_end')
|
|
892
|
+
self.run_hooks('on_end', sub='end')
|
|
717
893
|
self.export_profiler()
|
|
718
894
|
self.log_results()
|
|
719
895
|
|
|
720
896
|
def log_start(self):
|
|
721
897
|
"""Log runner start."""
|
|
722
|
-
if not self.
|
|
898
|
+
if not self.print_start:
|
|
899
|
+
return
|
|
900
|
+
if self.has_parent:
|
|
723
901
|
return
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
902
|
+
if self.config.type != 'task':
|
|
903
|
+
tree = textwrap.indent(build_runner_tree(self.config).render_tree(), ' ')
|
|
904
|
+
info = Info(message=f'{self.config.type.capitalize()} built:\n{tree}', _source=self.unique_name)
|
|
905
|
+
self._print(info, rich=True)
|
|
906
|
+
remote_str = 'started' if self.sync else 'started in worker'
|
|
907
|
+
msg = f'{self.config.type.capitalize()} {format_runner_name(self)}'
|
|
908
|
+
if self.description:
|
|
909
|
+
msg += f' ([dim]{self.description}[/])'
|
|
910
|
+
info = Info(message=f'{msg} {remote_str}', _source=self.unique_name)
|
|
911
|
+
self._print(info, rich=True)
|
|
727
912
|
|
|
728
913
|
def log_results(self):
|
|
729
914
|
"""Log runner results."""
|
|
730
|
-
|
|
731
|
-
|
|
915
|
+
if not self.print_end:
|
|
916
|
+
return
|
|
917
|
+
if self.has_parent:
|
|
918
|
+
return
|
|
919
|
+
info = Info(
|
|
920
|
+
message=(
|
|
921
|
+
f'{self.config.type.capitalize()} {format_runner_name(self)} finished with status '
|
|
922
|
+
f'[bold {STATE_COLORS[self.status]}]{self.status}[/] and found '
|
|
923
|
+
f'[bold]{len(self.findings)}[/] findings'
|
|
924
|
+
)
|
|
925
|
+
)
|
|
926
|
+
self._print(info, rich=True)
|
|
732
927
|
|
|
733
928
|
def export_reports(self):
|
|
734
929
|
"""Export reports."""
|
|
735
|
-
if self.enable_reports and self.exporters and not self.no_process:
|
|
930
|
+
if self.enable_reports and self.exporters and not self.no_process and not self.dry_run:
|
|
931
|
+
if self.print_end:
|
|
932
|
+
exporters_str = ', '.join([f'[bold cyan]{e.__name__.replace("Exporter", "").lower()}[/]' for e in self.exporters])
|
|
933
|
+
self._print(Info(message=f'Exporting results with exporters: {exporters_str}'), rich=True)
|
|
736
934
|
report = Report(self, exporters=self.exporters)
|
|
737
935
|
report.build()
|
|
738
936
|
report.send()
|
|
@@ -740,12 +938,13 @@ class Runner:
|
|
|
740
938
|
|
|
741
939
|
def export_profiler(self):
|
|
742
940
|
"""Export profiler."""
|
|
743
|
-
if self.
|
|
941
|
+
if self.enable_pyinstrument:
|
|
942
|
+
self.debug('stopping profiler', sub='end')
|
|
744
943
|
self.profiler.stop()
|
|
745
944
|
profile_path = Path(self.reports_folder) / f'{self.unique_name}_profile.html'
|
|
746
945
|
with profile_path.open('w', encoding='utf-8') as f_html:
|
|
747
946
|
f_html.write(self.profiler.output_html())
|
|
748
|
-
self._print_item(Info(message=f'Wrote profile to {str(profile_path)}'
|
|
947
|
+
self._print_item(Info(message=f'Wrote profile to {str(profile_path)}'), force=True)
|
|
749
948
|
|
|
750
949
|
def stop_celery_tasks(self):
|
|
751
950
|
"""Stop all tasks running in Celery worker."""
|
|
@@ -770,14 +969,14 @@ class Runner:
|
|
|
770
969
|
# Init the new item and the list of output types to load from
|
|
771
970
|
new_item = None
|
|
772
971
|
output_types = getattr(self, 'output_types', [])
|
|
773
|
-
self.debug(f'
|
|
972
|
+
self.debug(f'input item: {item}', sub='item.convert', verbose=True)
|
|
774
973
|
|
|
775
974
|
# Use a function to pick proper output types
|
|
776
975
|
output_discriminator = getattr(self, 'output_discriminator', None)
|
|
777
976
|
if output_discriminator:
|
|
778
977
|
result = output_discriminator(item)
|
|
779
978
|
if result:
|
|
780
|
-
self.debug(
|
|
979
|
+
self.debug('discriminated output type with output_discriminator', sub='item.convert', verbose=True)
|
|
781
980
|
output_types = [result]
|
|
782
981
|
else:
|
|
783
982
|
output_types = []
|
|
@@ -787,21 +986,21 @@ class Runner:
|
|
|
787
986
|
otypes = [o for o in output_types if o.get_name() == item['_type']]
|
|
788
987
|
if otypes:
|
|
789
988
|
output_types = [otypes[0]]
|
|
790
|
-
self.debug(
|
|
989
|
+
self.debug('discriminated output type with _type key', sub='item.convert', verbose=True)
|
|
791
990
|
|
|
792
991
|
# Load item using picked output types
|
|
793
|
-
self.debug(f'
|
|
992
|
+
self.debug(f'output types to try: {[str(o) for o in output_types]}', sub='item.convert', verbose=True)
|
|
794
993
|
for klass in output_types:
|
|
795
|
-
self.debug(f'
|
|
994
|
+
self.debug(f'loading item as {str(klass)}', sub='item.convert', verbose=True)
|
|
796
995
|
output_map = getattr(self, 'output_map', {}).get(klass, {})
|
|
797
996
|
try:
|
|
798
997
|
new_item = klass.load(item, output_map)
|
|
799
|
-
self.debug(f'
|
|
998
|
+
self.debug(f'successfully loaded item as {str(klass)}', sub='item.convert', verbose=True)
|
|
800
999
|
break
|
|
801
1000
|
except (TypeError, KeyError) as e:
|
|
802
1001
|
self.debug(
|
|
803
|
-
f'
|
|
804
|
-
sub='
|
|
1002
|
+
f'failed loading item as {str(klass)}: {type(e).__name__}: {str(e)}.',
|
|
1003
|
+
sub='item.convert', verbose=True)
|
|
805
1004
|
# error = Error.from_exception(e)
|
|
806
1005
|
# self.debug(repr(error), sub='debug.klass.load')
|
|
807
1006
|
continue
|
|
@@ -809,7 +1008,7 @@ class Runner:
|
|
|
809
1008
|
if not new_item:
|
|
810
1009
|
new_item = Warning(message=f'Failed to load item as output type:\n {item}')
|
|
811
1010
|
|
|
812
|
-
self.debug(f'
|
|
1011
|
+
self.debug(f'output item: {new_item.toDict()}', sub='item.convert', verbose=True)
|
|
813
1012
|
|
|
814
1013
|
return new_item
|
|
815
1014
|
|
|
@@ -870,72 +1069,44 @@ class Runner:
|
|
|
870
1069
|
return
|
|
871
1070
|
|
|
872
1071
|
# Run item validators
|
|
873
|
-
if not self.run_validators('validate_item', item, error=False):
|
|
1072
|
+
if not self.run_validators('validate_item', item, error=False, sub='item'):
|
|
874
1073
|
return
|
|
875
1074
|
|
|
876
1075
|
# Convert output dict to another schema
|
|
877
1076
|
if isinstance(item, dict):
|
|
878
|
-
item = self.run_hooks('on_item_pre_convert', item)
|
|
1077
|
+
item = self.run_hooks('on_item_pre_convert', item, sub='item')
|
|
879
1078
|
if not item:
|
|
880
1079
|
return
|
|
881
1080
|
item = self._convert_item_schema(item)
|
|
882
1081
|
|
|
883
|
-
# Update item context
|
|
884
|
-
item._context.update(self.context)
|
|
885
|
-
|
|
886
|
-
# Add uuid to item
|
|
887
|
-
if not item._uuid:
|
|
888
|
-
item._uuid = str(uuid.uuid4())
|
|
889
|
-
|
|
890
|
-
# Return if already seen
|
|
891
|
-
if item._uuid in self.uuids:
|
|
892
|
-
return
|
|
893
|
-
|
|
894
|
-
# Add source to item
|
|
895
|
-
if not item._source:
|
|
896
|
-
item._source = self.unique_name
|
|
897
|
-
|
|
898
|
-
# Check for state updates
|
|
899
|
-
if isinstance(item, State) and self.celery_result and item.task_id == self.celery_result.id:
|
|
900
|
-
self.debug(f'Sync runner state from remote: {item.state}')
|
|
901
|
-
if item.state in ['FAILURE', 'SUCCESS', 'REVOKED']:
|
|
902
|
-
self.started = True
|
|
903
|
-
self.done = True
|
|
904
|
-
self.progress = 100
|
|
905
|
-
self.end_time = datetime.fromtimestamp(time())
|
|
906
|
-
elif item.state in ['RUNNING']:
|
|
907
|
-
self.started = True
|
|
908
|
-
self.start_time = datetime.fromtimestamp(time())
|
|
909
|
-
self.end_time = None
|
|
910
|
-
self.last_updated_celery = item._timestamp
|
|
911
|
-
return
|
|
912
|
-
|
|
913
|
-
# If progress item, update runner progress
|
|
914
|
-
elif isinstance(item, Progress) and item._source == self.unique_name:
|
|
915
|
-
self.progress = item.percent
|
|
916
|
-
if not should_update(CONFIG.runners.progress_update_frequency, self.last_updated_progress, item._timestamp):
|
|
917
|
-
return
|
|
918
|
-
elif int(item.percent) in [0, 100]:
|
|
919
|
-
return
|
|
920
|
-
else:
|
|
921
|
-
self.last_updated_progress = item._timestamp
|
|
922
|
-
|
|
923
|
-
# If info item and task_id is defined, update runner celery_ids
|
|
924
|
-
elif isinstance(item, Info) and item.task_id and item.task_id not in self.celery_ids:
|
|
925
|
-
self.celery_ids.append(item.task_id)
|
|
926
|
-
|
|
927
|
-
# If output type, run on_item hooks
|
|
928
|
-
elif isinstance(item, tuple(OUTPUT_TYPES)):
|
|
929
|
-
item = self.run_hooks('on_item', item)
|
|
930
|
-
if not item:
|
|
931
|
-
return
|
|
932
|
-
|
|
933
1082
|
# Add item to results
|
|
934
|
-
self.add_result(item, print=print)
|
|
1083
|
+
self.add_result(item, print=print, queue=False)
|
|
935
1084
|
|
|
936
1085
|
# Yield item
|
|
937
1086
|
yield item
|
|
938
1087
|
|
|
1088
|
+
@staticmethod
|
|
1089
|
+
def _validate_inputs(self, inputs):
|
|
1090
|
+
"""Input type is not supported by runner"""
|
|
1091
|
+
supported_types = ', '.join(self.config.input_types) if self.config.input_types else 'any'
|
|
1092
|
+
for _input in inputs:
|
|
1093
|
+
input_type = autodetect_type(_input)
|
|
1094
|
+
if self.config.input_types and input_type not in self.config.input_types:
|
|
1095
|
+
message = (
|
|
1096
|
+
f'Validator failed: target [bold blue]{_input}[/] of type [bold green]{input_type}[/] '
|
|
1097
|
+
f'is not supported by [bold gold3]{self.unique_name}[/]. Supported types: [bold green]{supported_types}[/]'
|
|
1098
|
+
)
|
|
1099
|
+
if self.has_parent:
|
|
1100
|
+
message += '. Removing from current inputs (runner context)'
|
|
1101
|
+
info = Info(message=message)
|
|
1102
|
+
self.inputs.remove(_input)
|
|
1103
|
+
self.add_result(info)
|
|
1104
|
+
else:
|
|
1105
|
+
error = Error(message=message)
|
|
1106
|
+
self.add_result(error)
|
|
1107
|
+
return False
|
|
1108
|
+
return True
|
|
1109
|
+
|
|
939
1110
|
@staticmethod
|
|
940
1111
|
def resolve_exporters(exporters):
|
|
941
1112
|
"""Resolve exporters from output options.
|
|
@@ -957,8 +1128,8 @@ class Runner:
|
|
|
957
1128
|
]
|
|
958
1129
|
return [cls for cls in classes if cls]
|
|
959
1130
|
|
|
960
|
-
def
|
|
961
|
-
"""
|
|
1131
|
+
def resolve_profiles(self, profiles):
|
|
1132
|
+
"""Resolve profiles and update run options.
|
|
962
1133
|
|
|
963
1134
|
Args:
|
|
964
1135
|
profiles (list[str]): List of profile names to resolve.
|
|
@@ -966,22 +1137,64 @@ class Runner:
|
|
|
966
1137
|
Returns:
|
|
967
1138
|
list: List of profiles.
|
|
968
1139
|
"""
|
|
969
|
-
|
|
1140
|
+
# Return if profiles are disabled
|
|
1141
|
+
if not self.enable_profiles:
|
|
1142
|
+
return []
|
|
1143
|
+
|
|
1144
|
+
# Split profiles if comma separated
|
|
970
1145
|
if isinstance(profiles, str):
|
|
971
1146
|
profiles = profiles.split(',')
|
|
1147
|
+
|
|
1148
|
+
# Add default profiles
|
|
1149
|
+
default_profiles = CONFIG.profiles.defaults
|
|
1150
|
+
for p in default_profiles:
|
|
1151
|
+
if p in profiles:
|
|
1152
|
+
continue
|
|
1153
|
+
profiles.append(p)
|
|
1154
|
+
|
|
1155
|
+
# Abort if no profiles
|
|
1156
|
+
if not profiles:
|
|
1157
|
+
return []
|
|
1158
|
+
|
|
1159
|
+
# Get profile configs
|
|
972
1160
|
templates = []
|
|
1161
|
+
profile_configs = get_configs_by_type('profile')
|
|
973
1162
|
for pname in profiles:
|
|
974
|
-
matches = [p for p in
|
|
1163
|
+
matches = [p for p in profile_configs if p.name == pname]
|
|
975
1164
|
if not matches:
|
|
976
|
-
self._print(Warning(message=f'Profile "{pname}" was not found'), rich=True)
|
|
1165
|
+
self._print(Warning(message=f'Profile "{pname}" was not found. Run [bold green]secator profiles list[/] to see available profiles.'), rich=True) # noqa: E501
|
|
977
1166
|
else:
|
|
978
1167
|
templates.append(matches[0])
|
|
979
|
-
|
|
1168
|
+
|
|
1169
|
+
if not templates:
|
|
1170
|
+
self.debug('no profiles loaded', sub='init')
|
|
1171
|
+
return
|
|
1172
|
+
|
|
1173
|
+
# Put enforced profiles last
|
|
1174
|
+
enforced_templates = [p for p in templates if p.enforce]
|
|
1175
|
+
non_enforced_templates = [p for p in templates if not p.enforce]
|
|
1176
|
+
templates = non_enforced_templates + enforced_templates
|
|
1177
|
+
profile_opts = {}
|
|
980
1178
|
for profile in templates:
|
|
981
|
-
self.
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1179
|
+
self.debug(f'profile {profile.name} opts (enforced: {profile.enforce}): {profile.opts}', sub='init')
|
|
1180
|
+
enforced = profile.enforce or False
|
|
1181
|
+
description = profile.description or ''
|
|
1182
|
+
if enforced:
|
|
1183
|
+
profile_opts.update(profile.opts)
|
|
1184
|
+
else:
|
|
1185
|
+
profile_opts.update({k: self.run_opts.get(k) or v for k, v in profile.opts.items()})
|
|
1186
|
+
if self.print_profiles:
|
|
1187
|
+
msg = f'Loaded profile [bold pink3]{profile.name}[/]'
|
|
1188
|
+
if description:
|
|
1189
|
+
msg += f' ([dim]{description}[/])'
|
|
1190
|
+
if enforced:
|
|
1191
|
+
msg += ' [bold red](enforced)[/]'
|
|
1192
|
+
profile_opts_str = ", ".join([f'[bold yellow3]{k}[/]=[dim yellow3]{v}[/]' for k, v in profile.opts.items()])
|
|
1193
|
+
msg += rf' \[[dim]{profile_opts_str}[/]]'
|
|
1194
|
+
self._print(Info(message=msg), rich=True)
|
|
1195
|
+
if profile_opts:
|
|
1196
|
+
self.run_opts.update(profile_opts)
|
|
1197
|
+
return templates
|
|
985
1198
|
|
|
986
1199
|
@classmethod
|
|
987
1200
|
def get_func_path(cls, func):
|