secator 0.10.1a2__py3-none-any.whl → 0.10.1a4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of secator might be problematic. Click here for more details.

secator/celery.py CHANGED
@@ -2,7 +2,6 @@ import gc
2
2
  import json
3
3
  import logging
4
4
  import os
5
- import sys
6
5
  import uuid
7
6
 
8
7
  from time import time
@@ -13,14 +12,13 @@ from celery.app import trace
13
12
  from rich.logging import RichHandler
14
13
  from retry import retry
15
14
 
16
- from secator.celery_signals import setup_handlers
15
+ from secator.celery_signals import IN_CELERY_WORKER_PROCESS, setup_handlers
17
16
  from secator.config import CONFIG
18
17
  from secator.output_types import Info
19
18
  from secator.rich import console
20
19
  from secator.runners import Scan, Task, Workflow
21
20
  from secator.utils import (debug, deduplicate, flatten, should_update)
22
21
 
23
- IN_CELERY_WORKER_PROCESS = sys.argv and ('secator.celery.app' in sys.argv or 'worker' in sys.argv)
24
22
 
25
23
  #---------#
26
24
  # Logging #
@@ -138,64 +136,11 @@ def chunker(seq, size):
138
136
  return (seq[pos:pos + size] for pos in range(0, len(seq), size))
139
137
 
140
138
 
141
- @app.task(bind=True)
142
- def handle_runner_error(self, results, runner):
143
- """Handle errors in Celery workflows (chunked tasks or runners)."""
144
- results = forward_results(results)
145
- runner.results = results
146
- runner.log_results()
147
- runner.run_hooks('on_end')
148
- return runner.results
149
-
150
-
151
- def break_task(task, task_opts, targets, results=[], chunk_size=1):
152
- """Break a task into multiple of the same type."""
153
- chunks = targets
154
- if chunk_size > 1:
155
- chunks = list(chunker(targets, chunk_size))
156
- debug(
157
- '',
158
- obj={task.unique_name: 'CHUNKED', 'chunk_size': chunk_size, 'chunks': len(chunks), 'target_count': len(targets)},
159
- obj_after=False,
160
- sub='celery.state',
161
- verbose=True
162
- )
163
-
164
- # Clone opts
165
- opts = task_opts.copy()
166
-
167
- # Build signatures
168
- sigs = []
169
- task.ids_map = {}
170
- for ix, chunk in enumerate(chunks):
171
- if not isinstance(chunk, list):
172
- chunk = [chunk]
173
- if len(chunks) > 0: # add chunk to task opts for tracking chunks exec
174
- opts['chunk'] = ix + 1
175
- opts['chunk_count'] = len(chunks)
176
- task_id = str(uuid.uuid4())
177
- opts['has_parent'] = True
178
- opts['enable_duplicate_check'] = False
179
- opts['results'] = results
180
- sig = type(task).si(chunk, **opts).set(queue=type(task).profile, task_id=task_id)
181
- full_name = f'{task.name}_{ix + 1}'
182
- task.add_subtask(task_id, task.name, f'{task.name}_{ix + 1}')
183
- info = Info(message=f'Celery chunked task created: {task_id}', _source=full_name, _uuid=str(uuid.uuid4()))
184
- task.add_result(info)
185
- sigs.append(sig)
186
-
187
- # Build Celery workflow
188
- workflow = chord(
189
- tuple(sigs),
190
- handle_runner_error.s(runner=task).set(queue='results')
191
- )
192
- return workflow
193
-
194
-
195
139
  @app.task(bind=True)
196
140
  def run_task(self, args=[], kwargs={}):
197
- print('run task')
198
141
  console.print(Info(message=f'Running task {self.request.id}'))
142
+ if 'context' not in kwargs:
143
+ kwargs['context'] = {}
199
144
  kwargs['context']['celery_id'] = self.request.id
200
145
  task = Task(*args, **kwargs)
201
146
  task.run()
@@ -204,6 +149,8 @@ def run_task(self, args=[], kwargs={}):
204
149
  @app.task(bind=True)
205
150
  def run_workflow(self, args=[], kwargs={}):
206
151
  console.print(Info(message=f'Running workflow {self.request.id}'))
152
+ if 'context' not in kwargs:
153
+ kwargs['context'] = {}
207
154
  kwargs['context']['celery_id'] = self.request.id
208
155
  workflow = Workflow(*args, **kwargs)
209
156
  workflow.run()
@@ -241,12 +188,17 @@ def run_command(self, results, name, targets, opts={}):
241
188
  sync = not IN_CELERY_WORKER_PROCESS
242
189
  task_cls = Task.get_task_class(name)
243
190
  task = task_cls(targets, **opts)
191
+ task.started = True
192
+ task.run_hooks('on_start')
244
193
  update_state(self, task, force=True)
245
194
 
246
195
  # Chunk task if needed
247
- if task_cls.needs_chunking(targets, sync):
248
- console.print(Info(message=f'Task {name} requires chunking, breaking into {len(targets)} tasks'))
249
- return self.replace(break_task(task, opts, targets, results=results))
196
+ if task.needs_chunking(sync):
197
+ if IN_CELERY_WORKER_PROCESS:
198
+ console.print(Info(message=f'Task {name} requires chunking, breaking into {len(targets)} tasks'))
199
+ tasks = break_task(task, opts, results=results)
200
+ update_state(self, task, force=True)
201
+ return self.replace(tasks)
250
202
 
251
203
  # Update state live
252
204
  [update_state(self, task) for _ in task]
@@ -268,9 +220,56 @@ def forward_results(results):
268
220
  results = results['results']
269
221
  results = flatten(results)
270
222
  results = deduplicate(results, attr='_uuid')
271
- console.print(Info(message=f'Forwarding {len(results)} results ...'))
223
+ if IN_CELERY_WORKER_PROCESS:
224
+ console.print(Info(message=f'Forwarding {len(results)} results ...'))
272
225
  return results
273
226
 
227
+
228
+ @app.task
229
+ def mark_runner_started(runner):
230
+ """Mark a runner as started and run on_start hooks.
231
+
232
+ Args:
233
+ runner (Runner): Secator runner instance
234
+
235
+ Returns:
236
+ list: Runner results
237
+ """
238
+ runner.started = True
239
+ # runner.start_time = time()
240
+ runner.run_hooks('on_start')
241
+ return runner.results
242
+
243
+
244
+ @app.task
245
+ def mark_runner_complete(results, runner):
246
+ """Mark a runner as completed and run on_end hooks.
247
+
248
+ Args:
249
+ results (list): Task results
250
+ runner (Runner): Secator runner instance
251
+
252
+ Returns:
253
+ list: Final results
254
+ """
255
+ results = forward_results(results)
256
+
257
+ # If sync mode, don't update the runner as it's already done
258
+ if runner.sync:
259
+ return results
260
+
261
+ # Run final processing
262
+ runner.results = results
263
+ runner.done = True
264
+ runner.progress = 100
265
+ if not runner.no_process:
266
+ runner.mark_duplicates()
267
+ runner.results = runner.filter_results()
268
+ runner.log_results()
269
+ runner.run_hooks('on_end')
270
+ return runner.results
271
+
272
+
274
273
  #--------------#
275
274
  # Celery utils #
276
275
  #--------------#
@@ -285,3 +284,50 @@ def is_celery_worker_alive():
285
284
  else:
286
285
  console.print(Info(message='No Celery worker available, running locally'))
287
286
  return result
287
+
288
+
289
+ def break_task(task, task_opts, results=[]):
290
+ """Break a task into multiple of the same type."""
291
+ chunks = task.inputs
292
+ if task.input_chunk_size > 1:
293
+ chunks = list(chunker(task.inputs, task.input_chunk_size))
294
+ debug(
295
+ '',
296
+ obj={task.unique_name: 'CHUNKED', 'chunk_size': task.input_chunk_size, 'chunks': len(chunks), 'target_count': len(task.inputs)}, # noqa: E501
297
+ obj_after=False,
298
+ sub='celery.state',
299
+ verbose=True
300
+ )
301
+
302
+ # Clone opts
303
+ opts = task_opts.copy()
304
+
305
+ # Build signatures
306
+ sigs = []
307
+ task.ids_map = {}
308
+ for ix, chunk in enumerate(chunks):
309
+ if not isinstance(chunk, list):
310
+ chunk = [chunk]
311
+ if len(chunks) > 0: # add chunk to task opts for tracking chunks exec
312
+ opts['chunk'] = ix + 1
313
+ opts['chunk_count'] = len(chunks)
314
+ task_id = str(uuid.uuid4())
315
+ opts['has_parent'] = True
316
+ opts['enable_duplicate_check'] = False
317
+ opts['results'] = results
318
+ sig = type(task).si(chunk, **opts).set(queue=type(task).profile, task_id=task_id)
319
+ full_name = f'{task.name}_{ix + 1}'
320
+ task.add_subtask(task_id, task.name, f'{task.name}_{ix + 1}')
321
+ info = Info(message=f'Celery chunked task created: {task_id}', _source=full_name, _uuid=str(uuid.uuid4()))
322
+ task.add_result(info)
323
+ sigs.append(sig)
324
+
325
+ # Mark main task as async since it's being chunked
326
+ task.sync = False
327
+
328
+ # Build Celery workflow
329
+ workflow = chord(
330
+ tuple(sigs),
331
+ mark_runner_complete.s(runner=task).set(queue='results')
332
+ )
333
+ return workflow
secator/celery_signals.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import signal
3
+ import sys
3
4
  import threading
4
5
  from pathlib import Path
5
6
 
@@ -10,6 +11,7 @@ from secator.output_types import Info
10
11
  from secator.rich import console
11
12
 
12
13
  IDLE_TIMEOUT = CONFIG.celery.worker_kill_after_idle_seconds
14
+ IN_CELERY_WORKER_PROCESS = sys.argv and ('secator.celery.app' in sys.argv or 'worker' in sys.argv)
13
15
 
14
16
  # File-based state management system
15
17
  STATE_DIR = Path("/tmp/celery_state")
secator/celery_utils.py CHANGED
@@ -12,7 +12,7 @@ from rich.padding import Padding
12
12
  from rich.progress import Progress as RichProgress, SpinnerColumn, TextColumn, TimeElapsedColumn
13
13
  from secator.config import CONFIG
14
14
  from secator.definitions import STATE_COLORS
15
- from secator.output_types import Error
15
+ from secator.output_types import Error, Info, State
16
16
  from secator.rich import console
17
17
  from secator.utils import debug, traceback_as_string
18
18
 
@@ -76,10 +76,31 @@ class CeleryData(object):
76
76
 
77
77
  # Get live results and print progress
78
78
  for data in CeleryData.poll(result, ids_map, refresh_interval):
79
- yield from data['results']
79
+ for result in data['results']:
80
+
81
+ # Add dynamic subtask to ids_map
82
+ if isinstance(result, Info):
83
+ message = result.message
84
+ if message.startswith('Celery chunked task created: '):
85
+ task_id = message.split(' ')[-1]
86
+ ids_map[task_id] = {
87
+ 'id': task_id,
88
+ 'name': result._source,
89
+ 'full_name': result._source,
90
+ 'descr': '',
91
+ 'state': 'PENDING',
92
+ 'count': 0,
93
+ 'progress': 0
94
+ }
95
+ yield result
80
96
 
81
97
  if print_remote_info:
82
98
  task_id = data['id']
99
+ if task_id not in progress_cache:
100
+ if CONFIG.runners.show_subtasks:
101
+ progress_cache[task_id] = progress.add_task('', advance=0, **data)
102
+ else:
103
+ continue
83
104
  progress_id = progress_cache[task_id]
84
105
  CeleryData.update_progress(progress, progress_id, data)
85
106
 
@@ -117,9 +138,24 @@ class CeleryData(object):
117
138
  """
118
139
  while True:
119
140
  try:
141
+ main_task = State(
142
+ task_id=result.id,
143
+ state=result.state,
144
+ _source='celery'
145
+ )
146
+ debug(f"Main task state: {result.id} - {result.state}", sub='celery.poll', verbose=True)
147
+ yield {'id': result.id, 'results': [main_task]}
120
148
  yield from CeleryData.get_all_data(result, ids_map)
149
+
121
150
  if result.ready():
122
151
  debug('result is ready', sub='celery.poll', id=result.id)
152
+ main_task = State(
153
+ task_id=result.id,
154
+ state=result.state,
155
+ _source='celery'
156
+ )
157
+ debug(f"Final main task state: {result.id} - {result.state}", sub='celery.poll', verbose=True)
158
+ yield {'id': result.id, 'results': [main_task]}
123
159
  yield from CeleryData.get_all_data(result, ids_map)
124
160
  break
125
161
  except (KeyboardInterrupt, GreenletExit):
secator/config.py CHANGED
@@ -93,6 +93,7 @@ class Runners(StrictModel):
93
93
  skip_exploit_search: bool = False
94
94
  skip_cve_low_confidence: bool = False
95
95
  remove_duplicates: bool = False
96
+ show_chunk_progress: bool = False
96
97
 
97
98
 
98
99
  class Security(StrictModel):
@@ -7,6 +7,7 @@ __all__ = [
7
7
  'Progress',
8
8
  'Record',
9
9
  'Stat',
10
+ 'State',
10
11
  'Subdomain',
11
12
  'Url',
12
13
  'UserAccount',
@@ -29,9 +30,10 @@ from secator.output_types.info import Info
29
30
  from secator.output_types.warning import Warning
30
31
  from secator.output_types.error import Error
31
32
  from secator.output_types.stat import Stat
33
+ from secator.output_types.state import State
32
34
 
33
35
  EXECUTION_TYPES = [
34
- Target, Progress, Info, Warning, Error
36
+ Target, Progress, Info, Warning, Error, State
35
37
  ]
36
38
  STAT_TYPES = [
37
39
  Stat
@@ -0,0 +1,29 @@
1
+ import time
2
+ from dataclasses import dataclass, field
3
+
4
+ from secator.output_types._base import OutputType
5
+ from secator.utils import rich_to_ansi
6
+
7
+
8
+ @dataclass
9
+ class State(OutputType):
10
+ """Represents the state of a Celery task."""
11
+
12
+ task_id: str
13
+ state: str
14
+ _type: str = field(default='state', repr=True)
15
+ _source: str = field(default='', repr=True)
16
+ _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
17
+ _uuid: str = field(default='', repr=True, compare=False)
18
+ _context: dict = field(default_factory=dict, repr=True, compare=False)
19
+ _tagged: bool = field(default=False, repr=True, compare=False)
20
+ _duplicate: bool = field(default=False, repr=True, compare=False)
21
+ _related: list = field(default_factory=list, compare=False)
22
+ _icon = '📊'
23
+ _color = 'bright_blue'
24
+
25
+ def __str__(self) -> str:
26
+ return f"Task {self.task_id} is {self.state}"
27
+
28
+ def __repr__(self) -> str:
29
+ return rich_to_ansi(f"{self._icon} [bold {self._color}]{self.state}[/] {self.task_id}")
secator/runners/_base.py CHANGED
@@ -12,7 +12,7 @@ import humanize
12
12
  from secator.definitions import ADDONS_ENABLED
13
13
  from secator.celery_utils import CeleryData
14
14
  from secator.config import CONFIG
15
- from secator.output_types import FINDING_TYPES, OutputType, Progress, Info, Warning, Error, Target
15
+ from secator.output_types import FINDING_TYPES, OutputType, Progress, Info, Warning, Error, Target, State
16
16
  from secator.report import Report
17
17
  from secator.rich import console, console_stdout
18
18
  from secator.runners._helpers import (get_task_folder_id, process_extractor, run_extractors)
@@ -97,6 +97,7 @@ class Runner:
97
97
  self.threads = []
98
98
  self.no_poll = self.run_opts.get('no_poll', False)
99
99
  self.quiet = self.run_opts.get('quiet', False)
100
+ self.started = False
100
101
 
101
102
  # Runner process options
102
103
  self.no_process = self.run_opts.get('no_process', False)
@@ -117,10 +118,21 @@ class Runner:
117
118
  self.raise_on_error = self.run_opts.get('raise_on_error', False)
118
119
  self.print_opts = {k: v for k, v in self.__dict__.items() if k.startswith('print_') if v}
119
120
 
121
+ # Chunks
122
+ self.has_parent = self.run_opts.get('has_parent', False)
123
+ self.has_children = self.run_opts.get('has_children', False)
124
+ self.chunk = self.run_opts.get('chunk', None)
125
+ self.chunk_count = self.run_opts.get('chunk_count', None)
126
+ self.unique_name = self.name.replace('/', '_')
127
+ self.unique_name = f'{self.unique_name}_{self.chunk}' if self.chunk else self.unique_name
128
+
129
+ # Add prior results to runner results
130
+ [self.add_result(result, print=False, output=False) for result in results]
131
+
120
132
  # Determine inputs
121
133
  inputs = [inputs] if not isinstance(inputs, list) else inputs
122
- if results:
123
- inputs, run_opts, errors = run_extractors(results, run_opts, inputs)
134
+ if not self.chunk and self.results:
135
+ inputs, run_opts, errors = run_extractors(self.results, run_opts, inputs)
124
136
  for error in errors:
125
137
  self.add_result(error, print=True)
126
138
  self.inputs = inputs
@@ -163,18 +175,6 @@ class Runner:
163
175
  self.validators = {name: [] for name in VALIDATORS + getattr(self, 'validators', [])}
164
176
  self.register_validators(validators)
165
177
 
166
- # Chunks
167
- self.has_parent = self.run_opts.get('has_parent', False)
168
- self.has_children = self.run_opts.get('has_children', False)
169
- self.chunk = self.run_opts.get('chunk', None)
170
- self.chunk_count = self.run_opts.get('chunk_count', None)
171
- self.unique_name = self.name.replace('/', '_')
172
- self.unique_name = f'{self.unique_name}_{self.chunk}' if self.chunk else self.unique_name
173
-
174
- # Process prior results
175
- for result in results:
176
- list(self._process_item(result, print=False, output=False))
177
-
178
178
  # Input post-process
179
179
  self.run_hooks('before_init')
180
180
 
@@ -238,6 +238,8 @@ class Runner:
238
238
 
239
239
  @property
240
240
  def status(self):
241
+ if not self.started:
242
+ return 'PENDING'
241
243
  if not self.done:
242
244
  return 'RUNNING'
243
245
  return 'FAILURE' if len(self.self_errors) > 0 else 'SUCCESS'
@@ -283,11 +285,8 @@ class Runner:
283
285
  self.run_hooks('on_end')
284
286
  return
285
287
 
286
- # Choose yielder
287
- yielder = self.yielder_celery if self.celery_result else self.yielder
288
-
289
288
  # Loop and process items
290
- for item in yielder():
289
+ for item in self.yielder():
291
290
  yield from self._process_item(item)
292
291
  self.run_hooks('on_interval')
293
292
 
@@ -326,16 +325,18 @@ class Runner:
326
325
  self.add_result(error, print=True)
327
326
  yield error
328
327
 
329
- def add_result(self, item, print=False):
328
+ def add_result(self, item, print=False, output=True):
330
329
  """Add item to runner results.
331
330
 
332
331
  Args:
333
332
  item (OutputType): Item.
334
333
  print (bool): Whether to print it or not.
334
+ output (bool): Whether to add it to the output or not.
335
335
  """
336
336
  self.uuids.append(item._uuid)
337
337
  self.results.append(item)
338
- self.output += repr(item) + '\n'
338
+ if output:
339
+ self.output += repr(item) + '\n'
339
340
  if print:
340
341
  self._print_item(item)
341
342
 
@@ -481,16 +482,52 @@ class Runner:
481
482
  dupe = self.run_hooks('on_duplicate', dupe)
482
483
 
483
484
  def yielder(self):
484
- """Yield results. Should be implemented by derived classes."""
485
- raise NotImplementedError()
485
+ """Base yielder implementation.
486
+
487
+ This should be overridden by derived classes if they need custom behavior.
488
+ Otherwise, they can implement build_celery_workflow() and get standard behavior.
486
489
 
487
- def yielder_celery(self):
488
- """Yield results from Celery result."""
489
- yield from CeleryData.iter_results(
490
- self.celery_result,
491
- ids_map=self.celery_ids_map,
492
- print_remote_info=False
493
- )
490
+ Yields:
491
+ secator.output_types.OutputType: Secator output type.
492
+ """
493
+ # Build Celery workflow
494
+ workflow = self.build_celery_workflow()
495
+
496
+ # Run workflow and get results
497
+ if self.sync:
498
+ self.print_item = False
499
+ self.started = True
500
+ results = workflow.apply().get()
501
+ yield from results
502
+ else:
503
+ self.celery_result = workflow()
504
+ self.celery_ids.append(str(self.celery_result.id))
505
+ yield Info(
506
+ message=f'Celery task created: {self.celery_result.id}',
507
+ task_id=self.celery_result.id
508
+ )
509
+ if self.no_poll:
510
+ return
511
+ results = CeleryData.iter_results(
512
+ self.celery_result,
513
+ ids_map=self.celery_ids_map,
514
+ description=True,
515
+ print_remote_info=self.print_remote_info,
516
+ print_remote_title=f'[bold gold3]{self.__class__.__name__.capitalize()}[/] [bold magenta]{self.name}[/] results'
517
+ )
518
+
519
+ # Yield results
520
+ yield from results
521
+
522
+ def build_celery_workflow(self):
523
+ """Build Celery workflow.
524
+
525
+ This should be implemented by derived classes.
526
+
527
+ Returns:
528
+ celery.Signature: Celery task signature.
529
+ """
530
+ raise NotImplementedError("Derived classes must implement build_celery_workflow()")
494
531
 
495
532
  def toDict(self):
496
533
  """Dict representation of the runner."""
@@ -651,6 +688,7 @@ class Runner:
651
688
  """Log runner results."""
652
689
  if self.no_poll:
653
690
  return
691
+ self.started = True
654
692
  self.done = True
655
693
  self.progress = 100
656
694
  self.end_time = datetime.fromtimestamp(time())
@@ -831,20 +869,32 @@ class Runner:
831
869
  # Update item context
832
870
  item._context.update(self.context)
833
871
 
834
- # Return if already seen
835
- if item._uuid in self.uuids:
836
- return
837
-
838
872
  # Add uuid to item
839
873
  if not item._uuid:
840
874
  item._uuid = str(uuid.uuid4())
841
875
 
876
+ # Return if already seen
877
+ if item._uuid in self.uuids:
878
+ return
879
+
842
880
  # Add source to item
843
881
  if not item._source:
844
882
  item._source = self.unique_name
845
883
 
884
+ # Check for state updates
885
+ if isinstance(item, State) and self.celery_result and item.task_id == self.celery_result.id:
886
+ self.debug(f'Updating runner state from Celery: {item.state}', sub='state')
887
+ if item.state in ['FAILURE', 'SUCCESS', 'REVOKED']:
888
+ self.started = True
889
+ self.done = True
890
+ elif item.state in ['RUNNING']:
891
+ self.started = True
892
+ self.debug(f'Runner {self.unique_name} is {self.status} (started: {self.started}, done: {self.done})', sub='state')
893
+ self.last_updated_celery = item._timestamp
894
+ return
895
+
846
896
  # If progress item, update runner progress
847
- if isinstance(item, Progress) and item._source == self.unique_name:
897
+ elif isinstance(item, Progress) and item._source == self.unique_name:
848
898
  self.progress = item.percent
849
899
  if not should_update(CONFIG.runners.progress_update_frequency, self.last_updated_progress, item._timestamp):
850
900
  return
@@ -193,11 +193,10 @@ class Command(Runner):
193
193
  })
194
194
  return res
195
195
 
196
- @classmethod
197
- def needs_chunking(cls, targets, sync):
198
- many_targets = len(targets) > 1
199
- targets_over_chunk_size = cls.input_chunk_size and len(targets) > cls.input_chunk_size
200
- has_file_flag = cls.file_flag is not None
196
+ def needs_chunking(self, sync):
197
+ many_targets = len(self.inputs) > 1
198
+ targets_over_chunk_size = self.input_chunk_size and len(self.inputs) > self.input_chunk_size
199
+ has_file_flag = self.file_flag is not None
201
200
  chunk_it = (sync and many_targets and not has_file_flag) or (not sync and many_targets and targets_over_chunk_size)
202
201
  return chunk_it
203
202
 
secator/runners/scan.py CHANGED
@@ -1,9 +1,7 @@
1
1
  import logging
2
2
 
3
- from secator.template import TemplateLoader
4
3
  from secator.config import CONFIG
5
4
  from secator.runners._base import Runner
6
- from secator.runners._helpers import run_extractors
7
5
  from secator.runners.workflow import Workflow
8
6
  from secator.utils import merge_opts
9
7
 
@@ -19,25 +17,39 @@ class Scan(Runner):
19
17
  from secator.celery import run_scan
20
18
  return run_scan.delay(args=args, kwargs=kwargs)
21
19
 
22
- def yielder(self):
23
- """Run scan.
20
+ def build_celery_workflow(self):
21
+ """Build Celery workflow for scan execution.
24
22
 
25
- Yields:
26
- secator.output_types.OutputType: Secator output type.
23
+ Returns:
24
+ celery.Signature: Celery task signature.
27
25
  """
26
+ from celery import chain
27
+ from secator.celery import mark_runner_started, mark_runner_complete
28
+ from secator.template import TemplateLoader
29
+
28
30
  scan_opts = self.config.options
29
- self.print_item = False
31
+
32
+ # Build chain of workflows
33
+ sigs = []
30
34
  for name, workflow_opts in self.config.workflows.items():
31
- # Run workflow
32
35
  run_opts = self.run_opts.copy()
33
36
  opts = merge_opts(scan_opts, workflow_opts, run_opts)
37
+ config = TemplateLoader(name=f'workflows/{name}')
34
38
  workflow = Workflow(
35
- TemplateLoader(name=f'workflows/{name}'),
39
+ config,
36
40
  self.inputs,
37
- results=[],
41
+ results=self.results,
38
42
  run_opts=opts,
39
43
  hooks=self._hooks,
40
- context=self.context.copy())
41
-
42
- # Get results
43
- yield from workflow
44
+ context=self.context.copy()
45
+ )
46
+ celery_workflow = workflow.build_celery_workflow()
47
+ for task_id, task_info in workflow.celery_ids_map.items():
48
+ self.add_subtask(task_id, task_info['name'], task_info['descr'])
49
+ sigs.append(celery_workflow)
50
+
51
+ return chain(
52
+ mark_runner_started.si(self).set(queue='results'),
53
+ *sigs,
54
+ mark_runner_complete.s(self).set(queue='results'),
55
+ )
secator/runners/task.py CHANGED
@@ -1,8 +1,8 @@
1
+ import uuid
1
2
  from secator.config import CONFIG
2
3
  from secator.runners import Runner
3
4
  from secator.utils import discover_tasks
4
- from secator.celery_utils import CeleryData
5
- from secator.output_types import Info
5
+ from celery import chain
6
6
 
7
7
 
8
8
  class Task(Runner):
@@ -14,52 +14,46 @@ class Task(Runner):
14
14
  from secator.celery import run_task
15
15
  return run_task.apply_async(kwargs={'args': args, 'kwargs': kwargs}, queue='celery')
16
16
 
17
- def yielder(self):
18
- """Run task.
17
+ def build_celery_workflow(self):
18
+ """Build Celery workflow for task execution.
19
19
 
20
- Yields:
21
- secator.output_types.OutputType: Secator output type.
20
+ Args:
21
+ run_opts (dict): Run options.
22
+ results (list): Prior results.
23
+
24
+ Returns:
25
+ celery.Signature: Celery task signature.
22
26
  """
27
+ from secator.celery import run_command, mark_runner_started, mark_runner_complete
28
+
23
29
  # Get task class
24
30
  task_cls = Task.get_task_class(self.config.name)
25
31
 
26
32
  # Run opts
27
- run_opts = self.run_opts.copy()
28
- run_opts.pop('output', None)
29
- run_opts.pop('no_poll', False)
33
+ opts = self.run_opts.copy()
34
+ opts.pop('output', None)
35
+ opts.pop('no_poll', False)
30
36
 
31
37
  # Set task output types
32
38
  self.output_types = task_cls.output_types
33
39
  self.enable_duplicate_check = False
34
40
 
35
41
  # Get hooks
36
- hooks = {task_cls: self.hooks}
37
- run_opts['hooks'] = hooks
38
- run_opts['context'] = self.context
42
+ hooks = self._hooks.get(Task, {})
43
+ opts['hooks'] = hooks
44
+ opts['context'] = self.context
39
45
 
40
- # Run task
41
- if self.sync:
42
- self.print_item = False
43
- result = task_cls.si(self.inputs, **run_opts)
44
- results = result.apply().get()
45
- else:
46
- self.celery_result = task_cls.delay(self.inputs, **run_opts)
47
- self.add_subtask(self.celery_result.id, self.config.name, self.config.description or '')
48
- yield Info(
49
- message=f'Celery task created: {self.celery_result.id}',
50
- task_id=self.celery_result.id
51
- )
52
- if self.no_poll:
53
- return
54
- results = CeleryData.iter_results(
55
- self.celery_result,
56
- ids_map=self.celery_ids_map,
57
- description=True,
58
- print_remote_info=False,
59
- print_remote_title=f'[bold gold3]{self.__class__.__name__.capitalize()}[/] [bold magenta]{self.name}[/] results')
46
+ # Create task signature
47
+ task_id = str(uuid.uuid4())
48
+ sig = run_command.s(self.config.name, self.inputs, opts).set(queue=task_cls.profile, task_id=task_id)
49
+ self.add_subtask(task_id, self.config.name, self.config.description or '')
60
50
 
61
- # Yield task results
62
- yield from results
51
+ # Build signature chain with lifecycle management
52
+ return chain(
53
+ mark_runner_started.si(self).set(queue='results'),
54
+ sig,
55
+ mark_runner_complete.s(self).set(queue='results'),
56
+ )
63
57
 
64
58
  @staticmethod
65
59
  def get_task_class(name):
@@ -4,8 +4,6 @@ from secator.config import CONFIG
4
4
  from secator.runners._base import Runner
5
5
  from secator.runners.task import Task
6
6
  from secator.utils import merge_opts
7
- from secator.celery_utils import CeleryData
8
- from secator.output_types import Info
9
7
 
10
8
 
11
9
  class Workflow(Runner):
@@ -17,65 +15,38 @@ class Workflow(Runner):
17
15
  from secator.celery import run_workflow
18
16
  return run_workflow.delay(args=args, kwargs=kwargs)
19
17
 
20
- def yielder(self):
21
- """Run workflow.
18
+ @classmethod
19
+ def s(cls, *args, **kwargs):
20
+ from secator.celery import run_workflow
21
+ return run_workflow.s(args=args, kwargs=kwargs)
22
22
 
23
- Yields:
24
- secator.output_types.OutputType: Secator output type.
25
- """
26
- # Task opts
27
- run_opts = self.run_opts.copy()
28
- run_opts['hooks'] = self._hooks.get(Task, {})
29
- run_opts.pop('no_poll', False)
30
-
31
- # Build Celery workflow
32
- workflow = self.build_celery_workflow(
33
- run_opts=run_opts,
34
- results=self.results
35
- )
36
- self.celery_ids = list(self.celery_ids_map.keys())
37
-
38
- # Run Celery workflow and get results
39
- if self.sync:
40
- self.print_item = False
41
- results = workflow.apply().get()
42
- else:
43
- result = workflow()
44
- self.celery_ids.append(str(result.id))
45
- self.celery_result = result
46
- yield Info(
47
- message=f'Celery task created: {self.celery_result.id}',
48
- task_id=self.celery_result.id
49
- )
50
- if self.no_poll:
51
- return
52
- results = CeleryData.iter_results(
53
- self.celery_result,
54
- ids_map=self.celery_ids_map,
55
- description=True,
56
- print_remote_info=self.print_remote_info,
57
- print_remote_title=f'[bold gold3]{self.__class__.__name__.capitalize()}[/] [bold magenta]{self.name}[/] results'
58
- )
59
-
60
- # Get workflow results
61
- yield from results
62
-
63
- def build_celery_workflow(self, run_opts={}, results=[]):
64
- """"Build Celery workflow.
23
+ def build_celery_workflow(self):
24
+ """Build Celery workflow for workflow execution.
65
25
 
66
26
  Returns:
67
- tuple(celery.chain, List[str]): Celery task chain, Celery task ids.
27
+ celery.Signature: Celery task signature.
68
28
  """
69
29
  from celery import chain
70
- from secator.celery import forward_results
30
+ from secator.celery import mark_runner_started, mark_runner_complete
31
+
32
+ # Prepare run options
33
+ opts = self.run_opts.copy()
34
+ opts['hooks'] = self._hooks.get(Task, {})
35
+ opts.pop('no_poll', False)
36
+
37
+ # Build task signatures
71
38
  sigs = self.get_tasks(
72
39
  self.config.tasks.toDict(),
73
40
  self.inputs,
74
41
  self.config.options,
75
- run_opts)
76
- sigs = [forward_results.si(results).set(queue='results')] + sigs + [forward_results.s().set(queue='results')]
77
- workflow = chain(*sigs)
78
- return workflow
42
+ opts)
43
+
44
+ # Build workflow chain with lifecycle management
45
+ return chain(
46
+ mark_runner_started.si(self).set(queue='results'),
47
+ *sigs,
48
+ mark_runner_complete.s(self).set(queue='results'),
49
+ )
79
50
 
80
51
  def get_tasks(self, config, inputs, workflow_opts, run_opts):
81
52
  """Get tasks recursively as Celery chains / chords.
secator/scans/__init__.py CHANGED
@@ -2,9 +2,9 @@ from secator.cli import ALL_SCANS
2
2
 
3
3
 
4
4
  def generate_class(config):
5
- from secator.runners import Workflow
5
+ from secator.runners import Scan
6
6
 
7
- class workflow(Workflow):
7
+ class scan(Scan):
8
8
  def __init__(self, inputs=[], **run_opts):
9
9
  hooks = run_opts.pop('hooks', {})
10
10
  results = run_opts.pop('results', [])
@@ -16,12 +16,12 @@ def generate_class(config):
16
16
  run_opts=run_opts,
17
17
  hooks=hooks,
18
18
  context=context)
19
- return workflow, config.name
19
+ return scan, config.name
20
20
 
21
21
 
22
22
  DYNAMIC_SCANS = {}
23
- for workflow in ALL_SCANS:
24
- cls, name = generate_class(workflow)
23
+ for scan in ALL_SCANS:
24
+ cls, name = generate_class(scan)
25
25
  DYNAMIC_SCANS[name] = cls
26
26
 
27
27
  globals().update(DYNAMIC_SCANS)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secator
3
- Version: 0.10.1a2
3
+ Version: 0.10.1a4
4
4
  Summary: The pentester's swiss knife.
5
5
  Project-URL: Homepage, https://github.com/freelabz/secator
6
6
  Project-URL: Issues, https://github.com/freelabz/secator/issues
@@ -1,10 +1,10 @@
1
1
  secator/.gitignore,sha256=da8MUc3hdb6Mo0WjZu2upn5uZMbXcBGvhdhTQ1L89HI,3093
2
2
  secator/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- secator/celery.py,sha256=9KXKv4EamJYJrHt_Ppn7aIp1AiFaTn2V0J_tZBwtWK0,8802
4
- secator/celery_signals.py,sha256=HobT7hCbVKPEHvCNwxCvQxFVUyocU1kkrTXi67b1DDw,4346
5
- secator/celery_utils.py,sha256=UWqLZpUaOXcztC_GD6uEDLiP8bGmD3WiTQN-u3lialg,7712
3
+ secator/celery.py,sha256=8BV-G_r0gYZ48oYejMpvY4wvPJwYcjZ3NHnpjzOkB94,9706
4
+ secator/celery_signals.py,sha256=iumfx7tTeoavAbHijBtij0JzeIqElxQldNZtuZmFY_U,4456
5
+ secator/celery_utils.py,sha256=bW1yzMCjfIiesU4SOVNVuy0I8HukJyh8KmNB4w0woJM,8857
6
6
  secator/cli.py,sha256=3_tTTusW12MCejFgtOeYjiedjrJpyQj_gsCK8FkTMJA,43922
7
- secator/config.py,sha256=xItKM29yvMqzNZZygSNZXZ2V9vJbTdRuLTfIoRfP3XE,19653
7
+ secator/config.py,sha256=CdVBh6d4k13SpkQKyHQfMFHgkLypUH07kAKLmCJJO1w,19688
8
8
  secator/decorators.py,sha256=3kYadCz6haIZtnjkFHSRfenTdc6Yu7bHd-0IVjhD72w,13902
9
9
  secator/definitions.py,sha256=gFtLT9fjNtX_1qkiCjNfQyCvYq07IhScsQzX4o20_SE,3084
10
10
  secator/installer.py,sha256=Q5qmGbxGmuhysEA9YovTpy-YY2TxxFskhrzSX44c42E,17971
@@ -50,7 +50,7 @@ secator/exporters/txt.py,sha256=oMtr22di6cqyE_5yJoiWP-KElrI5QgvK1cOUrj7H7js,730
50
50
  secator/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
51
51
  secator/hooks/gcs.py,sha256=MIhntyWYz9BZdTXhWl5JznaczSq1_7fl3TVqPufuTSo,1490
52
52
  secator/hooks/mongodb.py,sha256=XKbm_SrcSbQ2koILWvhzSg4tqdvHXgX5aU5x46Edu1s,7716
53
- secator/output_types/__init__.py,sha256=LxCW0K1f2vdgUapc4pIEsUpBfC0TQVvqo7T57rGuZGk,1159
53
+ secator/output_types/__init__.py,sha256=L3q9NXPaW0TGeidx5YH-6dWhOXD1GizztAcL2lqIA8Q,1221
54
54
  secator/output_types/_base.py,sha256=OgS6ICt66TzPsqo1JZwRIIwbng2HRX1i_u5qbUECgNk,2820
55
55
  secator/output_types/error.py,sha256=39gpEJfKM2EuyOhD9lSkjjna2QicMvnLdFav6kHmhlg,1529
56
56
  secator/output_types/exploit.py,sha256=-BKTqPBg94rVgjw8YSmcYuBCI2x-73WwMd9ITP9qr3Y,1750
@@ -60,6 +60,7 @@ secator/output_types/port.py,sha256=JdqXnEF8XuwaWFMT8Vghj7fKLwtsImuUdRfMmITgmWM,
60
60
  secator/output_types/progress.py,sha256=MIbmnrLHNodLL42UgiaqLHL0OG5-w6mtUrhn0ZhksjA,1343
61
61
  secator/output_types/record.py,sha256=HnsKxlIhkgswA_Yjz7BZ1vDjP53l6OJ0BCOtCSDwCSY,1250
62
62
  secator/output_types/stat.py,sha256=90oN2Ghc4k0B0FOdp6MOWiNgmXMmLHYknjunDeEKKRE,1129
63
+ secator/output_types/state.py,sha256=-kQs_P-v_d_J8mgMRJA9Pa0SaOVHN__Fq_ateDc0tiA,1038
63
64
  secator/output_types/subdomain.py,sha256=ivJ_2kmrJ8hdB8wmvRJYlKV1BcE3Cds_vAI_5wL7ES4,1344
64
65
  secator/output_types/tag.py,sha256=_XEqWAvAvmi7nd2ldfEE71zQx97jTSph2iDHkeqGTyk,1470
65
66
  secator/output_types/target.py,sha256=lmPw2aFOGIOFG4XXo6vNVZBBAZlnApJjyDVepDY54TU,871
@@ -68,14 +69,14 @@ secator/output_types/user_account.py,sha256=rm10somxyu30JHjj629IkR15Nhahylud_fVO
68
69
  secator/output_types/vulnerability.py,sha256=nF7OT9zGez8sZvLrkhjBOORjVi8hCqfCYUFq3eZ_ywo,2870
69
70
  secator/output_types/warning.py,sha256=47GtmG083GqGPb_R5JDFmARJ9Mqrme58UxwJhgdGPuI,853
70
71
  secator/runners/__init__.py,sha256=EBbOk37vkBy9p8Hhrbi-2VtM_rTwQ3b-0ggTyiD22cE,290
71
- secator/runners/_base.py,sha256=T9gjOqe-UPDHe5ZdVRBtUtxTefRgDcq9JV08F6UV5ZU,29596
72
+ secator/runners/_base.py,sha256=3LNWH_jEfaZbtgHYzfl6mFmUJ9IoC62vKmLlv-MmTAw,31293
72
73
  secator/runners/_helpers.py,sha256=QhJmdmFdu5XSx3LBFf4Q4Hy2EXS6bLGnJUq8G7C6f68,2410
73
74
  secator/runners/celery.py,sha256=bqvDTTdoHiGRCt0FRvlgFHQ_nsjKMP5P0PzGbwfCj_0,425
74
- secator/runners/command.py,sha256=PqCOHDKJXvG4weB8mXDTElGxc8i8pK2RoyTKUBpHASU,25480
75
- secator/runners/scan.py,sha256=Pab_o_liI5fhlv2OOwYNmonz5JFYYVqtQFf9eyAQpiE,1071
76
- secator/runners/task.py,sha256=f2AduWpIy8JHK-Qitl_2Kh0fia573_YHAyAlV6MsJ50,2068
77
- secator/runners/workflow.py,sha256=XEhBfL-f3vGH0HgEPnj62d8ITxjH_tPXiNSVkaonuwQ,3862
78
- secator/scans/__init__.py,sha256=nlNLiRl7Vu--c_iXClFFcagMd_b_OWKitq8tX1-1krQ,641
75
+ secator/runners/command.py,sha256=9AvjZgSXctP8D-ffPCtlnXEiGqTeaD2wVGhiGNuROb0,25469
76
+ secator/runners/scan.py,sha256=9FjDsFmQrAWfA6crWkCJaVqG3-t2HBVjcsv4UQp_9b8,1500
77
+ secator/runners/task.py,sha256=59jPXKSxFtSNXsm6VTAz8li2jxpM0Bkcgcn77HIDCrY,1869
78
+ secator/runners/workflow.py,sha256=qldnRm7r_SCvRHJFkZ7eaml62RZkOeCdT18PU357grY,2982
79
+ secator/scans/__init__.py,sha256=1EEbngbDbvWxmeDYC6uux00WWy1v5qHtSpk6NVz27rM,617
79
80
  secator/serializers/__init__.py,sha256=OP5cmFl77ovgSCW_IDcZ21St2mUt5UK4QHfrsK2KvH8,248
80
81
  secator/serializers/dataclass.py,sha256=RqICpfsYWGjHAACAA2h2jZ_69CFHim4VZwcBqowGMcQ,1010
81
82
  secator/serializers/json.py,sha256=UJwAymRzjF-yBKOgz1MTOyBhQcdQg7fOKRXgmHIu8fo,411
@@ -109,8 +110,8 @@ secator/tasks/searchsploit.py,sha256=gvtLZbL2hzAZ07Cf0cSj2Qs0GvWK94XyHvoPFsetXu8
109
110
  secator/tasks/subfinder.py,sha256=C6W5NnXT92OUB1aSS9IYseqdI3wDMAz70TOEl8X-o3U,1213
110
111
  secator/tasks/wpscan.py,sha256=036ywiEqZfX_Bt071U7qIm7bi6pNk7vodflmuslJurA,5550
111
112
  secator/workflows/__init__.py,sha256=ivpZHiYYlj4JqlXLRmB9cmAPUGdk8QcUrCRL34hIqEA,665
112
- secator-0.10.1a2.dist-info/METADATA,sha256=YgZWsub4cdLtNiz-2UKkPKhLAi8mx0wFIPwGjnYU794,14726
113
- secator-0.10.1a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
114
- secator-0.10.1a2.dist-info/entry_points.txt,sha256=lPgsqqUXWgiuGSfKy-se5gHdQlAXIwS_A46NYq7Acic,44
115
- secator-0.10.1a2.dist-info/licenses/LICENSE,sha256=19W5Jsy4WTctNkqmZIqLRV1gTDOp01S3LDj9iSgWaJ0,2867
116
- secator-0.10.1a2.dist-info/RECORD,,
113
+ secator-0.10.1a4.dist-info/METADATA,sha256=EWh5NvMc8ZTE9xYmtbbE7eikaHPHgQfTasXPWlsyQdA,14726
114
+ secator-0.10.1a4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
115
+ secator-0.10.1a4.dist-info/entry_points.txt,sha256=lPgsqqUXWgiuGSfKy-se5gHdQlAXIwS_A46NYq7Acic,44
116
+ secator-0.10.1a4.dist-info/licenses/LICENSE,sha256=19W5Jsy4WTctNkqmZIqLRV1gTDOp01S3LDj9iSgWaJ0,2867
117
+ secator-0.10.1a4.dist-info/RECORD,,