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.

Files changed (68) hide show
  1. secator/.gitignore +162 -0
  2. secator/celery.py +8 -68
  3. secator/cli.py +631 -274
  4. secator/decorators.py +42 -6
  5. secator/definitions.py +104 -33
  6. secator/exporters/csv.py +1 -2
  7. secator/exporters/gdrive.py +1 -1
  8. secator/exporters/json.py +1 -2
  9. secator/exporters/txt.py +1 -2
  10. secator/hooks/mongodb.py +12 -12
  11. secator/installer.py +335 -0
  12. secator/report.py +2 -14
  13. secator/rich.py +3 -10
  14. secator/runners/_base.py +106 -34
  15. secator/runners/_helpers.py +18 -17
  16. secator/runners/command.py +91 -55
  17. secator/runners/scan.py +3 -1
  18. secator/runners/task.py +6 -4
  19. secator/runners/workflow.py +13 -11
  20. secator/tasks/_categories.py +14 -19
  21. secator/tasks/cariddi.py +2 -1
  22. secator/tasks/dalfox.py +2 -0
  23. secator/tasks/dirsearch.py +5 -7
  24. secator/tasks/dnsx.py +1 -0
  25. secator/tasks/dnsxbrute.py +1 -0
  26. secator/tasks/feroxbuster.py +6 -7
  27. secator/tasks/ffuf.py +4 -7
  28. secator/tasks/gau.py +1 -4
  29. secator/tasks/gf.py +2 -1
  30. secator/tasks/gospider.py +1 -0
  31. secator/tasks/grype.py +47 -47
  32. secator/tasks/h8mail.py +5 -6
  33. secator/tasks/httpx.py +24 -18
  34. secator/tasks/katana.py +11 -15
  35. secator/tasks/maigret.py +3 -3
  36. secator/tasks/mapcidr.py +1 -0
  37. secator/tasks/msfconsole.py +3 -1
  38. secator/tasks/naabu.py +2 -1
  39. secator/tasks/nmap.py +14 -17
  40. secator/tasks/nuclei.py +4 -3
  41. secator/tasks/searchsploit.py +4 -2
  42. secator/tasks/subfinder.py +1 -0
  43. secator/tasks/wpscan.py +11 -13
  44. secator/utils.py +64 -82
  45. secator/utils_test.py +3 -2
  46. secator-0.3.5.dist-info/METADATA +411 -0
  47. secator-0.3.5.dist-info/RECORD +100 -0
  48. {secator-0.0.1.dist-info → secator-0.3.5.dist-info}/WHEEL +1 -2
  49. secator-0.0.1.dist-info/METADATA +0 -199
  50. secator-0.0.1.dist-info/RECORD +0 -114
  51. secator-0.0.1.dist-info/top_level.txt +0 -2
  52. tests/__init__.py +0 -0
  53. tests/integration/__init__.py +0 -0
  54. tests/integration/inputs.py +0 -42
  55. tests/integration/outputs.py +0 -392
  56. tests/integration/test_scans.py +0 -82
  57. tests/integration/test_tasks.py +0 -103
  58. tests/integration/test_workflows.py +0 -163
  59. tests/performance/__init__.py +0 -0
  60. tests/performance/loadtester.py +0 -56
  61. tests/unit/__init__.py +0 -0
  62. tests/unit/test_celery.py +0 -39
  63. tests/unit/test_scans.py +0 -0
  64. tests/unit/test_serializers.py +0 -51
  65. tests/unit/test_tasks.py +0 -348
  66. tests/unit/test_workflows.py +0 -96
  67. {secator-0.0.1.dist-info → secator-0.3.5.dist-info}/entry_points.txt +0 -0
  68. {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.result = None
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', self.sync and not self.output_quiet)
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
- instance_func = getattr(self, key, None)
145
- if instance_func:
146
- self.hooks[key].append(instance_func)
147
- self.hooks[key].extend(hooks.get(self.__class__, {}).get(key, []))
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.result:
283
+ if self.celery_result:
253
284
  self._print('Revoking remote Celery tasks ...', color='bold red', rich=True)
254
- self.stop_live_tasks(self.result)
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=2)
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=2)
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
- debug('Duplicates:', sub='runner.mark_duplicates', level=2)
290
- debug('\n\t'.join([repr(i) for i in self.results if i._duplicate]), sub='runner.mark_duplicates', level=2)
291
- debug('duplicate check completed', id=self.config.name, sub='runner.mark_duplicates')
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 = f'{hook.__module__}.{hook.__name__}'
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._print(f'{fun} failed: "{e.__class__.__name__}". Skipping', color='bold red', rich=True)
335
- if DEBUG > 1:
336
- logger.exception(e)
370
+ if self.raise_on_error:
371
+ raise e
337
372
  else:
338
- self._print('Please set DEBUG to > 1 to see the detailed exception.', color='dim red', rich=True)
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'[bold magenta]{self.config.name}[/] infos ({len(self.infos)}):',
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'[bold magenta]{self.config.name}[/] errors ({len(self.errors)}):',
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(':adhesive_bandage: Found 0 results.', color='bold red', rich=True)
513
+ self._print(':exclamation_mark:Found 0 results.', color='bold red', rich=True)
472
514
  else:
473
- results_str = ':pill: Found ' + ' and '.join([
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): Termcolor color.
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 and int(item.percent) != 100:
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__}"
@@ -1,5 +1,4 @@
1
- from celery.result import AsyncResult, GroupResult
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 confirm_exit(func):
140
- """Decorator asking user for confirmation to exit.
141
-
142
- Args:
143
- func (func): Decorated function.
144
- """
145
- def inner_function(self, *args, **kwargs):
146
- try:
147
- func(self, *args, **kwargs)
148
- except KeyboardInterrupt:
149
- exit_confirmed = Confirm.ask('Are you sure you want to exit ?')
150
- if exit_confirmed:
151
- self.log_results()
152
- raise KeyboardInterrupt
153
- return inner_function
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
@@ -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 (DEBUG, DEFAULT_HTTP_PROXY,
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, DATA_FOLDER, DEFAULT_INPUT_CHUNK_SIZE)
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 get_file_timestamp, debug
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
- # Install command
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 install(cls):
264
- """Install command by running the content of cls.install_cmd."""
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
- @classmethod
283
- def run_command(cls, cmd, name='helperClass', cls_attributes={}, **kwargs):
284
- """Run adhoc command. Can be used without defining an inherited class to run a command, while still enjoying
285
- all the good stuff in this class.
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 utils install {self.config.name}`.'
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.strip()
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 and not self.output_quiet:
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
- timestr = get_file_timestamp()
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 fmt options
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