secator 0.0.1__py3-none-any.whl → 0.3.5__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 +8 -68
- secator/cli.py +631 -274
- secator/decorators.py +42 -6
- 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 +106 -34
- secator/runners/_helpers.py +18 -17
- secator/runners/command.py +91 -55
- secator/runners/scan.py +3 -1
- secator/runners/task.py +6 -4
- secator/runners/workflow.py +13 -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 +4 -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.5.dist-info/METADATA +411 -0
- secator-0.3.5.dist-info/RECORD +100 -0
- {secator-0.0.1.dist-info → secator-0.3.5.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.5.dist-info}/entry_points.txt +0 -0
- {secator-0.0.1.dist-info → secator-0.3.5.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,18 @@ 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
|
+
default_report_folder = f'{default_reports_folder_base}/{_id}'
|
|
115
|
+
self.reports_folder = run_opts.get('reports_folder') or default_report_folder
|
|
116
|
+
|
|
117
|
+
# Make reports folders
|
|
118
|
+
os.makedirs(self.reports_folder, exist_ok=True)
|
|
119
|
+
os.makedirs(f'{self.reports_folder}/.inputs', exist_ok=True)
|
|
120
|
+
os.makedirs(f'{self.reports_folder}/.outputs', exist_ok=True)
|
|
107
121
|
|
|
108
122
|
# Process input
|
|
109
123
|
self.input = targets
|
|
@@ -122,7 +136,8 @@ class Runner:
|
|
|
122
136
|
# Print options
|
|
123
137
|
self.print_start = self.run_opts.pop('print_start', False)
|
|
124
138
|
self.print_item = self.run_opts.pop('print_item', False)
|
|
125
|
-
self.print_line = self.run_opts.pop('print_line',
|
|
139
|
+
self.print_line = self.run_opts.pop('print_line', False)
|
|
140
|
+
self.print_errors = self.run_opts.pop('print_errors', True)
|
|
126
141
|
self.print_item_count = self.run_opts.pop('print_item_count', False)
|
|
127
142
|
self.print_cmd = self.run_opts.pop('print_cmd', False)
|
|
128
143
|
self.print_run_opts = self.run_opts.pop('print_run_opts', DEBUG > 1)
|
|
@@ -139,12 +154,26 @@ class Runner:
|
|
|
139
154
|
self.opts_to_print = {k: v for k, v in self.__dict__.items() if k.startswith('print_') if v}
|
|
140
155
|
|
|
141
156
|
# Hooks
|
|
157
|
+
self.raise_on_error = self.run_opts.get('raise_on_error', False)
|
|
142
158
|
self.hooks = {name: [] for name in HOOKS}
|
|
143
159
|
for key in self.hooks:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
160
|
+
|
|
161
|
+
# Register class specific hooks
|
|
162
|
+
class_hook = getattr(self, key, None)
|
|
163
|
+
if class_hook:
|
|
164
|
+
name = f'{self.__class__.__name__}.{key}'
|
|
165
|
+
fun = self.get_func_path(class_hook)
|
|
166
|
+
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'registered'}, sub='hooks', level=3)
|
|
167
|
+
self.hooks[key].append(class_hook)
|
|
168
|
+
|
|
169
|
+
# Register user hooks
|
|
170
|
+
user_hooks = hooks.get(self.__class__, {}).get(key, [])
|
|
171
|
+
user_hooks.extend(hooks.get(key, []))
|
|
172
|
+
for hook in user_hooks:
|
|
173
|
+
name = f'{self.__class__.__name__}.{key}'
|
|
174
|
+
fun = self.get_func_path(hook)
|
|
175
|
+
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'registered (user)'}, sub='hooks', level=3)
|
|
176
|
+
self.hooks[key].extend(user_hooks)
|
|
148
177
|
|
|
149
178
|
# Validators
|
|
150
179
|
self.validators = {name: [] for name in VALIDATORS}
|
|
@@ -159,6 +188,8 @@ class Runner:
|
|
|
159
188
|
self.has_children = self.run_opts.get('has_children', False)
|
|
160
189
|
self.chunk = self.run_opts.get('chunk', None)
|
|
161
190
|
self.chunk_count = self.run_opts.get('chunk_count', None)
|
|
191
|
+
self.unique_name = self.name.replace('/', '_')
|
|
192
|
+
self.unique_name = f'{self.unique_name}_{self.chunk}' if self.chunk else self.unique_name
|
|
162
193
|
self._set_print_prefix()
|
|
163
194
|
|
|
164
195
|
# Input post-process
|
|
@@ -234,7 +265,7 @@ class Runner:
|
|
|
234
265
|
|
|
235
266
|
elif item and isinstance(item, str):
|
|
236
267
|
if self.print_line:
|
|
237
|
-
self._print(item, out=sys.stderr)
|
|
268
|
+
self._print(item, out=sys.stderr, end='\n')
|
|
238
269
|
if not self.output_json:
|
|
239
270
|
self.results.append(item)
|
|
240
271
|
yield item
|
|
@@ -249,9 +280,9 @@ class Runner:
|
|
|
249
280
|
|
|
250
281
|
except KeyboardInterrupt:
|
|
251
282
|
self._print('Process was killed manually (CTRL+C / CTRL+X).', color='bold red', rich=True)
|
|
252
|
-
if self.
|
|
283
|
+
if self.celery_result:
|
|
253
284
|
self._print('Revoking remote Celery tasks ...', color='bold red', rich=True)
|
|
254
|
-
self.stop_live_tasks(self.
|
|
285
|
+
self.stop_live_tasks(self.celery_result)
|
|
255
286
|
|
|
256
287
|
# Filter results and log info
|
|
257
288
|
self.mark_duplicates()
|
|
@@ -260,9 +291,10 @@ class Runner:
|
|
|
260
291
|
self.run_hooks('on_end')
|
|
261
292
|
|
|
262
293
|
def mark_duplicates(self):
|
|
263
|
-
debug('duplicate check', id=self.config.name, sub='runner.mark_duplicates')
|
|
294
|
+
debug('running duplicate check', id=self.config.name, sub='runner.mark_duplicates')
|
|
295
|
+
dupe_count = 0
|
|
264
296
|
for item in self.results:
|
|
265
|
-
debug('duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=
|
|
297
|
+
debug('running duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=5)
|
|
266
298
|
others = [f for f in self.results if f == item and f._uuid != item._uuid]
|
|
267
299
|
if others:
|
|
268
300
|
main = max(item, *others)
|
|
@@ -282,13 +314,16 @@ class Runner:
|
|
|
282
314
|
if not dupe._duplicate:
|
|
283
315
|
debug(
|
|
284
316
|
'found new duplicate', obj=dupe.toDict(), obj_breaklines=True,
|
|
285
|
-
sub='runner.mark_duplicates', level=
|
|
317
|
+
sub='runner.mark_duplicates', level=5)
|
|
318
|
+
dupe_count += 1
|
|
286
319
|
dupe._duplicate = True
|
|
287
320
|
dupe = self.run_hooks('on_duplicate', dupe)
|
|
288
321
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
322
|
+
duplicates = [repr(i) for i in self.results if i._duplicate]
|
|
323
|
+
if duplicates:
|
|
324
|
+
duplicates_str = '\n\t'.join(duplicates)
|
|
325
|
+
debug(f'Duplicates ({dupe_count}):\n\t{duplicates_str}', sub='runner.mark_duplicates', level=5)
|
|
326
|
+
debug(f'duplicate check completed: {dupe_count} found', id=self.config.name, sub='runner.mark_duplicates')
|
|
292
327
|
|
|
293
328
|
def yielder(self):
|
|
294
329
|
raise NotImplementedError()
|
|
@@ -325,17 +360,24 @@ class Runner:
|
|
|
325
360
|
return result
|
|
326
361
|
for hook in self.hooks[hook_type]:
|
|
327
362
|
name = f'{self.__class__.__name__}.{hook_type}'
|
|
328
|
-
fun =
|
|
363
|
+
fun = self.get_func_path(hook)
|
|
329
364
|
try:
|
|
330
365
|
_id = self.context.get('task_id', '') or self.context.get('workflow_id', '') or self.context.get('scan_id', '')
|
|
331
366
|
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'started'}, id=_id, sub='hooks', level=3)
|
|
332
367
|
result = hook(self, *args)
|
|
368
|
+
debug('', obj={name + ' [dim yellow]->[/] ' + fun: 'ended'}, id=_id, sub='hooks', level=3)
|
|
333
369
|
except Exception as e:
|
|
334
|
-
self.
|
|
335
|
-
|
|
336
|
-
logger.exception(e)
|
|
370
|
+
if self.raise_on_error:
|
|
371
|
+
raise e
|
|
337
372
|
else:
|
|
338
|
-
|
|
373
|
+
if DEBUG > 1:
|
|
374
|
+
logger.exception(e)
|
|
375
|
+
else:
|
|
376
|
+
self._print(
|
|
377
|
+
f'{fun} failed: "{e.__class__.__name__}: {str(e)}". Skipping',
|
|
378
|
+
color='bold red',
|
|
379
|
+
rich=True)
|
|
380
|
+
self._print('Set DEBUG to > 1 to see the detailed exception.', color='dim red', rich=True)
|
|
339
381
|
return result
|
|
340
382
|
|
|
341
383
|
def run_validators(self, validator_type, *args):
|
|
@@ -444,15 +486,15 @@ class Runner:
|
|
|
444
486
|
# Log runner infos
|
|
445
487
|
if self.infos:
|
|
446
488
|
self._print(
|
|
447
|
-
f'
|
|
489
|
+
f':heavy_check_mark: [bold magenta]{self.config.name}[/] infos ({len(self.infos)}):',
|
|
448
490
|
color='bold green', rich=True)
|
|
449
491
|
for info in self.infos:
|
|
450
492
|
self._print(f' • {info}', color='bold green', rich=True)
|
|
451
493
|
|
|
452
494
|
# Log runner errors
|
|
453
|
-
if self.errors:
|
|
495
|
+
if self.errors and self.print_errors:
|
|
454
496
|
self._print(
|
|
455
|
-
f'
|
|
497
|
+
f':exclamation_mark:[bold magenta]{self.config.name}[/] errors ({len(self.errors)}):',
|
|
456
498
|
color='bold red', rich=True)
|
|
457
499
|
for error in self.errors:
|
|
458
500
|
self._print(f' • {error}', color='bold red', rich=True)
|
|
@@ -468,9 +510,9 @@ class Runner:
|
|
|
468
510
|
if self.print_item_count and not self.print_raw and not self.orig:
|
|
469
511
|
count_map = self._get_results_count()
|
|
470
512
|
if all(count == 0 for count in count_map.values()):
|
|
471
|
-
self._print(':
|
|
513
|
+
self._print(':exclamation_mark:Found 0 results.', color='bold red', rich=True)
|
|
472
514
|
else:
|
|
473
|
-
results_str = ':
|
|
515
|
+
results_str = ':heavy_check_mark: Found ' + ' and '.join([
|
|
474
516
|
f'{count} {pluralize(name) if count > 1 or count == 0 else name}'
|
|
475
517
|
for name, count in count_map.items()
|
|
476
518
|
]) + '.'
|
|
@@ -486,6 +528,7 @@ class Runner:
|
|
|
486
528
|
Yields:
|
|
487
529
|
dict: Subtasks state and results.
|
|
488
530
|
"""
|
|
531
|
+
from celery.result import AsyncResult
|
|
489
532
|
res = AsyncResult(result.id)
|
|
490
533
|
while True:
|
|
491
534
|
# Yield results
|
|
@@ -625,7 +668,6 @@ class Runner:
|
|
|
625
668
|
# continue
|
|
626
669
|
|
|
627
670
|
# Handle messages if any
|
|
628
|
-
# TODO: error handling should be moved to process_live_tasks
|
|
629
671
|
state = data['state']
|
|
630
672
|
error = data.get('error')
|
|
631
673
|
info = data.get('info')
|
|
@@ -711,7 +753,7 @@ class Runner:
|
|
|
711
753
|
break # found an item that fits
|
|
712
754
|
except (TypeError, KeyError) as e: # can't load using class
|
|
713
755
|
debug(
|
|
714
|
-
f'[dim red]Failed loading item as {klass.__name__}: {str(e)}.[/] [dim green]Continuing.[/]',
|
|
756
|
+
f'[dim red]Failed loading item as {klass.__name__}: {type(e).__name__}: {str(e)}.[/] [dim green]Continuing.[/]',
|
|
715
757
|
sub='klass.load',
|
|
716
758
|
level=5)
|
|
717
759
|
if DEBUG == 6:
|
|
@@ -725,12 +767,12 @@ class Runner:
|
|
|
725
767
|
|
|
726
768
|
return new_item
|
|
727
769
|
|
|
728
|
-
def _print(self, data, color=None, out=sys.stderr, rich=False):
|
|
770
|
+
def _print(self, data, color=None, out=sys.stderr, rich=False, end='\n'):
|
|
729
771
|
"""Print function.
|
|
730
772
|
|
|
731
773
|
Args:
|
|
732
774
|
data (str or dict): Input data.
|
|
733
|
-
color (str, Optional):
|
|
775
|
+
color (str, Optional): Rich color.
|
|
734
776
|
out (str, Optional): Output pipe (sys.stderr, sys.stdout, ...)
|
|
735
777
|
rich (bool, Optional): Force rich output.
|
|
736
778
|
"""
|
|
@@ -743,7 +785,7 @@ class Runner:
|
|
|
743
785
|
|
|
744
786
|
if self.sync or rich:
|
|
745
787
|
_console = console_stdout if out == sys.stdout else console
|
|
746
|
-
_console.print(data, highlight=False, style=color, soft_wrap=True)
|
|
788
|
+
_console.print(data, highlight=False, style=color, soft_wrap=True, end=end)
|
|
747
789
|
else:
|
|
748
790
|
print(data, file=out)
|
|
749
791
|
|
|
@@ -807,10 +849,12 @@ class Runner:
|
|
|
807
849
|
if not item._uuid:
|
|
808
850
|
item._uuid = str(uuid.uuid4())
|
|
809
851
|
|
|
810
|
-
if item._type == 'progress' and item._source == self.config.name
|
|
852
|
+
if item._type == 'progress' and item._source == self.config.name:
|
|
811
853
|
self.progress = item.percent
|
|
812
854
|
if self.last_updated_progress and (item._timestamp - self.last_updated_progress) < DEFAULT_PROGRESS_UPDATE_FREQUENCY:
|
|
813
855
|
return None
|
|
856
|
+
elif int(item.percent) in [0, 100]:
|
|
857
|
+
return None
|
|
814
858
|
else:
|
|
815
859
|
self.last_updated_progress = item._timestamp
|
|
816
860
|
|
|
@@ -831,3 +875,31 @@ class Runner:
|
|
|
831
875
|
elif isinstance(item, OutputType):
|
|
832
876
|
item = repr(item)
|
|
833
877
|
return item
|
|
878
|
+
|
|
879
|
+
@classmethod
|
|
880
|
+
def get_func_path(cls, func):
|
|
881
|
+
"""
|
|
882
|
+
Get the full symbolic path of a function or method, including staticmethods,
|
|
883
|
+
using function and method attributes.
|
|
884
|
+
|
|
885
|
+
Args:
|
|
886
|
+
func (function, method, or staticmethod): A function or method object.
|
|
887
|
+
"""
|
|
888
|
+
if hasattr(func, '__self__'):
|
|
889
|
+
if func.__self__ is not None:
|
|
890
|
+
# It's a method bound to an instance
|
|
891
|
+
class_name = func.__self__.__class__.__name__
|
|
892
|
+
return f"{func.__module__}.{class_name}.{func.__name__}"
|
|
893
|
+
else:
|
|
894
|
+
# It's a method bound to a class (class method)
|
|
895
|
+
class_name = func.__qualname__.rsplit('.', 1)[0]
|
|
896
|
+
return f"{func.__module__}.{class_name}.{func.__name__}"
|
|
897
|
+
else:
|
|
898
|
+
# Handle static and regular functions
|
|
899
|
+
if '.' in func.__qualname__:
|
|
900
|
+
# Static method or a function defined inside a class
|
|
901
|
+
class_name, func_name = func.__qualname__.rsplit('.', 1)
|
|
902
|
+
return f"{func.__module__}.{class_name}.{func_name}"
|
|
903
|
+
else:
|
|
904
|
+
# Regular function not attached to a class
|
|
905
|
+
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,15 @@ 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
|
+
run_opts['reports_folder'] = self.reports_folder
|
|
47
48
|
fmt_opts = {
|
|
48
49
|
'json': run_opts.get('json', False),
|
|
49
50
|
'print_item': False,
|
|
50
51
|
'print_start': True,
|
|
51
52
|
'print_run_summary': True,
|
|
53
|
+
'print_progress': self.sync
|
|
52
54
|
}
|
|
53
55
|
run_opts.update(fmt_opts)
|
|
54
56
|
|