secator 0.6.0__py3-none-any.whl → 0.8.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 +160 -185
- secator/celery_utils.py +268 -0
- secator/cli.py +427 -176
- secator/config.py +114 -68
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/subdomain_recon.yaml +2 -2
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_dirsearch.yaml +1 -1
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +170 -92
- secator/definitions.py +11 -4
- secator/exporters/__init__.py +7 -5
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +27 -19
- secator/exporters/gdrive.py +16 -11
- secator/exporters/json.py +3 -1
- secator/exporters/table.py +30 -2
- secator/exporters/txt.py +20 -16
- secator/hooks/gcs.py +53 -0
- secator/hooks/mongodb.py +53 -27
- secator/installer.py +277 -60
- secator/output_types/__init__.py +29 -11
- secator/output_types/_base.py +11 -1
- secator/output_types/error.py +36 -0
- secator/output_types/exploit.py +12 -8
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +8 -1
- secator/output_types/port.py +9 -2
- secator/output_types/progress.py +5 -0
- secator/output_types/record.py +5 -3
- secator/output_types/stat.py +33 -0
- secator/output_types/subdomain.py +1 -1
- secator/output_types/tag.py +8 -6
- secator/output_types/target.py +2 -2
- secator/output_types/url.py +14 -11
- secator/output_types/user_account.py +6 -6
- secator/output_types/vulnerability.py +8 -6
- secator/output_types/warning.py +24 -0
- secator/report.py +56 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +629 -638
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +404 -214
- secator/runners/scan.py +8 -24
- secator/runners/task.py +21 -55
- secator/runners/workflow.py +41 -40
- secator/scans/__init__.py +28 -0
- secator/serializers/dataclass.py +6 -0
- secator/serializers/json.py +10 -5
- secator/serializers/regex.py +12 -4
- secator/tasks/_categories.py +147 -42
- secator/tasks/bbot.py +295 -0
- secator/tasks/bup.py +99 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +14 -25
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +10 -20
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +4 -4
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +11 -13
- secator/tasks/h8mail.py +32 -42
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +19 -23
- secator/tasks/maigret.py +27 -25
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +22 -19
- secator/tasks/naabu.py +18 -2
- secator/tasks/nmap.py +82 -55
- secator/tasks/nuclei.py +13 -3
- secator/tasks/searchsploit.py +26 -11
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +91 -94
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +417 -78
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
- secator-0.8.0.dist-info/RECORD +115 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
- secator-0.6.0.dist-info/RECORD +0 -101
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/licenses/LICENSE +0 -0
secator/runners/command.py
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import getpass
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
import re
|
|
5
6
|
import shlex
|
|
7
|
+
import signal
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
10
|
+
import uuid
|
|
8
11
|
|
|
9
|
-
from time import
|
|
12
|
+
from time import time
|
|
10
13
|
|
|
14
|
+
import psutil
|
|
11
15
|
from fp.fp import FreeProxy
|
|
12
16
|
|
|
13
|
-
from secator.template import TemplateLoader
|
|
14
17
|
from secator.definitions import OPT_NOT_SUPPORTED, OPT_PIPE_INPUT
|
|
15
18
|
from secator.config import CONFIG
|
|
19
|
+
from secator.output_types import Info, Error, Target, Stat
|
|
16
20
|
from secator.runners import Runner
|
|
17
|
-
from secator.
|
|
18
|
-
from secator.utils import debug
|
|
21
|
+
from secator.template import TemplateLoader
|
|
22
|
+
from secator.utils import debug, rich_escape as _s
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
logger = logging.getLogger(__name__)
|
|
@@ -53,18 +57,18 @@ class Command(Runner):
|
|
|
53
57
|
# Output encoding
|
|
54
58
|
encoding = 'utf-8'
|
|
55
59
|
|
|
56
|
-
# Environment variables
|
|
57
|
-
env = {}
|
|
58
|
-
|
|
59
60
|
# Flag to take the input
|
|
60
61
|
input_flag = None
|
|
61
62
|
|
|
62
63
|
# Input path (if a file is constructed)
|
|
63
64
|
input_path = None
|
|
64
65
|
|
|
65
|
-
# Input chunk size
|
|
66
|
+
# Input chunk size
|
|
66
67
|
input_chunk_size = CONFIG.runners.input_chunk_size
|
|
67
68
|
|
|
69
|
+
# Input required
|
|
70
|
+
input_required = True
|
|
71
|
+
|
|
68
72
|
# Flag to take a file as input
|
|
69
73
|
file_flag = None
|
|
70
74
|
|
|
@@ -75,12 +79,21 @@ class Command(Runner):
|
|
|
75
79
|
version_flag = None
|
|
76
80
|
|
|
77
81
|
# Install
|
|
82
|
+
install_pre = None
|
|
83
|
+
install_post = None
|
|
78
84
|
install_cmd = None
|
|
79
85
|
install_github_handle = None
|
|
80
86
|
|
|
81
87
|
# Serializer
|
|
82
88
|
item_loader = None
|
|
83
|
-
item_loaders = [
|
|
89
|
+
item_loaders = []
|
|
90
|
+
|
|
91
|
+
# Hooks
|
|
92
|
+
hooks = [
|
|
93
|
+
'on_cmd',
|
|
94
|
+
'on_cmd_done',
|
|
95
|
+
'on_line'
|
|
96
|
+
]
|
|
84
97
|
|
|
85
98
|
# Ignore return code
|
|
86
99
|
ignore_return_code = False
|
|
@@ -88,9 +101,6 @@ class Command(Runner):
|
|
|
88
101
|
# Return code
|
|
89
102
|
return_code = -1
|
|
90
103
|
|
|
91
|
-
# Error
|
|
92
|
-
error = ''
|
|
93
|
-
|
|
94
104
|
# Output
|
|
95
105
|
output = ''
|
|
96
106
|
|
|
@@ -102,7 +112,8 @@ class Command(Runner):
|
|
|
102
112
|
# Profile
|
|
103
113
|
profile = 'cpu'
|
|
104
114
|
|
|
105
|
-
def __init__(self,
|
|
115
|
+
def __init__(self, inputs=[], **run_opts):
|
|
116
|
+
|
|
106
117
|
# Build runnerconfig on-the-fly
|
|
107
118
|
config = TemplateLoader(input={
|
|
108
119
|
'name': self.__class__.__name__,
|
|
@@ -110,26 +121,45 @@ class Command(Runner):
|
|
|
110
121
|
'description': run_opts.get('description', None)
|
|
111
122
|
})
|
|
112
123
|
|
|
113
|
-
#
|
|
124
|
+
# Extract run opts
|
|
114
125
|
hooks = run_opts.pop('hooks', {})
|
|
126
|
+
caller = run_opts.get('caller', None)
|
|
115
127
|
results = run_opts.pop('results', [])
|
|
116
128
|
context = run_opts.pop('context', {})
|
|
129
|
+
self.skip_if_no_inputs = run_opts.pop('skip_if_no_inputs', False)
|
|
130
|
+
|
|
131
|
+
# Prepare validators
|
|
132
|
+
input_validators = []
|
|
133
|
+
if not self.skip_if_no_inputs:
|
|
134
|
+
input_validators.append(self._validate_input_nonempty)
|
|
135
|
+
if not caller:
|
|
136
|
+
input_validators.append(self._validate_chunked_input)
|
|
137
|
+
validators = {'validate_input': input_validators}
|
|
138
|
+
|
|
139
|
+
# Call super().__init__
|
|
117
140
|
super().__init__(
|
|
118
141
|
config=config,
|
|
119
|
-
|
|
142
|
+
inputs=inputs,
|
|
120
143
|
results=results,
|
|
121
144
|
run_opts=run_opts,
|
|
122
145
|
hooks=hooks,
|
|
146
|
+
validators=validators,
|
|
123
147
|
context=context)
|
|
124
148
|
|
|
149
|
+
# Inputs path
|
|
150
|
+
self.inputs_path = None
|
|
151
|
+
|
|
125
152
|
# Current working directory for cmd
|
|
126
153
|
self.cwd = self.run_opts.get('cwd', None)
|
|
127
154
|
|
|
128
|
-
#
|
|
129
|
-
self.
|
|
155
|
+
# Print cmd
|
|
156
|
+
self.print_cmd = self.run_opts.get('print_cmd', False)
|
|
130
157
|
|
|
131
|
-
#
|
|
132
|
-
self.
|
|
158
|
+
# Stat update
|
|
159
|
+
self.last_updated_stat = None
|
|
160
|
+
|
|
161
|
+
# Process
|
|
162
|
+
self.process = None
|
|
133
163
|
|
|
134
164
|
# Proxy config (global)
|
|
135
165
|
self.proxy = self.run_opts.pop('proxy', False)
|
|
@@ -141,6 +171,9 @@ class Command(Runner):
|
|
|
141
171
|
# Build command
|
|
142
172
|
self._build_cmd()
|
|
143
173
|
|
|
174
|
+
# Run on_cmd hook
|
|
175
|
+
self.run_hooks('on_cmd')
|
|
176
|
+
|
|
144
177
|
# Build item loaders
|
|
145
178
|
instance_func = getattr(self, 'item_loader', None)
|
|
146
179
|
item_loaders = self.item_loaders.copy()
|
|
@@ -148,40 +181,6 @@ class Command(Runner):
|
|
|
148
181
|
item_loaders.append(instance_func)
|
|
149
182
|
self.item_loaders = item_loaders
|
|
150
183
|
|
|
151
|
-
# Print built cmd
|
|
152
|
-
if self.print_cmd and not self.has_children:
|
|
153
|
-
if self.sync and self.description:
|
|
154
|
-
self._print(f'\n:wrench: {self.description} ...', color='bold gold3', rich=True)
|
|
155
|
-
self._print(self.cmd.replace('[', '\\['), color='bold cyan', rich=True)
|
|
156
|
-
|
|
157
|
-
# Print built input
|
|
158
|
-
if self.print_input_file and self.input_path:
|
|
159
|
-
input_str = '\n '.join(self.input).strip()
|
|
160
|
-
debug(f'[dim magenta]File input:[/]\n [italic medium_turquoise]{input_str}[/]')
|
|
161
|
-
|
|
162
|
-
# Print run options
|
|
163
|
-
if self.print_run_opts:
|
|
164
|
-
input_str = '\n '.join([
|
|
165
|
-
f'[dim blue]{k}[/] -> [dim green]{v}[/]' for k, v in self.run_opts.items() if v is not None]).strip()
|
|
166
|
-
debug(f'[dim magenta]Run opts:[/]\n {input_str}')
|
|
167
|
-
|
|
168
|
-
# Print format options
|
|
169
|
-
if self.print_fmt_opts:
|
|
170
|
-
input_str = '\n '.join([
|
|
171
|
-
f'[dim blue]{k}[/] -> [dim green]{v}[/]' for k, v in self.opts_to_print.items() if v is not None]).strip()
|
|
172
|
-
debug(f'[dim magenta]Print opts:[/]\n {input_str}')
|
|
173
|
-
|
|
174
|
-
# Print hooks
|
|
175
|
-
if self.print_hooks:
|
|
176
|
-
input_str = ''
|
|
177
|
-
for hook_name, hook_funcs in self.hooks.items():
|
|
178
|
-
hook_funcs_str = ', '.join([f'[dim green]{h.__module__}.{h.__qualname__}[/]' for h in hook_funcs])
|
|
179
|
-
if hook_funcs:
|
|
180
|
-
input_str += f'[dim blue]{hook_name}[/] -> {hook_funcs_str}\n '
|
|
181
|
-
input_str = input_str.strip()
|
|
182
|
-
if input_str:
|
|
183
|
-
debug(f'[dim magenta]Hooks:[/]\n {input_str}')
|
|
184
|
-
|
|
185
184
|
def toDict(self):
|
|
186
185
|
res = super().toDict()
|
|
187
186
|
res.update({
|
|
@@ -196,6 +195,7 @@ class Command(Runner):
|
|
|
196
195
|
# TODO: Move this to TaskBase
|
|
197
196
|
from secator.celery import run_command
|
|
198
197
|
results = kwargs.get('results', [])
|
|
198
|
+
kwargs['sync'] = False
|
|
199
199
|
name = cls.__name__
|
|
200
200
|
return run_command.apply_async(args=[results, name] + list(args), kwargs={'opts': kwargs}, queue=cls.profile)
|
|
201
201
|
|
|
@@ -206,7 +206,7 @@ class Command(Runner):
|
|
|
206
206
|
return run_command.s(cls.__name__, *args, opts=kwargs).set(queue=cls.profile)
|
|
207
207
|
|
|
208
208
|
@classmethod
|
|
209
|
-
def si(cls,
|
|
209
|
+
def si(cls, *args, results=[], **kwargs):
|
|
210
210
|
# TODO: Move this to TaskBase
|
|
211
211
|
from secator.celery import run_command
|
|
212
212
|
return run_command.si(results, cls.__name__, *args, opts=kwargs).set(queue=cls.profile)
|
|
@@ -226,12 +226,14 @@ class Command(Runner):
|
|
|
226
226
|
d[k] = v.__name__
|
|
227
227
|
return d
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
cls_opts = copy.deepcopy(cls.opts)
|
|
230
|
+
opts = {k: convert(v) for k, v in cls_opts.items()}
|
|
230
231
|
for k, v in opts.items():
|
|
231
232
|
v['meta'] = cls.__name__
|
|
232
233
|
v['supported'] = True
|
|
233
234
|
|
|
234
|
-
|
|
235
|
+
cls_meta_opts = copy.deepcopy(cls.meta_opts)
|
|
236
|
+
meta_opts = {k: convert(v) for k, v in cls_meta_opts.items() if cls.opt_key_map.get(k) is not OPT_NOT_SUPPORTED}
|
|
235
237
|
for k, v in meta_opts.items():
|
|
236
238
|
v['meta'] = 'meta'
|
|
237
239
|
if cls.opt_key_map.get(k) is OPT_NOT_SUPPORTED:
|
|
@@ -247,7 +249,7 @@ class Command(Runner):
|
|
|
247
249
|
#---------------#
|
|
248
250
|
|
|
249
251
|
@classmethod
|
|
250
|
-
def execute(cls, cmd, name=None, cls_attributes={}, **kwargs):
|
|
252
|
+
def execute(cls, cmd, name=None, cls_attributes={}, run=True, **kwargs):
|
|
251
253
|
"""Execute an ad-hoc command.
|
|
252
254
|
|
|
253
255
|
Can be used without defining an inherited class to run a command, while still enjoying all the good stuff in
|
|
@@ -264,15 +266,13 @@ class Command(Runner):
|
|
|
264
266
|
secator.runners.Command: instance of the Command.
|
|
265
267
|
"""
|
|
266
268
|
name = name or cmd.split(' ')[0]
|
|
267
|
-
kwargs['no_process'] = kwargs.get('no_process', True)
|
|
268
269
|
kwargs['print_cmd'] = not kwargs.get('quiet', False)
|
|
269
|
-
kwargs['
|
|
270
|
-
kwargs['
|
|
271
|
-
|
|
272
|
-
cmd_instance = type(name, (Command,), {'cmd': cmd})(**kwargs)
|
|
270
|
+
kwargs['print_line'] = True
|
|
271
|
+
kwargs['no_process'] = kwargs.get('no_process', True)
|
|
272
|
+
cmd_instance = type(name, (Command,), {'cmd': cmd, 'input_required': False})(**kwargs)
|
|
273
273
|
for k, v in cls_attributes.items():
|
|
274
274
|
setattr(cmd_instance, k, v)
|
|
275
|
-
if
|
|
275
|
+
if run:
|
|
276
276
|
cmd_instance.run()
|
|
277
277
|
return cmd_instance
|
|
278
278
|
|
|
@@ -328,124 +328,257 @@ class Command(Runner):
|
|
|
328
328
|
|
|
329
329
|
Yields:
|
|
330
330
|
str: Command stdout / stderr.
|
|
331
|
-
dict:
|
|
331
|
+
dict: Serialized object.
|
|
332
332
|
"""
|
|
333
|
-
|
|
334
|
-
self.status = 'RUNNING'
|
|
333
|
+
try:
|
|
335
334
|
|
|
336
|
-
|
|
337
|
-
|
|
335
|
+
# Abort if it has children tasks
|
|
336
|
+
if self.has_children:
|
|
337
|
+
return
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
339
|
+
# Print task description
|
|
340
|
+
self.print_description()
|
|
341
341
|
|
|
342
|
-
|
|
343
|
-
|
|
342
|
+
# Abort if no inputs
|
|
343
|
+
if len(self.inputs) == 0 and self.skip_if_no_inputs:
|
|
344
|
+
yield Info(message=f'{self.unique_name} skipped (no inputs)', _source=self.unique_name, _uuid=str(uuid.uuid4()))
|
|
345
|
+
return
|
|
344
346
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
347
|
+
# Yield targets
|
|
348
|
+
for input in self.inputs:
|
|
349
|
+
yield Target(name=input, _source=self.unique_name, _uuid=str(uuid.uuid4()))
|
|
350
|
+
|
|
351
|
+
# Check for sudo requirements and prepare the password if needed
|
|
352
|
+
sudo_password, error = self._prompt_sudo(self.cmd)
|
|
353
|
+
if error:
|
|
354
|
+
yield Error(
|
|
355
|
+
message=error,
|
|
356
|
+
_source=self.unique_name,
|
|
357
|
+
_uuid=str(uuid.uuid4())
|
|
358
|
+
)
|
|
359
|
+
return
|
|
348
360
|
|
|
349
|
-
|
|
350
|
-
|
|
361
|
+
# Prepare cmds
|
|
362
|
+
command = self.cmd if self.shell else shlex.split(self.cmd)
|
|
363
|
+
|
|
364
|
+
# Check command is installed and auto-install
|
|
365
|
+
if not self.no_process and not self.is_installed():
|
|
366
|
+
if CONFIG.security.auto_install_commands:
|
|
367
|
+
from secator.installer import ToolInstaller
|
|
368
|
+
yield Info(
|
|
369
|
+
message=f'Command {self.name} is missing but auto-installing since security.autoinstall_commands is set', # noqa: E501
|
|
370
|
+
_source=self.unique_name,
|
|
371
|
+
_uuid=str(uuid.uuid4())
|
|
372
|
+
)
|
|
373
|
+
status = ToolInstaller.install(self.__class__)
|
|
374
|
+
if not status.is_ok():
|
|
375
|
+
yield Error(
|
|
376
|
+
message=f'Failed installing {self.name}',
|
|
377
|
+
_source=self.unique_name,
|
|
378
|
+
_uuid=str(uuid.uuid4())
|
|
379
|
+
)
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# Output and results
|
|
383
|
+
self.return_code = 0
|
|
384
|
+
self.killed = False
|
|
385
|
+
|
|
386
|
+
# Run the command using subprocess
|
|
351
387
|
env = os.environ
|
|
352
|
-
env.update(self.env)
|
|
353
388
|
self.process = subprocess.Popen(
|
|
354
389
|
command,
|
|
355
390
|
stdin=subprocess.PIPE if sudo_password else None,
|
|
356
|
-
stdout=
|
|
357
|
-
stderr=
|
|
391
|
+
stdout=subprocess.PIPE,
|
|
392
|
+
stderr=subprocess.STDOUT,
|
|
358
393
|
universal_newlines=True,
|
|
359
394
|
shell=self.shell,
|
|
360
395
|
env=env,
|
|
361
396
|
cwd=self.cwd)
|
|
397
|
+
self.print_command()
|
|
362
398
|
|
|
363
399
|
# If sudo password is provided, send it to stdin
|
|
364
400
|
if sudo_password:
|
|
365
401
|
self.process.stdin.write(f"{sudo_password}\n")
|
|
366
402
|
self.process.stdin.flush()
|
|
367
403
|
|
|
368
|
-
except FileNotFoundError as e:
|
|
369
|
-
if self.config.name in str(e):
|
|
370
|
-
error = 'Executable not found.'
|
|
371
|
-
if self.install_cmd:
|
|
372
|
-
error += f' Install it with `secator install tools {self.config.name}`.'
|
|
373
|
-
else:
|
|
374
|
-
error = str(e)
|
|
375
|
-
celery_id = self.context.get('celery_id', '')
|
|
376
|
-
if celery_id:
|
|
377
|
-
error += f' [{celery_id}]'
|
|
378
|
-
self.errors.append(error)
|
|
379
|
-
self.return_code = 1
|
|
380
|
-
return
|
|
381
|
-
|
|
382
|
-
try:
|
|
383
|
-
# No capture mode, wait for command to finish and return
|
|
384
|
-
if self.no_capture:
|
|
385
|
-
self._wait_for_end()
|
|
386
|
-
return
|
|
387
|
-
|
|
388
404
|
# Process the output in real-time
|
|
389
405
|
for line in iter(lambda: self.process.stdout.readline(), b''):
|
|
390
|
-
sleep(0) # for async to give up control
|
|
406
|
+
# sleep(0) # for async to give up control
|
|
391
407
|
if not line:
|
|
392
408
|
break
|
|
409
|
+
yield from self.process_line(line)
|
|
393
410
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
411
|
+
# Run hooks after cmd has completed successfully
|
|
412
|
+
result = self.run_hooks('on_cmd_done')
|
|
413
|
+
if result:
|
|
414
|
+
yield from result
|
|
415
|
+
|
|
416
|
+
except FileNotFoundError as e:
|
|
417
|
+
yield from self.handle_file_not_found(e)
|
|
418
|
+
|
|
419
|
+
except BaseException as e:
|
|
420
|
+
self.debug(f'{self.unique_name}: {type(e).__name__}.', sub='error')
|
|
421
|
+
self.stop_process()
|
|
422
|
+
yield Error.from_exception(e, _source=self.unique_name, _uuid=str(uuid.uuid4()))
|
|
423
|
+
|
|
424
|
+
finally:
|
|
425
|
+
yield from self._wait_for_end()
|
|
426
|
+
|
|
427
|
+
def is_installed(self):
|
|
428
|
+
"""Check if a command is installed by using `which`.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
command (str): The command to check.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
bool: True if the command is installed, False otherwise.
|
|
435
|
+
"""
|
|
436
|
+
result = subprocess.Popen(["which", self.cmd.split(' ')[0]], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
437
|
+
result.communicate()
|
|
438
|
+
return result.returncode == 0
|
|
439
|
+
|
|
440
|
+
def process_line(self, line):
|
|
441
|
+
"""Process a single line of output emitted on stdout / stderr and yield results."""
|
|
442
|
+
|
|
443
|
+
# Strip line endings
|
|
444
|
+
line = line.rstrip()
|
|
399
445
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
line = ansi_escape.sub('', line)
|
|
406
|
-
line = line.replace('\\x0d\\x0a', '\n')
|
|
446
|
+
# Some commands output ANSI text, so we need to remove those ANSI chars
|
|
447
|
+
if self.encoding == 'ansi':
|
|
448
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
449
|
+
line = ansi_escape.sub('', line)
|
|
450
|
+
line = line.replace('\\x0d\\x0a', '\n')
|
|
407
451
|
|
|
408
|
-
|
|
409
|
-
|
|
452
|
+
# Run on_line hooks
|
|
453
|
+
line = self.run_hooks('on_line', line)
|
|
454
|
+
if line is None:
|
|
455
|
+
return
|
|
456
|
+
|
|
457
|
+
# Run item_loader to try parsing as dict
|
|
458
|
+
item_count = 0
|
|
459
|
+
for item in self.run_item_loaders(line):
|
|
460
|
+
yield item
|
|
461
|
+
item_count += 1
|
|
462
|
+
|
|
463
|
+
# Yield line if no items were yielded
|
|
464
|
+
if item_count == 0:
|
|
465
|
+
yield line
|
|
466
|
+
|
|
467
|
+
# Skip rest of iteration (no process mode)
|
|
468
|
+
if self.no_process:
|
|
469
|
+
return
|
|
470
|
+
|
|
471
|
+
# Yield command stats (CPU, memory, conns ...)
|
|
472
|
+
# TODO: enable stats support with timer
|
|
473
|
+
if self.last_updated_stat and (time() - self.last_updated_stat) < CONFIG.runners.stat_update_frequency:
|
|
474
|
+
return
|
|
410
475
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
if self.output_json:
|
|
414
|
-
items = self.run_item_loaders(line)
|
|
476
|
+
yield from self.stats()
|
|
477
|
+
self.last_updated_stat = time()
|
|
415
478
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
479
|
+
def print_description(self):
|
|
480
|
+
"""Print description"""
|
|
481
|
+
if self.sync and not self.has_children:
|
|
482
|
+
if self.caller and self.description:
|
|
483
|
+
self._print(f'\n[bold gold3]:wrench: {self.description} [dim cyan]({self.config.name})[/][/] ...', rich=True)
|
|
419
484
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
485
|
+
def print_command(self):
|
|
486
|
+
"""Print command."""
|
|
487
|
+
if self.print_cmd:
|
|
488
|
+
cmd_str = _s(self.cmd)
|
|
489
|
+
if self.sync and self.chunk and self.chunk_count:
|
|
490
|
+
cmd_str += f' [dim gray11]({self.chunk}/{self.chunk_count})[/]'
|
|
491
|
+
self._print(cmd_str, color='bold cyan', rich=True)
|
|
492
|
+
self.debug('Command', obj={'cmd': self.cmd}, sub='init')
|
|
493
|
+
|
|
494
|
+
def handle_file_not_found(self, exc):
|
|
495
|
+
"""Handle case where binary is not found.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
exc (FileNotFoundError): the exception.
|
|
499
|
+
|
|
500
|
+
Yields:
|
|
501
|
+
secator.output_types.Error: the error.
|
|
502
|
+
"""
|
|
503
|
+
self.return_code = 127
|
|
504
|
+
if self.config.name in str(exc):
|
|
505
|
+
message = 'Executable not found.'
|
|
506
|
+
if self.install_cmd:
|
|
507
|
+
message += f' Install it with [bold green4]secator install tools {self.config.name}[/].'
|
|
508
|
+
error = Error(message=message)
|
|
509
|
+
else:
|
|
510
|
+
error = Error.from_exception(exc)
|
|
511
|
+
error._source = self.unique_name
|
|
512
|
+
error._uuid = str(uuid.uuid4())
|
|
513
|
+
yield error
|
|
514
|
+
|
|
515
|
+
def stop_process(self):
|
|
516
|
+
"""Sends SIGINT to running process, if any."""
|
|
517
|
+
if not self.process:
|
|
518
|
+
return
|
|
519
|
+
self.debug(f'Sending SIGINT to process {self.process.pid}.', sub='error')
|
|
520
|
+
self.process.send_signal(signal.SIGINT)
|
|
423
521
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
522
|
+
def stats(self):
|
|
523
|
+
"""Gather stats about the current running process, if any."""
|
|
524
|
+
if not self.process or not self.process.pid:
|
|
525
|
+
return
|
|
526
|
+
proc = psutil.Process(self.process.pid)
|
|
527
|
+
stats = Command.get_process_info(proc, children=True)
|
|
528
|
+
for info in stats:
|
|
529
|
+
name = info['name']
|
|
530
|
+
pid = info['pid']
|
|
531
|
+
cpu_percent = info['cpu_percent']
|
|
532
|
+
mem_percent = info['memory_percent']
|
|
533
|
+
net_conns = info.get('net_connections') or []
|
|
534
|
+
extra_data = {k: v for k, v in info.items() if k not in ['cpu_percent', 'memory_percent', 'net_connections']}
|
|
535
|
+
yield Stat(
|
|
536
|
+
name=name,
|
|
537
|
+
pid=pid,
|
|
538
|
+
cpu=cpu_percent,
|
|
539
|
+
memory=mem_percent,
|
|
540
|
+
net_conns=len(net_conns),
|
|
541
|
+
extra_data=extra_data
|
|
542
|
+
)
|
|
427
543
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
544
|
+
@staticmethod
|
|
545
|
+
def get_process_info(process, children=False):
|
|
546
|
+
"""Get process information from psutil.
|
|
431
547
|
|
|
432
|
-
|
|
433
|
-
|
|
548
|
+
Args:
|
|
549
|
+
process (subprocess.Process): Process.
|
|
550
|
+
children (bool): Whether to gather stats about children processes too.
|
|
551
|
+
"""
|
|
552
|
+
try:
|
|
553
|
+
data = {
|
|
554
|
+
k: v._asdict() if hasattr(v, '_asdict') else v
|
|
555
|
+
for k, v in process.as_dict().items()
|
|
556
|
+
if k not in ['memory_maps', 'open_files', 'environ']
|
|
557
|
+
}
|
|
558
|
+
yield data
|
|
559
|
+
except (psutil.Error, FileNotFoundError):
|
|
560
|
+
return
|
|
561
|
+
if children:
|
|
562
|
+
for subproc in process.children(recursive=True):
|
|
563
|
+
yield from Command.get_process_info(subproc, children=False)
|
|
434
564
|
|
|
435
565
|
def run_item_loaders(self, line):
|
|
436
|
-
"""Run item loaders
|
|
437
|
-
|
|
566
|
+
"""Run item loaders against an output line.
|
|
567
|
+
|
|
568
|
+
Args:
|
|
569
|
+
line (str): Output line.
|
|
570
|
+
"""
|
|
571
|
+
if self.no_process:
|
|
572
|
+
return
|
|
438
573
|
for item_loader in self.item_loaders:
|
|
439
|
-
result = None
|
|
440
574
|
if (callable(item_loader)):
|
|
441
|
-
|
|
575
|
+
yield from item_loader(self, line)
|
|
442
576
|
elif item_loader:
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return items
|
|
577
|
+
name = item_loader.__class__.__name__.replace('Serializer', '').lower()
|
|
578
|
+
default_callback = lambda self, x: [(yield x)] # noqa: E731
|
|
579
|
+
callback = getattr(self, f'on_{name}_loaded', None) or default_callback
|
|
580
|
+
for item in item_loader.run(line):
|
|
581
|
+
yield from callback(self, item)
|
|
449
582
|
|
|
450
583
|
def _prompt_sudo(self, command):
|
|
451
584
|
"""
|
|
@@ -455,27 +588,31 @@ class Command(Runner):
|
|
|
455
588
|
command (str): The initial command to be executed.
|
|
456
589
|
|
|
457
590
|
Returns:
|
|
458
|
-
|
|
591
|
+
tuple: (sudo password, error).
|
|
459
592
|
"""
|
|
460
593
|
sudo_password = None
|
|
461
594
|
|
|
462
595
|
# Check if sudo is required by the command
|
|
463
596
|
if not re.search(r'\bsudo\b', command):
|
|
464
|
-
return None
|
|
597
|
+
return None, []
|
|
465
598
|
|
|
466
599
|
# Check if sudo can be executed without a password
|
|
467
|
-
|
|
468
|
-
|
|
600
|
+
try:
|
|
601
|
+
if subprocess.run(['sudo', '-n', 'true'], capture_output=False).returncode == 0:
|
|
602
|
+
return None, None
|
|
603
|
+
except ValueError:
|
|
604
|
+
self._print('[bold orange3]Could not run sudo check test.[/][bold green]Passing.[/]')
|
|
469
605
|
|
|
470
606
|
# Check if we have a tty
|
|
471
607
|
if not os.isatty(sys.stdin.fileno()):
|
|
472
|
-
|
|
473
|
-
|
|
608
|
+
error = "No TTY detected. Sudo password prompt requires a TTY to proceed."
|
|
609
|
+
return -1, error
|
|
474
610
|
|
|
475
611
|
# If not, prompt the user for a password
|
|
476
|
-
self._print('[bold red]Please enter sudo password to continue.[/]')
|
|
612
|
+
self._print('[bold red]Please enter sudo password to continue.[/]', rich=True)
|
|
477
613
|
for _ in range(3):
|
|
478
|
-
|
|
614
|
+
user = getpass.getuser()
|
|
615
|
+
self._print(rf'\[sudo] password for {user}: ▌', rich=True)
|
|
479
616
|
sudo_password = getpass.getpass()
|
|
480
617
|
result = subprocess.run(
|
|
481
618
|
['sudo', '-S', '-p', '', 'true'],
|
|
@@ -484,31 +621,43 @@ class Command(Runner):
|
|
|
484
621
|
capture_output=True
|
|
485
622
|
)
|
|
486
623
|
if result.returncode == 0:
|
|
487
|
-
return sudo_password # Password is correct
|
|
624
|
+
return sudo_password, None # Password is correct
|
|
488
625
|
self._print("Sorry, try again.")
|
|
489
|
-
|
|
490
|
-
return
|
|
626
|
+
error = "Sudo password verification failed after 3 attempts."
|
|
627
|
+
return -1, error
|
|
491
628
|
|
|
492
629
|
def _wait_for_end(self):
|
|
493
630
|
"""Wait for process to finish and process output and return code."""
|
|
631
|
+
if not self.process:
|
|
632
|
+
return
|
|
633
|
+
for line in self.process.stdout.readlines():
|
|
634
|
+
yield from self.process_line(line)
|
|
494
635
|
self.process.wait()
|
|
495
636
|
self.return_code = self.process.returncode
|
|
637
|
+
self.process.stdout.close()
|
|
638
|
+
self.return_code = 0 if self.ignore_return_code else self.return_code
|
|
639
|
+
self.output = self.output.strip()
|
|
640
|
+
self.killed = self.return_code == -2 or self.killed
|
|
641
|
+
self.debug(f'Command {self.cmd} finished with return code {self.return_code}', sub='command')
|
|
496
642
|
|
|
497
|
-
if self.
|
|
498
|
-
self.output = ''
|
|
499
|
-
else:
|
|
500
|
-
self.output = self.output.strip()
|
|
501
|
-
self.process.stdout.close()
|
|
502
|
-
|
|
503
|
-
if self.ignore_return_code:
|
|
504
|
-
self.return_code = 0
|
|
505
|
-
|
|
506
|
-
if self.return_code == -2 or self.killed:
|
|
643
|
+
if self.killed:
|
|
507
644
|
error = 'Process was killed manually (CTRL+C / CTRL+X)'
|
|
508
|
-
|
|
645
|
+
yield Error(
|
|
646
|
+
message=error,
|
|
647
|
+
_source=self.unique_name,
|
|
648
|
+
_uuid=str(uuid.uuid4())
|
|
649
|
+
)
|
|
650
|
+
|
|
509
651
|
elif self.return_code != 0:
|
|
510
652
|
error = f'Command failed with return code {self.return_code}.'
|
|
511
|
-
self.
|
|
653
|
+
last_lines = self.output.split('\n')
|
|
654
|
+
last_lines = last_lines[max(0, len(last_lines) - 2):]
|
|
655
|
+
yield Error(
|
|
656
|
+
message=error,
|
|
657
|
+
traceback='\n'.join(last_lines),
|
|
658
|
+
_source=self.unique_name,
|
|
659
|
+
_uuid=str(uuid.uuid4())
|
|
660
|
+
)
|
|
512
661
|
|
|
513
662
|
@staticmethod
|
|
514
663
|
def _process_opts(
|
|
@@ -518,15 +667,19 @@ class Command(Runner):
|
|
|
518
667
|
opt_value_map={},
|
|
519
668
|
opt_prefix='-',
|
|
520
669
|
command_name=None):
|
|
521
|
-
"""Process a dict of options using a config, option key map / value map
|
|
522
|
-
and option character like '-' or '--'.
|
|
670
|
+
"""Process a dict of options using a config, option key map / value map and option character like '-' or '--'.
|
|
523
671
|
|
|
524
672
|
Args:
|
|
525
673
|
opts (dict): Command options as input on the CLI.
|
|
526
674
|
opts_conf (dict): Options config (Click options definition).
|
|
675
|
+
opt_key_map (dict[str, str | Callable]): A dict to map option key with their actual values.
|
|
676
|
+
opt_value_map (dict, str | Callable): A dict to map option values with their actual values.
|
|
677
|
+
opt_prefix (str, default: '-'): Option prefix.
|
|
678
|
+
command_name (str | None, default: None): Command name.
|
|
527
679
|
"""
|
|
528
680
|
opts_str = ''
|
|
529
681
|
for opt_name, opt_conf in opts_conf.items():
|
|
682
|
+
debug('before get_opt_value', obj={'name': opt_name, 'conf': opt_conf}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
530
683
|
|
|
531
684
|
# Get opt value
|
|
532
685
|
default_val = opt_conf.get('default')
|
|
@@ -537,25 +690,35 @@ class Command(Runner):
|
|
|
537
690
|
opt_prefix=command_name,
|
|
538
691
|
default=default_val)
|
|
539
692
|
|
|
693
|
+
debug('after get_opt_value', obj={'name': opt_name, 'value': opt_val, 'conf': opt_conf}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
694
|
+
|
|
540
695
|
# Skip option if value is falsy
|
|
541
696
|
if opt_val in [None, False, []]:
|
|
542
|
-
|
|
697
|
+
debug('skipped (falsy)', obj={'name': opt_name, 'value': opt_val}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
543
698
|
continue
|
|
544
699
|
|
|
700
|
+
# Apply process function on opt value
|
|
701
|
+
if 'process' in opt_conf:
|
|
702
|
+
func = opt_conf['process']
|
|
703
|
+
opt_val = func(opt_val)
|
|
704
|
+
|
|
545
705
|
# Convert opt value to expected command opt value
|
|
546
706
|
mapped_opt_val = opt_value_map.get(opt_name)
|
|
547
|
-
if
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
707
|
+
if mapped_opt_val:
|
|
708
|
+
if callable(mapped_opt_val):
|
|
709
|
+
opt_val = mapped_opt_val(opt_val)
|
|
710
|
+
else:
|
|
711
|
+
opt_val = mapped_opt_val
|
|
551
712
|
|
|
552
713
|
# Convert opt name to expected command opt name
|
|
553
714
|
mapped_opt_name = opt_key_map.get(opt_name)
|
|
554
|
-
if mapped_opt_name
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
715
|
+
if mapped_opt_name is not None:
|
|
716
|
+
if mapped_opt_name == OPT_NOT_SUPPORTED:
|
|
717
|
+
debug('skipped (unsupported)', obj={'name': opt_name, 'value': opt_val}, sub='command.options', verbose=True) # noqa: E501
|
|
718
|
+
continue
|
|
719
|
+
else:
|
|
720
|
+
opt_name = mapped_opt_name
|
|
721
|
+
debug('mapped key / value', obj={'name': opt_name, 'value': opt_val}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
559
722
|
|
|
560
723
|
# Avoid shell injections and detect opt prefix
|
|
561
724
|
opt_name = str(opt_name).split(' ')[0] # avoid cmd injection
|
|
@@ -570,31 +733,66 @@ class Command(Runner):
|
|
|
570
733
|
# Append opt name + opt value to option string.
|
|
571
734
|
# Note: does not append opt value if value is True (flag)
|
|
572
735
|
opts_str += f' {opt_name}'
|
|
736
|
+
shlex_quote = opt_conf.get('shlex', True)
|
|
573
737
|
if opt_val is not True:
|
|
574
|
-
|
|
738
|
+
if shlex_quote:
|
|
739
|
+
opt_val = shlex.quote(str(opt_val))
|
|
575
740
|
opts_str += f' {opt_val}'
|
|
741
|
+
debug('final', obj={'name': opt_name, 'value': opt_val}, sub='command.options', obj_after=False, verbose=True)
|
|
576
742
|
|
|
577
743
|
return opts_str.strip()
|
|
578
744
|
|
|
745
|
+
@staticmethod
|
|
746
|
+
def _validate_chunked_input(self, inputs):
|
|
747
|
+
"""Command does not suport multiple inputs in non-worker mode. Consider using .delay() instead."""
|
|
748
|
+
if len(inputs) > 1 and self.sync and self.file_flag is None:
|
|
749
|
+
return False
|
|
750
|
+
return True
|
|
751
|
+
|
|
752
|
+
@staticmethod
|
|
753
|
+
def _validate_input_nonempty(self, inputs):
|
|
754
|
+
"""Input is empty."""
|
|
755
|
+
if not self.input_required:
|
|
756
|
+
return True
|
|
757
|
+
if not inputs or len(inputs) == 0:
|
|
758
|
+
return False
|
|
759
|
+
return True
|
|
760
|
+
|
|
761
|
+
# @staticmethod
|
|
762
|
+
# def _validate_input_types_valid(self, input):
|
|
763
|
+
# pass
|
|
764
|
+
|
|
765
|
+
@staticmethod
|
|
766
|
+
def _get_opt_default(opt_name, opts_conf):
|
|
767
|
+
for k, v in opts_conf.items():
|
|
768
|
+
if k == opt_name:
|
|
769
|
+
return v.get('default', None)
|
|
770
|
+
return None
|
|
771
|
+
|
|
579
772
|
@staticmethod
|
|
580
773
|
def _get_opt_value(opts, opt_name, opts_conf={}, opt_prefix='', default=None):
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
774
|
+
default = default or Command._get_opt_default(opt_name, opts_conf)
|
|
775
|
+
opt_names = [
|
|
776
|
+
f'{opt_prefix}.{opt_name}',
|
|
777
|
+
f'{opt_prefix}_{opt_name}',
|
|
778
|
+
opt_name,
|
|
585
779
|
]
|
|
586
|
-
|
|
780
|
+
opt_values = [opts.get(o) for o in opt_names]
|
|
781
|
+
alias = [conf.get('short') for _, conf in opts_conf.items() if conf.get('short') in opts and _ == opt_name]
|
|
587
782
|
if alias:
|
|
588
|
-
|
|
589
|
-
if OPT_NOT_SUPPORTED in
|
|
783
|
+
opt_values.append(opts.get(alias[0]))
|
|
784
|
+
if OPT_NOT_SUPPORTED in opt_values:
|
|
785
|
+
debug('skipped (unsupported)', obj={'name': opt_name}, obj_after=False, sub='command.options', verbose=True)
|
|
590
786
|
return None
|
|
591
|
-
|
|
787
|
+
value = next((v for v in opt_values if v is not None), default)
|
|
788
|
+
debug('got opt value', obj={'name': opt_name, 'value': value, 'aliases': opt_names, 'values': opt_values}, obj_after=False, sub='command.options', verbose=True) # noqa: E501
|
|
789
|
+
return value
|
|
592
790
|
|
|
593
791
|
def _build_cmd(self):
|
|
594
792
|
"""Build command string."""
|
|
595
793
|
|
|
596
794
|
# Add JSON flag to cmd
|
|
597
|
-
if self.
|
|
795
|
+
if self.json_flag:
|
|
598
796
|
self.cmd += f' {self.json_flag}'
|
|
599
797
|
|
|
600
798
|
# Add options to cmd
|
|
@@ -624,47 +822,39 @@ class Command(Runner):
|
|
|
624
822
|
string or a list to the cmd.
|
|
625
823
|
"""
|
|
626
824
|
cmd = self.cmd
|
|
627
|
-
|
|
825
|
+
inputs = self.inputs
|
|
628
826
|
|
|
629
|
-
# If
|
|
630
|
-
if not
|
|
827
|
+
# If inputs is empty, return the previous command
|
|
828
|
+
if not inputs:
|
|
631
829
|
return
|
|
632
830
|
|
|
633
|
-
# If
|
|
634
|
-
|
|
635
|
-
|
|
831
|
+
# If inputs has a single element but the tool does not support an input flag, use echo-piped_input input.
|
|
832
|
+
# If the tool's input flag is set to None, assume it is a positional argument at the end of the command.
|
|
833
|
+
# Otherwise use the input flag to pass the input.
|
|
834
|
+
if len(inputs) == 1:
|
|
835
|
+
input = shlex.quote(inputs[0])
|
|
836
|
+
if self.input_flag == OPT_PIPE_INPUT:
|
|
837
|
+
cmd = f'echo {input} | {cmd}'
|
|
838
|
+
elif not self.input_flag:
|
|
839
|
+
cmd += f' {input}'
|
|
840
|
+
else:
|
|
841
|
+
cmd += f' {self.input_flag} {input}'
|
|
636
842
|
|
|
637
|
-
# If
|
|
843
|
+
# If inputs has multiple elements and the tool has input_flag set to OPT_PIPE_INPUT, use cat-piped_input input.
|
|
638
844
|
# Otherwise pass the file path to the tool.
|
|
639
|
-
|
|
845
|
+
else:
|
|
640
846
|
fpath = f'{self.reports_folder}/.inputs/{self.unique_name}.txt'
|
|
641
847
|
|
|
642
848
|
# Write the input to a file
|
|
643
849
|
with open(fpath, 'w') as f:
|
|
644
|
-
f.write('\n'.join(
|
|
850
|
+
f.write('\n'.join(inputs))
|
|
645
851
|
|
|
646
852
|
if self.file_flag == OPT_PIPE_INPUT:
|
|
647
853
|
cmd = f'cat {fpath} | {cmd}'
|
|
648
854
|
elif self.file_flag:
|
|
649
855
|
cmd += f' {self.file_flag} {fpath}'
|
|
650
|
-
else:
|
|
651
|
-
self._print(f'{self.__class__.__name__} does not support multiple inputs.', color='bold red')
|
|
652
|
-
self.input_valid = False
|
|
653
856
|
|
|
654
|
-
self.
|
|
655
|
-
|
|
656
|
-
# If input is a string but the tool does not support an input flag, use echo-piped input.
|
|
657
|
-
# If the tool's input flag is set to None, assume it is a positional argument at the end of the command.
|
|
658
|
-
# Otherwise use the input flag to pass the input.
|
|
659
|
-
else:
|
|
660
|
-
input = shlex.quote(input)
|
|
661
|
-
if self.input_flag == OPT_PIPE_INPUT:
|
|
662
|
-
cmd = f'echo {input} | {cmd}'
|
|
663
|
-
elif not self.input_flag:
|
|
664
|
-
cmd += f' {input}'
|
|
665
|
-
else:
|
|
666
|
-
cmd += f' {self.input_flag} {input}'
|
|
857
|
+
self.inputs_path = fpath
|
|
667
858
|
|
|
668
859
|
self.cmd = cmd
|
|
669
860
|
self.shell = ' | ' in self.cmd
|
|
670
|
-
self.input = input
|