secator 0.0.1__py3-none-any.whl → 0.3.6__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/.gitignore +162 -0
- secator/celery.py +7 -67
- secator/cli.py +631 -274
- secator/decorators.py +54 -11
- secator/definitions.py +104 -33
- secator/exporters/csv.py +1 -2
- secator/exporters/gdrive.py +1 -1
- secator/exporters/json.py +1 -2
- secator/exporters/txt.py +1 -2
- secator/hooks/mongodb.py +12 -12
- secator/installer.py +335 -0
- secator/report.py +2 -14
- secator/rich.py +3 -10
- secator/runners/_base.py +105 -34
- secator/runners/_helpers.py +18 -17
- secator/runners/command.py +91 -55
- secator/runners/scan.py +2 -1
- secator/runners/task.py +5 -4
- secator/runners/workflow.py +12 -11
- secator/tasks/_categories.py +14 -19
- secator/tasks/cariddi.py +2 -1
- secator/tasks/dalfox.py +2 -0
- secator/tasks/dirsearch.py +5 -7
- secator/tasks/dnsx.py +1 -0
- secator/tasks/dnsxbrute.py +1 -0
- secator/tasks/feroxbuster.py +6 -7
- secator/tasks/ffuf.py +4 -7
- secator/tasks/gau.py +1 -4
- secator/tasks/gf.py +2 -1
- secator/tasks/gospider.py +1 -0
- secator/tasks/grype.py +47 -47
- secator/tasks/h8mail.py +5 -6
- secator/tasks/httpx.py +24 -18
- secator/tasks/katana.py +11 -15
- secator/tasks/maigret.py +3 -3
- secator/tasks/mapcidr.py +1 -0
- secator/tasks/msfconsole.py +3 -1
- secator/tasks/naabu.py +2 -1
- secator/tasks/nmap.py +14 -17
- secator/tasks/nuclei.py +4 -3
- secator/tasks/searchsploit.py +3 -2
- secator/tasks/subfinder.py +1 -0
- secator/tasks/wpscan.py +11 -13
- secator/utils.py +64 -82
- secator/utils_test.py +3 -2
- secator-0.3.6.dist-info/METADATA +411 -0
- secator-0.3.6.dist-info/RECORD +100 -0
- {secator-0.0.1.dist-info → secator-0.3.6.dist-info}/WHEEL +1 -2
- secator-0.0.1.dist-info/METADATA +0 -199
- secator-0.0.1.dist-info/RECORD +0 -114
- secator-0.0.1.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/inputs.py +0 -42
- tests/integration/outputs.py +0 -392
- tests/integration/test_scans.py +0 -82
- tests/integration/test_tasks.py +0 -103
- tests/integration/test_workflows.py +0 -163
- tests/performance/__init__.py +0 -0
- tests/performance/loadtester.py +0 -56
- tests/unit/__init__.py +0 -0
- tests/unit/test_celery.py +0 -39
- tests/unit/test_scans.py +0 -0
- tests/unit/test_serializers.py +0 -51
- tests/unit/test_tasks.py +0 -348
- tests/unit/test_workflows.py +0 -96
- {secator-0.0.1.dist-info → secator-0.3.6.dist-info}/entry_points.txt +0 -0
- {secator-0.0.1.dist-info → secator-0.3.6.dist-info/licenses}/LICENSE +0 -0
secator/runners/_base.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
|
+
import os
|
|
3
4
|
import sys
|
|
4
5
|
import uuid
|
|
5
6
|
from contextlib import nullcontext
|
|
@@ -7,18 +8,17 @@ from datetime import datetime
|
|
|
7
8
|
from time import sleep, time
|
|
8
9
|
|
|
9
10
|
import humanize
|
|
10
|
-
from celery.result import AsyncResult
|
|
11
11
|
from dotmap import DotMap
|
|
12
12
|
from rich.padding import Padding
|
|
13
13
|
from rich.panel import Panel
|
|
14
14
|
from rich.progress import Progress as RichProgress
|
|
15
15
|
from rich.progress import SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
16
16
|
|
|
17
|
-
from secator.definitions import DEBUG, DEFAULT_PROGRESS_UPDATE_FREQUENCY
|
|
17
|
+
from secator.definitions import DEBUG, DEFAULT_PROGRESS_UPDATE_FREQUENCY, REPORTS_FOLDER
|
|
18
18
|
from secator.output_types import OUTPUT_TYPES, OutputType, Progress
|
|
19
19
|
from secator.report import Report
|
|
20
20
|
from secator.rich import console, console_stdout
|
|
21
|
-
from secator.runners._helpers import (get_task_data, get_task_ids,
|
|
21
|
+
from secator.runners._helpers import (get_task_data, get_task_ids, get_task_folder_id,
|
|
22
22
|
process_extractor)
|
|
23
23
|
from secator.utils import (debug, import_dynamic, merge_opts, pluralize,
|
|
24
24
|
rich_to_ansi)
|
|
@@ -76,6 +76,9 @@ class Runner:
|
|
|
76
76
|
# Run hooks
|
|
77
77
|
enable_hooks = True
|
|
78
78
|
|
|
79
|
+
# Reports folder
|
|
80
|
+
reports_folder = None
|
|
81
|
+
|
|
79
82
|
def __init__(self, config, targets, results=[], run_opts={}, hooks={}, context={}):
|
|
80
83
|
self.config = config
|
|
81
84
|
self.name = run_opts.get('name', config.name)
|
|
@@ -103,7 +106,17 @@ class Runner:
|
|
|
103
106
|
self.context = context
|
|
104
107
|
self.delay = run_opts.get('delay', False)
|
|
105
108
|
self.uuids = []
|
|
106
|
-
self.
|
|
109
|
+
self.celery_result = None
|
|
110
|
+
|
|
111
|
+
# Determine report folder
|
|
112
|
+
default_reports_folder_base = f'{REPORTS_FOLDER}/{self.workspace_name}/{self.config.type}s'
|
|
113
|
+
_id = get_task_folder_id(default_reports_folder_base)
|
|
114
|
+
self.reports_folder = f'{default_reports_folder_base}/{_id}'
|
|
115
|
+
|
|
116
|
+
# Make reports folders
|
|
117
|
+
os.makedirs(self.reports_folder, exist_ok=True)
|
|
118
|
+
os.makedirs(f'{self.reports_folder}/.inputs', exist_ok=True)
|
|
119
|
+
os.makedirs(f'{self.reports_folder}/.outputs', exist_ok=True)
|
|
107
120
|
|
|
108
121
|
# Process input
|
|
109
122
|
self.input = targets
|
|
@@ -122,7 +135,8 @@ class Runner:
|
|
|
122
135
|
# Print options
|
|
123
136
|
self.print_start = self.run_opts.pop('print_start', False)
|
|
124
137
|
self.print_item = self.run_opts.pop('print_item', False)
|
|
125
|
-
self.print_line = self.run_opts.pop('print_line',
|
|
138
|
+
self.print_line = self.run_opts.pop('print_line', False)
|
|
139
|
+
self.print_errors = self.run_opts.pop('print_errors', True)
|
|
126
140
|
self.print_item_count = self.run_opts.pop('print_item_count', False)
|
|
127
141
|
self.print_cmd = self.run_opts.pop('print_cmd', False)
|
|
128
142
|
self.print_run_opts = self.run_opts.pop('print_run_opts', DEBUG > 1)
|
|
@@ -139,12 +153,26 @@ class Runner:
|
|
|
139
153
|
self.opts_to_print = {k: v for k, v in self.__dict__.items() if k.startswith('print_') if v}
|
|
140
154
|
|
|
141
155
|
# Hooks
|
|
156
|
+
self.raise_on_error = self.run_opts.get('raise_on_error', False)
|
|
142
157
|
self.hooks = {name: [] for name in HOOKS}
|
|
143
158
|
for key in self.hooks:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
|
|
160
|
+
# Register class specific hooks
|
|
161
|
+
class_hook = getattr(self, key, None)
|
|
162
|
+
if class_hook:
|
|
163
|
+
name = f'{self.__class__.__name__}.{key}'
|
|
164
|
+
fun = self.get_func_path(class_hook)
|
|
165
|
+
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'registered'}, sub='hooks', level=3)
|
|
166
|
+
self.hooks[key].append(class_hook)
|
|
167
|
+
|
|
168
|
+
# Register user hooks
|
|
169
|
+
user_hooks = hooks.get(self.__class__, {}).get(key, [])
|
|
170
|
+
user_hooks.extend(hooks.get(key, []))
|
|
171
|
+
for hook in user_hooks:
|
|
172
|
+
name = f'{self.__class__.__name__}.{key}'
|
|
173
|
+
fun = self.get_func_path(hook)
|
|
174
|
+
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'registered (user)'}, sub='hooks', level=3)
|
|
175
|
+
self.hooks[key].extend(user_hooks)
|
|
148
176
|
|
|
149
177
|
# Validators
|
|
150
178
|
self.validators = {name: [] for name in VALIDATORS}
|
|
@@ -159,6 +187,8 @@ class Runner:
|
|
|
159
187
|
self.has_children = self.run_opts.get('has_children', False)
|
|
160
188
|
self.chunk = self.run_opts.get('chunk', None)
|
|
161
189
|
self.chunk_count = self.run_opts.get('chunk_count', None)
|
|
190
|
+
self.unique_name = self.name.replace('/', '_')
|
|
191
|
+
self.unique_name = f'{self.unique_name}_{self.chunk}' if self.chunk else self.unique_name
|
|
162
192
|
self._set_print_prefix()
|
|
163
193
|
|
|
164
194
|
# Input post-process
|
|
@@ -234,7 +264,7 @@ class Runner:
|
|
|
234
264
|
|
|
235
265
|
elif item and isinstance(item, str):
|
|
236
266
|
if self.print_line:
|
|
237
|
-
self._print(item, out=sys.stderr)
|
|
267
|
+
self._print(item, out=sys.stderr, end='\n')
|
|
238
268
|
if not self.output_json:
|
|
239
269
|
self.results.append(item)
|
|
240
270
|
yield item
|
|
@@ -249,9 +279,9 @@ class Runner:
|
|
|
249
279
|
|
|
250
280
|
except KeyboardInterrupt:
|
|
251
281
|
self._print('Process was killed manually (CTRL+C / CTRL+X).', color='bold red', rich=True)
|
|
252
|
-
if self.
|
|
282
|
+
if self.celery_result:
|
|
253
283
|
self._print('Revoking remote Celery tasks ...', color='bold red', rich=True)
|
|
254
|
-
self.stop_live_tasks(self.
|
|
284
|
+
self.stop_live_tasks(self.celery_result)
|
|
255
285
|
|
|
256
286
|
# Filter results and log info
|
|
257
287
|
self.mark_duplicates()
|
|
@@ -260,9 +290,10 @@ class Runner:
|
|
|
260
290
|
self.run_hooks('on_end')
|
|
261
291
|
|
|
262
292
|
def mark_duplicates(self):
|
|
263
|
-
debug('duplicate check', id=self.config.name, sub='runner.mark_duplicates')
|
|
293
|
+
debug('running duplicate check', id=self.config.name, sub='runner.mark_duplicates')
|
|
294
|
+
dupe_count = 0
|
|
264
295
|
for item in self.results:
|
|
265
|
-
debug('duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=
|
|
296
|
+
debug('running duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=5)
|
|
266
297
|
others = [f for f in self.results if f == item and f._uuid != item._uuid]
|
|
267
298
|
if others:
|
|
268
299
|
main = max(item, *others)
|
|
@@ -282,13 +313,16 @@ class Runner:
|
|
|
282
313
|
if not dupe._duplicate:
|
|
283
314
|
debug(
|
|
284
315
|
'found new duplicate', obj=dupe.toDict(), obj_breaklines=True,
|
|
285
|
-
sub='runner.mark_duplicates', level=
|
|
316
|
+
sub='runner.mark_duplicates', level=5)
|
|
317
|
+
dupe_count += 1
|
|
286
318
|
dupe._duplicate = True
|
|
287
319
|
dupe = self.run_hooks('on_duplicate', dupe)
|
|
288
320
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
321
|
+
duplicates = [repr(i) for i in self.results if i._duplicate]
|
|
322
|
+
if duplicates:
|
|
323
|
+
duplicates_str = '\n\t'.join(duplicates)
|
|
324
|
+
debug(f'Duplicates ({dupe_count}):\n\t{duplicates_str}', sub='runner.mark_duplicates', level=5)
|
|
325
|
+
debug(f'duplicate check completed: {dupe_count} found', id=self.config.name, sub='runner.mark_duplicates')
|
|
292
326
|
|
|
293
327
|
def yielder(self):
|
|
294
328
|
raise NotImplementedError()
|
|
@@ -325,17 +359,24 @@ class Runner:
|
|
|
325
359
|
return result
|
|
326
360
|
for hook in self.hooks[hook_type]:
|
|
327
361
|
name = f'{self.__class__.__name__}.{hook_type}'
|
|
328
|
-
fun =
|
|
362
|
+
fun = self.get_func_path(hook)
|
|
329
363
|
try:
|
|
330
364
|
_id = self.context.get('task_id', '') or self.context.get('workflow_id', '') or self.context.get('scan_id', '')
|
|
331
365
|
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'started'}, id=_id, sub='hooks', level=3)
|
|
332
366
|
result = hook(self, *args)
|
|
367
|
+
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'ended'}, id=_id, sub='hooks', level=3)
|
|
333
368
|
except Exception as e:
|
|
334
|
-
self.
|
|
335
|
-
|
|
336
|
-
logger.exception(e)
|
|
369
|
+
if self.raise_on_error:
|
|
370
|
+
raise e
|
|
337
371
|
else:
|
|
338
|
-
|
|
372
|
+
if DEBUG > 1:
|
|
373
|
+
logger.exception(e)
|
|
374
|
+
else:
|
|
375
|
+
self._print(
|
|
376
|
+
f'{fun} failed: "{e.__class__.__name__}: {str(e)}". Skipping',
|
|
377
|
+
color='bold red',
|
|
378
|
+
rich=True)
|
|
379
|
+
self._print('Set DEBUG to > 1 to see the detailed exception.', color='dim red', rich=True)
|
|
339
380
|
return result
|
|
340
381
|
|
|
341
382
|
def run_validators(self, validator_type, *args):
|
|
@@ -444,15 +485,15 @@ class Runner:
|
|
|
444
485
|
# Log runner infos
|
|
445
486
|
if self.infos:
|
|
446
487
|
self._print(
|
|
447
|
-
f'
|
|
488
|
+
f':heavy_check_mark: [bold magenta]{self.config.name}[/] infos ({len(self.infos)}):',
|
|
448
489
|
color='bold green', rich=True)
|
|
449
490
|
for info in self.infos:
|
|
450
491
|
self._print(f' • {info}', color='bold green', rich=True)
|
|
451
492
|
|
|
452
493
|
# Log runner errors
|
|
453
|
-
if self.errors:
|
|
494
|
+
if self.errors and self.print_errors:
|
|
454
495
|
self._print(
|
|
455
|
-
f'
|
|
496
|
+
f':exclamation_mark:[bold magenta]{self.config.name}[/] errors ({len(self.errors)}):',
|
|
456
497
|
color='bold red', rich=True)
|
|
457
498
|
for error in self.errors:
|
|
458
499
|
self._print(f' • {error}', color='bold red', rich=True)
|
|
@@ -468,9 +509,9 @@ class Runner:
|
|
|
468
509
|
if self.print_item_count and not self.print_raw and not self.orig:
|
|
469
510
|
count_map = self._get_results_count()
|
|
470
511
|
if all(count == 0 for count in count_map.values()):
|
|
471
|
-
self._print(':
|
|
512
|
+
self._print(':exclamation_mark:Found 0 results.', color='bold red', rich=True)
|
|
472
513
|
else:
|
|
473
|
-
results_str = ':
|
|
514
|
+
results_str = ':heavy_check_mark: Found ' + ' and '.join([
|
|
474
515
|
f'{count} {pluralize(name) if count > 1 or count == 0 else name}'
|
|
475
516
|
for name, count in count_map.items()
|
|
476
517
|
]) + '.'
|
|
@@ -486,6 +527,7 @@ class Runner:
|
|
|
486
527
|
Yields:
|
|
487
528
|
dict: Subtasks state and results.
|
|
488
529
|
"""
|
|
530
|
+
from celery.result import AsyncResult
|
|
489
531
|
res = AsyncResult(result.id)
|
|
490
532
|
while True:
|
|
491
533
|
# Yield results
|
|
@@ -625,7 +667,6 @@ class Runner:
|
|
|
625
667
|
# continue
|
|
626
668
|
|
|
627
669
|
# Handle messages if any
|
|
628
|
-
# TODO: error handling should be moved to process_live_tasks
|
|
629
670
|
state = data['state']
|
|
630
671
|
error = data.get('error')
|
|
631
672
|
info = data.get('info')
|
|
@@ -711,7 +752,7 @@ class Runner:
|
|
|
711
752
|
break # found an item that fits
|
|
712
753
|
except (TypeError, KeyError) as e: # can't load using class
|
|
713
754
|
debug(
|
|
714
|
-
f'[dim red]Failed loading item as {klass.__name__}: {str(e)}.[/] [dim green]Continuing.[/]',
|
|
755
|
+
f'[dim red]Failed loading item as {klass.__name__}: {type(e).__name__}: {str(e)}.[/] [dim green]Continuing.[/]',
|
|
715
756
|
sub='klass.load',
|
|
716
757
|
level=5)
|
|
717
758
|
if DEBUG == 6:
|
|
@@ -725,12 +766,12 @@ class Runner:
|
|
|
725
766
|
|
|
726
767
|
return new_item
|
|
727
768
|
|
|
728
|
-
def _print(self, data, color=None, out=sys.stderr, rich=False):
|
|
769
|
+
def _print(self, data, color=None, out=sys.stderr, rich=False, end='\n'):
|
|
729
770
|
"""Print function.
|
|
730
771
|
|
|
731
772
|
Args:
|
|
732
773
|
data (str or dict): Input data.
|
|
733
|
-
color (str, Optional):
|
|
774
|
+
color (str, Optional): Rich color.
|
|
734
775
|
out (str, Optional): Output pipe (sys.stderr, sys.stdout, ...)
|
|
735
776
|
rich (bool, Optional): Force rich output.
|
|
736
777
|
"""
|
|
@@ -743,7 +784,7 @@ class Runner:
|
|
|
743
784
|
|
|
744
785
|
if self.sync or rich:
|
|
745
786
|
_console = console_stdout if out == sys.stdout else console
|
|
746
|
-
_console.print(data, highlight=False, style=color, soft_wrap=True)
|
|
787
|
+
_console.print(data, highlight=False, style=color, soft_wrap=True, end=end)
|
|
747
788
|
else:
|
|
748
789
|
print(data, file=out)
|
|
749
790
|
|
|
@@ -807,10 +848,12 @@ class Runner:
|
|
|
807
848
|
if not item._uuid:
|
|
808
849
|
item._uuid = str(uuid.uuid4())
|
|
809
850
|
|
|
810
|
-
if item._type == 'progress' and item._source == self.config.name
|
|
851
|
+
if item._type == 'progress' and item._source == self.config.name:
|
|
811
852
|
self.progress = item.percent
|
|
812
853
|
if self.last_updated_progress and (item._timestamp - self.last_updated_progress) < DEFAULT_PROGRESS_UPDATE_FREQUENCY:
|
|
813
854
|
return None
|
|
855
|
+
elif int(item.percent) in [0, 100]:
|
|
856
|
+
return None
|
|
814
857
|
else:
|
|
815
858
|
self.last_updated_progress = item._timestamp
|
|
816
859
|
|
|
@@ -831,3 +874,31 @@ class Runner:
|
|
|
831
874
|
elif isinstance(item, OutputType):
|
|
832
875
|
item = repr(item)
|
|
833
876
|
return item
|
|
877
|
+
|
|
878
|
+
@classmethod
|
|
879
|
+
def get_func_path(cls, func):
|
|
880
|
+
"""
|
|
881
|
+
Get the full symbolic path of a function or method, including staticmethods,
|
|
882
|
+
using function and method attributes.
|
|
883
|
+
|
|
884
|
+
Args:
|
|
885
|
+
func (function, method, or staticmethod): A function or method object.
|
|
886
|
+
"""
|
|
887
|
+
if hasattr(func, '__self__'):
|
|
888
|
+
if func.__self__ is not None:
|
|
889
|
+
# It's a method bound to an instance
|
|
890
|
+
class_name = func.__self__.__class__.__name__
|
|
891
|
+
return f"{func.__module__}.{class_name}.{func.__name__}"
|
|
892
|
+
else:
|
|
893
|
+
# It's a method bound to a class (class method)
|
|
894
|
+
class_name = func.__qualname__.rsplit('.', 1)[0]
|
|
895
|
+
return f"{func.__module__}.{class_name}.{func.__name__}"
|
|
896
|
+
else:
|
|
897
|
+
# Handle static and regular functions
|
|
898
|
+
if '.' in func.__qualname__:
|
|
899
|
+
# Static method or a function defined inside a class
|
|
900
|
+
class_name, func_name = func.__qualname__.rsplit('.', 1)
|
|
901
|
+
return f"{func.__module__}.{class_name}.{func_name}"
|
|
902
|
+
else:
|
|
903
|
+
# Regular function not attached to a class
|
|
904
|
+
return f"{func.__module__}.{func.__name__}"
|
secator/runners/_helpers.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
from rich.prompt import Confirm
|
|
1
|
+
import os
|
|
3
2
|
|
|
4
3
|
from secator.utils import deduplicate
|
|
5
4
|
|
|
@@ -77,6 +76,7 @@ def get_task_ids(result, ids=[]):
|
|
|
77
76
|
result (Union[AsyncResult, GroupResult]): Celery result object.
|
|
78
77
|
ids (list): List of ids.
|
|
79
78
|
"""
|
|
79
|
+
from celery.result import AsyncResult, GroupResult
|
|
80
80
|
if result is None:
|
|
81
81
|
return
|
|
82
82
|
|
|
@@ -105,6 +105,7 @@ def get_task_data(task_id):
|
|
|
105
105
|
Returns:
|
|
106
106
|
dict: Task info (id, name, state, results, chunk_info, count, error, ready).
|
|
107
107
|
"""
|
|
108
|
+
from celery.result import AsyncResult
|
|
108
109
|
res = AsyncResult(task_id)
|
|
109
110
|
if not (res and res.args and len(res.args) > 1):
|
|
110
111
|
return
|
|
@@ -136,18 +137,18 @@ def get_task_data(task_id):
|
|
|
136
137
|
return data
|
|
137
138
|
|
|
138
139
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return
|
|
140
|
+
def get_task_folder_id(path):
|
|
141
|
+
names = []
|
|
142
|
+
if not os.path.exists(path):
|
|
143
|
+
return 0
|
|
144
|
+
for f in os.scandir(path):
|
|
145
|
+
if f.is_dir():
|
|
146
|
+
try:
|
|
147
|
+
int(f.name)
|
|
148
|
+
names.append(int(f.name))
|
|
149
|
+
except ValueError:
|
|
150
|
+
continue
|
|
151
|
+
names.sort()
|
|
152
|
+
if names:
|
|
153
|
+
return names[-1] + 1
|
|
154
|
+
return 0
|
secator/runners/command.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import getpass
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
3
4
|
import re
|
|
@@ -7,19 +8,17 @@ import sys
|
|
|
7
8
|
|
|
8
9
|
from time import sleep
|
|
9
10
|
|
|
10
|
-
from celery.result import AsyncResult
|
|
11
11
|
from fp.fp import FreeProxy
|
|
12
12
|
|
|
13
13
|
from secator.config import ConfigLoader
|
|
14
|
-
from secator.definitions import (
|
|
14
|
+
from secator.definitions import (DEFAULT_HTTP_PROXY,
|
|
15
15
|
DEFAULT_FREEPROXY_TIMEOUT,
|
|
16
16
|
DEFAULT_PROXYCHAINS_COMMAND,
|
|
17
17
|
DEFAULT_SOCKS5_PROXY, OPT_NOT_SUPPORTED,
|
|
18
|
-
OPT_PIPE_INPUT,
|
|
19
|
-
from secator.rich import console
|
|
18
|
+
OPT_PIPE_INPUT, DEFAULT_INPUT_CHUNK_SIZE)
|
|
20
19
|
from secator.runners import Runner
|
|
21
20
|
from secator.serializers import JSONSerializer
|
|
22
|
-
from secator.utils import
|
|
21
|
+
from secator.utils import debug
|
|
23
22
|
|
|
24
23
|
# from rich.markup import escape
|
|
25
24
|
# from rich.text import Text
|
|
@@ -78,8 +77,12 @@ class Command(Runner):
|
|
|
78
77
|
# Flag to enable output JSON
|
|
79
78
|
json_flag = None
|
|
80
79
|
|
|
81
|
-
#
|
|
80
|
+
# Flag to show version
|
|
81
|
+
version_flag = None
|
|
82
|
+
|
|
83
|
+
# Install
|
|
82
84
|
install_cmd = None
|
|
85
|
+
install_github_handle = None
|
|
83
86
|
|
|
84
87
|
# Serializer
|
|
85
88
|
item_loader = None
|
|
@@ -97,9 +100,6 @@ class Command(Runner):
|
|
|
97
100
|
# Output
|
|
98
101
|
output = ''
|
|
99
102
|
|
|
100
|
-
# Default run opts
|
|
101
|
-
default_run_opts = {}
|
|
102
|
-
|
|
103
103
|
# Proxy options
|
|
104
104
|
proxychains = False
|
|
105
105
|
proxy_socks5 = False
|
|
@@ -134,6 +134,9 @@ class Command(Runner):
|
|
|
134
134
|
# No capturing of stdout / stderr.
|
|
135
135
|
self.no_capture = self.run_opts.get('no_capture', False)
|
|
136
136
|
|
|
137
|
+
# No processing of output lines.
|
|
138
|
+
self.no_process = self.run_opts.get('no_process', False)
|
|
139
|
+
|
|
137
140
|
# Proxy config (global)
|
|
138
141
|
self.proxy = self.run_opts.pop('proxy', False)
|
|
139
142
|
self.configure_proxy()
|
|
@@ -155,7 +158,7 @@ class Command(Runner):
|
|
|
155
158
|
if self.print_cmd and not self.has_children:
|
|
156
159
|
if self.sync and self.description:
|
|
157
160
|
self._print(f'\n:wrench: {self.description} ...', color='bold gold3', rich=True)
|
|
158
|
-
self._print(self.cmd, color='bold cyan', rich=True)
|
|
161
|
+
self._print(self.cmd.replace('[', '\\['), color='bold cyan', rich=True)
|
|
159
162
|
|
|
160
163
|
# Print built input
|
|
161
164
|
if self.print_input_file and self.input_path:
|
|
@@ -214,16 +217,6 @@ class Command(Runner):
|
|
|
214
217
|
from secator.celery import run_command
|
|
215
218
|
return run_command.si(results, cls.__name__, *args, opts=kwargs).set(queue=cls.profile)
|
|
216
219
|
|
|
217
|
-
@classmethod
|
|
218
|
-
def poll(cls, result):
|
|
219
|
-
# TODO: Move this to TaskBase
|
|
220
|
-
while not result.ready():
|
|
221
|
-
data = AsyncResult(result.id).info
|
|
222
|
-
if DEBUG > 1 and isinstance(data, dict):
|
|
223
|
-
print(data)
|
|
224
|
-
sleep(1)
|
|
225
|
-
return result.get()
|
|
226
|
-
|
|
227
220
|
def get_opt_value(self, opt_name):
|
|
228
221
|
return Command._get_opt_value(
|
|
229
222
|
self.run_opts,
|
|
@@ -260,35 +253,30 @@ class Command(Runner):
|
|
|
260
253
|
#---------------#
|
|
261
254
|
|
|
262
255
|
@classmethod
|
|
263
|
-
def
|
|
264
|
-
"""
|
|
265
|
-
console.log(f':pill: Installing {cls.__name__}...', style='bold yellow')
|
|
266
|
-
if not cls.install_cmd:
|
|
267
|
-
console.log(f'{cls.__name__} install is not supported yet. Please install it manually.', style='bold red')
|
|
268
|
-
return
|
|
269
|
-
ret = cls.run_command(
|
|
270
|
-
cls.install_cmd,
|
|
271
|
-
name=cls.__name__,
|
|
272
|
-
print_cmd=True,
|
|
273
|
-
print_line=True,
|
|
274
|
-
cls_attributes={'shell': True}
|
|
275
|
-
)
|
|
276
|
-
if ret.return_code != 0:
|
|
277
|
-
console.log(f'Failed to install {cls.__name__}.', style='bold red')
|
|
278
|
-
else:
|
|
279
|
-
console.log(f'{cls.__name__} installed successfully !', style='bold green')
|
|
280
|
-
return ret
|
|
256
|
+
def execute(cls, cmd, name=None, cls_attributes={}, **kwargs):
|
|
257
|
+
"""Execute an ad-hoc command.
|
|
281
258
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
259
|
+
Can be used without defining an inherited class to run a command, while still enjoying all the good stuff in
|
|
260
|
+
this class.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
cls (object): Class.
|
|
264
|
+
cmd (str): Command.
|
|
265
|
+
name (str): Printed name.
|
|
266
|
+
cls_attributes (dict): Class attributes.
|
|
267
|
+
kwargs (dict): Options.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
secator.runners.Command: instance of the Command.
|
|
286
271
|
"""
|
|
272
|
+
name = name or cmd.split(' ')[0]
|
|
273
|
+
kwargs['no_process'] = True
|
|
274
|
+
kwargs['print_cmd'] = not kwargs.get('quiet', False)
|
|
275
|
+
kwargs['print_item'] = not kwargs.get('quiet', False)
|
|
276
|
+
kwargs['print_line'] = not kwargs.get('quiet', False)
|
|
287
277
|
cmd_instance = type(name, (Command,), {'cmd': cmd})(**kwargs)
|
|
288
278
|
for k, v in cls_attributes.items():
|
|
289
279
|
setattr(cmd_instance, k, v)
|
|
290
|
-
cmd_instance.print_line = not kwargs.get('quiet', False)
|
|
291
|
-
cmd_instance.print_item = not kwargs.get('quiet', False)
|
|
292
280
|
cmd_instance.run()
|
|
293
281
|
return cmd_instance
|
|
294
282
|
|
|
@@ -352,6 +340,9 @@ class Command(Runner):
|
|
|
352
340
|
# Callback before running command
|
|
353
341
|
self.run_hooks('on_start')
|
|
354
342
|
|
|
343
|
+
# Check for sudo requirements and prepare the password if needed
|
|
344
|
+
sudo_password = self._prompt_sudo(self.cmd)
|
|
345
|
+
|
|
355
346
|
# Prepare cmds
|
|
356
347
|
command = self.cmd if self.shell else shlex.split(self.cmd)
|
|
357
348
|
|
|
@@ -365,6 +356,7 @@ class Command(Runner):
|
|
|
365
356
|
env.update(self.env)
|
|
366
357
|
process = subprocess.Popen(
|
|
367
358
|
command,
|
|
359
|
+
stdin=subprocess.PIPE if sudo_password else None,
|
|
368
360
|
stdout=sys.stdout if self.no_capture else subprocess.PIPE,
|
|
369
361
|
stderr=sys.stderr if self.no_capture else subprocess.STDOUT,
|
|
370
362
|
universal_newlines=True,
|
|
@@ -372,11 +364,16 @@ class Command(Runner):
|
|
|
372
364
|
env=env,
|
|
373
365
|
cwd=self.cwd)
|
|
374
366
|
|
|
367
|
+
# If sudo password is provided, send it to stdin
|
|
368
|
+
if sudo_password:
|
|
369
|
+
process.stdin.write(f"{sudo_password}\n")
|
|
370
|
+
process.stdin.flush()
|
|
371
|
+
|
|
375
372
|
except FileNotFoundError as e:
|
|
376
373
|
if self.config.name in str(e):
|
|
377
374
|
error = 'Executable not found.'
|
|
378
375
|
if self.install_cmd:
|
|
379
|
-
error += f' Install it with `secator
|
|
376
|
+
error += f' Install it with `secator install tools {self.config.name}`.'
|
|
380
377
|
else:
|
|
381
378
|
error = str(e)
|
|
382
379
|
celery_id = self.context.get('celery_id', '')
|
|
@@ -384,8 +381,6 @@ class Command(Runner):
|
|
|
384
381
|
error += f' [{celery_id}]'
|
|
385
382
|
self.errors.append(error)
|
|
386
383
|
self.return_code = 1
|
|
387
|
-
if error:
|
|
388
|
-
self._print(error, color='bold red')
|
|
389
384
|
return
|
|
390
385
|
|
|
391
386
|
try:
|
|
@@ -400,8 +395,11 @@ class Command(Runner):
|
|
|
400
395
|
if not line:
|
|
401
396
|
break
|
|
402
397
|
|
|
403
|
-
# Strip line
|
|
404
|
-
line = line.
|
|
398
|
+
# Strip line endings
|
|
399
|
+
line = line.rstrip()
|
|
400
|
+
if self.no_process:
|
|
401
|
+
yield line
|
|
402
|
+
continue
|
|
405
403
|
|
|
406
404
|
# Some commands output ANSI text, so we need to remove those ANSI chars
|
|
407
405
|
if self.encoding == 'ansi':
|
|
@@ -420,7 +418,7 @@ class Command(Runner):
|
|
|
420
418
|
items = self.run_item_loaders(line)
|
|
421
419
|
|
|
422
420
|
# Yield line if no items parsed
|
|
423
|
-
if not items
|
|
421
|
+
if not items:
|
|
424
422
|
yield line
|
|
425
423
|
|
|
426
424
|
# Turn results into list if not already a list
|
|
@@ -453,6 +451,48 @@ class Command(Runner):
|
|
|
453
451
|
items.extend(result)
|
|
454
452
|
return items
|
|
455
453
|
|
|
454
|
+
def _prompt_sudo(self, command):
|
|
455
|
+
"""
|
|
456
|
+
Checks if the command requires sudo and prompts for the password if necessary.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
command (str): The initial command to be executed.
|
|
460
|
+
|
|
461
|
+
Returns:
|
|
462
|
+
str or None: The sudo password if required; otherwise, None.
|
|
463
|
+
"""
|
|
464
|
+
sudo_password = None
|
|
465
|
+
|
|
466
|
+
# Check if sudo is required by the command
|
|
467
|
+
if not re.search(r'\bsudo\b', command):
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
# Check if sudo can be executed without a password
|
|
471
|
+
if subprocess.run(['sudo', '-n', 'true'], capture_output=True).returncode == 0:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
# Check if we have a tty
|
|
475
|
+
if not os.isatty(sys.stdin.fileno()):
|
|
476
|
+
self._print("No TTY detected. Sudo password prompt requires a TTY to proceed.", color='bold red')
|
|
477
|
+
sys.exit(1)
|
|
478
|
+
|
|
479
|
+
# If not, prompt the user for a password
|
|
480
|
+
self._print('[bold red]Please enter sudo password to continue.[/]')
|
|
481
|
+
for _ in range(3):
|
|
482
|
+
self._print('\[sudo] password: ')
|
|
483
|
+
sudo_password = getpass.getpass()
|
|
484
|
+
result = subprocess.run(
|
|
485
|
+
['sudo', '-S', '-p', '', 'true'],
|
|
486
|
+
input=sudo_password + "\n",
|
|
487
|
+
text=True,
|
|
488
|
+
capture_output=True
|
|
489
|
+
)
|
|
490
|
+
if result.returncode == 0:
|
|
491
|
+
return sudo_password # Password is correct
|
|
492
|
+
self._print("Sorry, try again.")
|
|
493
|
+
self._print("Sudo password verification failed after 3 attempts.")
|
|
494
|
+
return None
|
|
495
|
+
|
|
456
496
|
def _wait_for_end(self, process):
|
|
457
497
|
"""Wait for process to finish and process output and return code."""
|
|
458
498
|
process.wait()
|
|
@@ -469,11 +509,9 @@ class Command(Runner):
|
|
|
469
509
|
|
|
470
510
|
if self.return_code == -2 or self.killed:
|
|
471
511
|
error = 'Process was killed manually (CTRL+C / CTRL+X)'
|
|
472
|
-
self._print(error, color='bold red')
|
|
473
512
|
self.errors.append(error)
|
|
474
513
|
elif self.return_code != 0:
|
|
475
514
|
error = f'Command failed with return code {self.return_code}.'
|
|
476
|
-
self._print(error, color='bold red')
|
|
477
515
|
self.errors.append(error)
|
|
478
516
|
|
|
479
517
|
@staticmethod
|
|
@@ -603,9 +641,7 @@ class Command(Runner):
|
|
|
603
641
|
# If input is a list and the tool has input_flag set to OPT_PIPE_INPUT, use cat-piped input.
|
|
604
642
|
# Otherwise pass the file path to the tool.
|
|
605
643
|
if isinstance(input, list):
|
|
606
|
-
|
|
607
|
-
cmd_name = cmd.split(' ')[0].split('/')[-1]
|
|
608
|
-
fpath = f'{DATA_FOLDER}/{cmd_name}_{timestr}.txt'
|
|
644
|
+
fpath = f'{self.reports_folder}/.inputs/{self.unique_name}.txt'
|
|
609
645
|
|
|
610
646
|
# Write the input to a file
|
|
611
647
|
with open(fpath, 'w') as f:
|
secator/runners/scan.py
CHANGED
|
@@ -42,13 +42,14 @@ class Scan(Runner):
|
|
|
42
42
|
console.log(f'No targets were specified for workflow {name}. Skipping.')
|
|
43
43
|
continue
|
|
44
44
|
|
|
45
|
-
# Workflow
|
|
45
|
+
# Workflow opts
|
|
46
46
|
run_opts = self.run_opts.copy()
|
|
47
47
|
fmt_opts = {
|
|
48
48
|
'json': run_opts.get('json', False),
|
|
49
49
|
'print_item': False,
|
|
50
50
|
'print_start': True,
|
|
51
51
|
'print_run_summary': True,
|
|
52
|
+
'print_progress': self.sync
|
|
52
53
|
}
|
|
53
54
|
run_opts.update(fmt_opts)
|
|
54
55
|
|
secator/runners/task.py
CHANGED
|
@@ -24,7 +24,7 @@ class Task(Runner):
|
|
|
24
24
|
# Get task class
|
|
25
25
|
task_cls = Task.get_task_class(self.config.name)
|
|
26
26
|
|
|
27
|
-
#
|
|
27
|
+
# Run opts
|
|
28
28
|
run_opts = self.run_opts.copy()
|
|
29
29
|
run_opts.pop('output', None)
|
|
30
30
|
dry_run = run_opts.get('show', False)
|
|
@@ -39,7 +39,8 @@ class Task(Runner):
|
|
|
39
39
|
'print_input_file': DEBUG > 0,
|
|
40
40
|
'print_item': True,
|
|
41
41
|
'print_item_count': not self.sync and not dry_run,
|
|
42
|
-
'print_line':
|
|
42
|
+
'print_line': True
|
|
43
|
+
# 'print_line': self.sync and not self.output_quiet,
|
|
43
44
|
}
|
|
44
45
|
# self.print_item = not self.sync # enable print_item for base Task only if running remote
|
|
45
46
|
run_opts.update(fmt_opts)
|
|
@@ -58,9 +59,9 @@ class Task(Runner):
|
|
|
58
59
|
if dry_run: # don't run
|
|
59
60
|
return
|
|
60
61
|
else:
|
|
61
|
-
|
|
62
|
+
self.celery_result = task_cls.delay(self.targets, **run_opts)
|
|
62
63
|
task = self.process_live_tasks(
|
|
63
|
-
|
|
64
|
+
self.celery_result,
|
|
64
65
|
description=False,
|
|
65
66
|
results_only=True,
|
|
66
67
|
print_remote_status=self.print_remote_status)
|