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.

Files changed (68) hide show
  1. secator/.gitignore +162 -0
  2. secator/celery.py +7 -67
  3. secator/cli.py +631 -274
  4. secator/decorators.py +54 -11
  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 +105 -34
  15. secator/runners/_helpers.py +18 -17
  16. secator/runners/command.py +91 -55
  17. secator/runners/scan.py +2 -1
  18. secator/runners/task.py +5 -4
  19. secator/runners/workflow.py +12 -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 +3 -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.6.dist-info/METADATA +411 -0
  47. secator-0.3.6.dist-info/RECORD +100 -0
  48. {secator-0.0.1.dist-info → secator-0.3.6.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.6.dist-info}/entry_points.txt +0 -0
  68. {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.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
+ 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', self.sync and not self.output_quiet)
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
- 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, []))
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.result:
282
+ if self.celery_result:
253
283
  self._print('Revoking remote Celery tasks ...', color='bold red', rich=True)
254
- self.stop_live_tasks(self.result)
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=2)
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=2)
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
- 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')
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 = f'{hook.__module__}.{hook.__name__}'
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._print(f'{fun} failed: "{e.__class__.__name__}". Skipping', color='bold red', rich=True)
335
- if DEBUG > 1:
336
- logger.exception(e)
369
+ if self.raise_on_error:
370
+ raise e
337
371
  else:
338
- self._print('Please set DEBUG to > 1 to see the detailed exception.', color='dim red', rich=True)
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'[bold magenta]{self.config.name}[/] infos ({len(self.infos)}):',
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'[bold magenta]{self.config.name}[/] errors ({len(self.errors)}):',
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(':adhesive_bandage: Found 0 results.', color='bold red', rich=True)
512
+ self._print(':exclamation_mark:Found 0 results.', color='bold red', rich=True)
472
513
  else:
473
- results_str = ':pill: Found ' + ' and '.join([
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): Termcolor color.
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 and int(item.percent) != 100:
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__}"
@@ -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,14 @@ 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
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
- # Task opts
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': not self.output_quiet,
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
- result = task_cls.delay(self.targets, **run_opts)
62
+ self.celery_result = task_cls.delay(self.targets, **run_opts)
62
63
  task = self.process_live_tasks(
63
- result,
64
+ self.celery_result,
64
65
  description=False,
65
66
  results_only=True,
66
67
  print_remote_status=self.print_remote_status)