secator 0.22.0__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.
- secator/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +453 -0
- secator/celery_signals.py +138 -0
- secator/celery_utils.py +320 -0
- secator/cli.py +2035 -0
- secator/cli_helper.py +395 -0
- secator/click.py +87 -0
- secator/config.py +670 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +8 -0
- secator/configs/profiles/all_ports.yaml +7 -0
- secator/configs/profiles/full.yaml +31 -0
- secator/configs/profiles/http_headless.yaml +7 -0
- secator/configs/profiles/http_record.yaml +8 -0
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/passive.yaml +11 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +31 -0
- secator/configs/scans/host.yaml +23 -0
- secator/configs/scans/network.yaml +30 -0
- secator/configs/scans/subdomain.yaml +27 -0
- secator/configs/scans/url.yaml +19 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +48 -0
- secator/configs/workflows/code_scan.yaml +29 -0
- secator/configs/workflows/domain_recon.yaml +46 -0
- secator/configs/workflows/host_recon.yaml +95 -0
- secator/configs/workflows/subdomain_recon.yaml +120 -0
- secator/configs/workflows/url_bypass.yaml +15 -0
- secator/configs/workflows/url_crawl.yaml +98 -0
- secator/configs/workflows/url_dirsearch.yaml +62 -0
- secator/configs/workflows/url_fuzz.yaml +68 -0
- secator/configs/workflows/url_params_fuzz.yaml +66 -0
- secator/configs/workflows/url_secrets_hunt.yaml +23 -0
- secator/configs/workflows/url_vuln.yaml +91 -0
- secator/configs/workflows/user_hunt.yaml +29 -0
- secator/configs/workflows/wordpress.yaml +38 -0
- secator/cve.py +718 -0
- secator/decorators.py +7 -0
- secator/definitions.py +168 -0
- secator/exporters/__init__.py +14 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +37 -0
- secator/exporters/gdrive.py +123 -0
- secator/exporters/json.py +16 -0
- secator/exporters/table.py +36 -0
- secator/exporters/txt.py +28 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/gcs.py +80 -0
- secator/hooks/mongodb.py +281 -0
- secator/installer.py +694 -0
- secator/loader.py +128 -0
- secator/output_types/__init__.py +49 -0
- secator/output_types/_base.py +108 -0
- secator/output_types/certificate.py +78 -0
- secator/output_types/domain.py +50 -0
- secator/output_types/error.py +42 -0
- secator/output_types/exploit.py +58 -0
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +47 -0
- secator/output_types/port.py +55 -0
- secator/output_types/progress.py +36 -0
- secator/output_types/record.py +36 -0
- secator/output_types/stat.py +41 -0
- secator/output_types/state.py +29 -0
- secator/output_types/subdomain.py +45 -0
- secator/output_types/tag.py +69 -0
- secator/output_types/target.py +38 -0
- secator/output_types/url.py +112 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +101 -0
- secator/output_types/warning.py +30 -0
- secator/report.py +140 -0
- secator/rich.py +130 -0
- secator/runners/__init__.py +14 -0
- secator/runners/_base.py +1240 -0
- secator/runners/_helpers.py +218 -0
- secator/runners/celery.py +18 -0
- secator/runners/command.py +1178 -0
- secator/runners/python.py +126 -0
- secator/runners/scan.py +87 -0
- secator/runners/task.py +81 -0
- secator/runners/workflow.py +168 -0
- secator/scans/__init__.py +29 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +39 -0
- secator/serializers/json.py +45 -0
- secator/serializers/regex.py +25 -0
- secator/tasks/__init__.py +8 -0
- secator/tasks/_categories.py +487 -0
- secator/tasks/arjun.py +113 -0
- secator/tasks/arp.py +53 -0
- secator/tasks/arpscan.py +70 -0
- secator/tasks/bbot.py +372 -0
- secator/tasks/bup.py +118 -0
- secator/tasks/cariddi.py +193 -0
- secator/tasks/dalfox.py +87 -0
- secator/tasks/dirsearch.py +84 -0
- secator/tasks/dnsx.py +186 -0
- secator/tasks/feroxbuster.py +93 -0
- secator/tasks/ffuf.py +135 -0
- secator/tasks/fping.py +85 -0
- secator/tasks/gau.py +102 -0
- secator/tasks/getasn.py +60 -0
- secator/tasks/gf.py +36 -0
- secator/tasks/gitleaks.py +96 -0
- secator/tasks/gospider.py +84 -0
- secator/tasks/grype.py +109 -0
- secator/tasks/h8mail.py +75 -0
- secator/tasks/httpx.py +167 -0
- secator/tasks/jswhois.py +36 -0
- secator/tasks/katana.py +203 -0
- secator/tasks/maigret.py +87 -0
- secator/tasks/mapcidr.py +42 -0
- secator/tasks/msfconsole.py +179 -0
- secator/tasks/naabu.py +85 -0
- secator/tasks/nmap.py +487 -0
- secator/tasks/nuclei.py +151 -0
- secator/tasks/search_vulns.py +225 -0
- secator/tasks/searchsploit.py +109 -0
- secator/tasks/sshaudit.py +299 -0
- secator/tasks/subfinder.py +48 -0
- secator/tasks/testssl.py +283 -0
- secator/tasks/trivy.py +130 -0
- secator/tasks/trufflehog.py +240 -0
- secator/tasks/urlfinder.py +100 -0
- secator/tasks/wafw00f.py +106 -0
- secator/tasks/whois.py +34 -0
- secator/tasks/wpprobe.py +116 -0
- secator/tasks/wpscan.py +202 -0
- secator/tasks/x8.py +94 -0
- secator/tasks/xurlfind3r.py +83 -0
- secator/template.py +294 -0
- secator/thread.py +24 -0
- secator/tree.py +196 -0
- secator/utils.py +922 -0
- secator/utils_test.py +297 -0
- secator/workflows/__init__.py +29 -0
- secator-0.22.0.dist-info/METADATA +447 -0
- secator-0.22.0.dist-info/RECORD +150 -0
- secator-0.22.0.dist-info/WHEEL +4 -0
- secator-0.22.0.dist-info/entry_points.txt +2 -0
- secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/.gitignore
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# ---> Python
|
|
2
|
+
# Byte-compiled / optimized / DLL files
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*$py.class
|
|
6
|
+
|
|
7
|
+
# C extensions
|
|
8
|
+
*.so
|
|
9
|
+
|
|
10
|
+
# Distribution / packaging
|
|
11
|
+
.Python
|
|
12
|
+
build/
|
|
13
|
+
develop-eggs/
|
|
14
|
+
dist/
|
|
15
|
+
downloads/
|
|
16
|
+
eggs/
|
|
17
|
+
.eggs/
|
|
18
|
+
lib/
|
|
19
|
+
lib64/
|
|
20
|
+
parts/
|
|
21
|
+
sdist/
|
|
22
|
+
var/
|
|
23
|
+
wheels/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
#Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# poetry
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
103
|
+
#poetry.lock
|
|
104
|
+
|
|
105
|
+
# pdm
|
|
106
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
107
|
+
#pdm.lock
|
|
108
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
109
|
+
# in version control.
|
|
110
|
+
# https://pdm.fming.dev/#use-with-ide
|
|
111
|
+
.pdm.toml
|
|
112
|
+
|
|
113
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
114
|
+
__pypackages__/
|
|
115
|
+
|
|
116
|
+
# Celery stuff
|
|
117
|
+
celerybeat-schedule
|
|
118
|
+
celerybeat.pid
|
|
119
|
+
|
|
120
|
+
# SageMath parsed files
|
|
121
|
+
*.sage.py
|
|
122
|
+
|
|
123
|
+
# Environments
|
|
124
|
+
.env
|
|
125
|
+
.venv
|
|
126
|
+
env/
|
|
127
|
+
venv/
|
|
128
|
+
ENV/
|
|
129
|
+
env.bak/
|
|
130
|
+
venv.bak/
|
|
131
|
+
|
|
132
|
+
# Spyder project settings
|
|
133
|
+
.spyderproject
|
|
134
|
+
.spyproject
|
|
135
|
+
|
|
136
|
+
# Rope project settings
|
|
137
|
+
.ropeproject
|
|
138
|
+
|
|
139
|
+
# mkdocs documentation
|
|
140
|
+
/site
|
|
141
|
+
|
|
142
|
+
# mypy
|
|
143
|
+
.mypy_cache/
|
|
144
|
+
.dmypy.json
|
|
145
|
+
dmypy.json
|
|
146
|
+
|
|
147
|
+
# Pyre type checker
|
|
148
|
+
.pyre/
|
|
149
|
+
|
|
150
|
+
# pytype static type analyzer
|
|
151
|
+
.pytype/
|
|
152
|
+
|
|
153
|
+
# Cython debug symbols
|
|
154
|
+
cython_debug/
|
|
155
|
+
|
|
156
|
+
# PyCharm
|
|
157
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
158
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
159
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
160
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
161
|
+
#.idea/
|
|
162
|
+
|
secator/__init__.py
ADDED
|
File without changes
|
secator/celery.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from time import time
|
|
7
|
+
|
|
8
|
+
from celery import Celery, chord
|
|
9
|
+
from celery.canvas import signature
|
|
10
|
+
from celery.app import trace
|
|
11
|
+
|
|
12
|
+
from rich.logging import RichHandler
|
|
13
|
+
from retry import retry
|
|
14
|
+
|
|
15
|
+
from secator.celery_signals import IN_CELERY_WORKER_PROCESS, setup_handlers
|
|
16
|
+
from secator.config import CONFIG
|
|
17
|
+
from secator.output_types import Info
|
|
18
|
+
from secator.rich import console
|
|
19
|
+
from secator.runners import Scan, Task, Workflow
|
|
20
|
+
from secator.utils import (debug, deduplicate, flatten, should_update)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
#---------#
|
|
24
|
+
# Logging #
|
|
25
|
+
#---------#
|
|
26
|
+
|
|
27
|
+
rich_handler = RichHandler(rich_tracebacks=True)
|
|
28
|
+
rich_handler.setLevel(logging.INFO)
|
|
29
|
+
logging.basicConfig(
|
|
30
|
+
level='NOTSET',
|
|
31
|
+
format="%(threadName)s:%(message)s",
|
|
32
|
+
datefmt="[%X]",
|
|
33
|
+
handlers=[rich_handler],
|
|
34
|
+
force=True)
|
|
35
|
+
logging.getLogger('kombu').setLevel(logging.ERROR)
|
|
36
|
+
logging.getLogger('celery').setLevel(logging.DEBUG if 'celery.debug' in CONFIG.debug or 'celery.*' in CONFIG.debug else logging.WARNING) # noqa: E501
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
trace.LOG_SUCCESS = "Task %(name)s[%(id)s] succeeded in %(runtime)ss"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
#------------#
|
|
42
|
+
# Celery app #
|
|
43
|
+
#------------#
|
|
44
|
+
|
|
45
|
+
app = Celery(__name__)
|
|
46
|
+
app.conf.update({
|
|
47
|
+
# Content types
|
|
48
|
+
'accept_content': ['application/x-python-serialize', 'application/json'],
|
|
49
|
+
|
|
50
|
+
# Broker config
|
|
51
|
+
'broker_url': CONFIG.celery.broker_url,
|
|
52
|
+
'broker_transport_options': json.loads(CONFIG.celery.broker_transport_options) if CONFIG.celery.broker_transport_options else { # noqa: E501
|
|
53
|
+
'data_folder_in': CONFIG.dirs.celery_data,
|
|
54
|
+
'data_folder_out': CONFIG.dirs.celery_data,
|
|
55
|
+
'control_folder': CONFIG.dirs.celery_data,
|
|
56
|
+
'visibility_timeout': CONFIG.celery.broker_visibility_timeout,
|
|
57
|
+
},
|
|
58
|
+
'broker_connection_retry_on_startup': True,
|
|
59
|
+
'broker_pool_limit': CONFIG.celery.broker_pool_limit,
|
|
60
|
+
'broker_connection_timeout': CONFIG.celery.broker_connection_timeout,
|
|
61
|
+
|
|
62
|
+
# Result backend config
|
|
63
|
+
'result_backend': CONFIG.celery.result_backend,
|
|
64
|
+
'result_expires': CONFIG.celery.result_expires,
|
|
65
|
+
'result_backend_transport_options': json.loads(CONFIG.celery.result_backend_transport_options) if CONFIG.celery.result_backend_transport_options else {}, # noqa: E501
|
|
66
|
+
'result_extended': not CONFIG.addons.mongodb.enabled,
|
|
67
|
+
'result_backend_thread_safe': True,
|
|
68
|
+
'result_serializer': 'pickle',
|
|
69
|
+
'result_accept_content': ['application/x-python-serialize'],
|
|
70
|
+
|
|
71
|
+
# Task config
|
|
72
|
+
'task_acks_late': CONFIG.celery.task_acks_late,
|
|
73
|
+
'task_compression': 'gzip',
|
|
74
|
+
'task_create_missing_queues': True,
|
|
75
|
+
'task_eager_propagates': False,
|
|
76
|
+
'task_reject_on_worker_lost': CONFIG.celery.task_reject_on_worker_lost,
|
|
77
|
+
'task_routes': {
|
|
78
|
+
'secator.celery.run_workflow': {'queue': 'celery'},
|
|
79
|
+
'secator.celery.run_scan': {'queue': 'celery'},
|
|
80
|
+
'secator.celery.run_task': {'queue': 'celery'},
|
|
81
|
+
'secator.celery.forward_results': {'queue': 'results'},
|
|
82
|
+
'secator.hooks.mongodb.*': {'queue': 'mongodb'}
|
|
83
|
+
},
|
|
84
|
+
'task_store_eager_result': True,
|
|
85
|
+
'task_send_sent_event': CONFIG.celery.task_send_sent_event,
|
|
86
|
+
'task_serializer': 'pickle',
|
|
87
|
+
'task_accept_content': ['application/x-python-serialize'],
|
|
88
|
+
|
|
89
|
+
# Event config
|
|
90
|
+
'event_serializer': 'pickle',
|
|
91
|
+
'event_accept_content': ['application/x-python-serialize'],
|
|
92
|
+
|
|
93
|
+
# Worker config
|
|
94
|
+
# 'worker_direct': True, # TODO: consider enabling this to allow routing to specific workers
|
|
95
|
+
'worker_max_tasks_per_child': CONFIG.celery.worker_max_tasks_per_child,
|
|
96
|
+
# 'worker_max_memory_per_child': 100000 # TODO: consider enabling this
|
|
97
|
+
'worker_pool_restarts': True,
|
|
98
|
+
'worker_prefetch_multiplier': CONFIG.celery.worker_prefetch_multiplier,
|
|
99
|
+
'worker_send_task_events': CONFIG.celery.worker_send_task_events
|
|
100
|
+
})
|
|
101
|
+
app.autodiscover_tasks(['secator.hooks.mongodb'], related_name=None)
|
|
102
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
103
|
+
setup_handlers()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@retry(Exception, tries=3, delay=2)
|
|
107
|
+
def update_state(celery_task, task, force=False):
|
|
108
|
+
"""Update task state to add metadata information."""
|
|
109
|
+
if not IN_CELERY_WORKER_PROCESS:
|
|
110
|
+
return
|
|
111
|
+
if task.no_live_updates:
|
|
112
|
+
return
|
|
113
|
+
if not force and not should_update(CONFIG.runners.backend_update_frequency, task.last_updated_celery):
|
|
114
|
+
return
|
|
115
|
+
task.last_updated_celery = time()
|
|
116
|
+
debug(
|
|
117
|
+
'',
|
|
118
|
+
sub='celery.state',
|
|
119
|
+
id=celery_task.request.id,
|
|
120
|
+
obj={task.unique_name: task.status, 'count': task.self_findings_count},
|
|
121
|
+
obj_after=False,
|
|
122
|
+
verbose=True
|
|
123
|
+
)
|
|
124
|
+
return celery_task.update_state(
|
|
125
|
+
state='RUNNING',
|
|
126
|
+
meta=task.celery_state
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def revoke_task(task_id, task_name=None):
|
|
131
|
+
message = f'Revoked task {task_id}'
|
|
132
|
+
if task_name:
|
|
133
|
+
message += f' ({task_name})'
|
|
134
|
+
app.control.revoke(task_id, terminate=True)
|
|
135
|
+
console.print(Info(message=message))
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
#--------------#
|
|
139
|
+
# Celery tasks #
|
|
140
|
+
#--------------#
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def chunker(seq, size):
|
|
144
|
+
return (seq[pos:pos + size] for pos in range(0, len(seq), size))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@app.task(bind=True)
|
|
148
|
+
def run_task(self, args=[], kwargs={}):
|
|
149
|
+
console.print(Info(message=f'Running task {self.request.id}'))
|
|
150
|
+
if 'context' not in kwargs:
|
|
151
|
+
kwargs['context'] = {}
|
|
152
|
+
kwargs['context']['celery_id'] = self.request.id
|
|
153
|
+
task = Task(*args, **kwargs)
|
|
154
|
+
task.run()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@app.task(bind=True)
|
|
158
|
+
def run_workflow(self, args=[], kwargs={}):
|
|
159
|
+
console.print(Info(message=f'Running workflow {self.request.id}'))
|
|
160
|
+
if 'context' not in kwargs:
|
|
161
|
+
kwargs['context'] = {}
|
|
162
|
+
kwargs['context']['celery_id'] = self.request.id
|
|
163
|
+
workflow = Workflow(*args, **kwargs)
|
|
164
|
+
workflow.run()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@app.task(bind=True)
|
|
168
|
+
def run_scan(self, args=[], kwargs={}):
|
|
169
|
+
console.print(Info(message=f'Running scan {self.request.id}'))
|
|
170
|
+
if 'context' not in kwargs:
|
|
171
|
+
kwargs['context'] = {}
|
|
172
|
+
kwargs['context']['celery_id'] = self.request.id
|
|
173
|
+
scan = Scan(*args, **kwargs)
|
|
174
|
+
scan.run()
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.task(bind=True)
|
|
178
|
+
def run_command(self, results, name, targets, opts={}):
|
|
179
|
+
# Set Celery request id in context
|
|
180
|
+
context = opts.get('context', {})
|
|
181
|
+
context['celery_id'] = self.request.id
|
|
182
|
+
context['worker_name'] = os.environ.get('WORKER_NAME', 'unknown')
|
|
183
|
+
|
|
184
|
+
# Set routing key in context
|
|
185
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
186
|
+
quiet = not CONFIG.cli.worker_command_verbose
|
|
187
|
+
opts.update({
|
|
188
|
+
'print_item': True,
|
|
189
|
+
'print_line': True,
|
|
190
|
+
'print_cmd': True,
|
|
191
|
+
'print_target': True,
|
|
192
|
+
'print_profiles': True,
|
|
193
|
+
'quiet': quiet
|
|
194
|
+
})
|
|
195
|
+
routing_key = self.request.delivery_info['routing_key']
|
|
196
|
+
context['routing_key'] = routing_key
|
|
197
|
+
debug(f'Task "{name}" running with routing key "{routing_key}"', sub='celery.state')
|
|
198
|
+
|
|
199
|
+
# Flatten + dedupe + filter results
|
|
200
|
+
results = forward_results(results)
|
|
201
|
+
|
|
202
|
+
# Set task opts
|
|
203
|
+
opts['context'] = context
|
|
204
|
+
opts['results'] = results
|
|
205
|
+
opts['sync'] = True
|
|
206
|
+
|
|
207
|
+
# Initialize task
|
|
208
|
+
sync = not IN_CELERY_WORKER_PROCESS
|
|
209
|
+
task_cls = Task.get_task_class(name)
|
|
210
|
+
task = task_cls(targets, **opts)
|
|
211
|
+
chunk_it = task.needs_chunking(sync)
|
|
212
|
+
task.has_children = chunk_it
|
|
213
|
+
task.mark_started()
|
|
214
|
+
update_state(self, task, force=True)
|
|
215
|
+
|
|
216
|
+
# Chunk task if needed
|
|
217
|
+
if chunk_it:
|
|
218
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
219
|
+
console.print(Info(message=f'Task {name} requires chunking'))
|
|
220
|
+
workflow = break_task(task, opts, results=results)
|
|
221
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
222
|
+
console.print(Info(message=f'Task {name} successfully broken into {len(workflow)} chunks'))
|
|
223
|
+
update_state(self, task, force=True)
|
|
224
|
+
console.print(Info(message=f'Task {name} updated state, replacing task with Celery chord workflow'))
|
|
225
|
+
return replace(self, workflow)
|
|
226
|
+
|
|
227
|
+
# Update state live
|
|
228
|
+
for _ in task:
|
|
229
|
+
update_state(self, task)
|
|
230
|
+
update_state(self, task, force=True)
|
|
231
|
+
|
|
232
|
+
if CONFIG.addons.mongodb.enabled:
|
|
233
|
+
return [r._uuid for r in task.results]
|
|
234
|
+
return task.results
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@app.task
|
|
238
|
+
def forward_results(results):
|
|
239
|
+
"""Forward results to the next task (bridge task).
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
results (list): Results to forward.
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
list: List of uuids.
|
|
246
|
+
"""
|
|
247
|
+
if isinstance(results, list):
|
|
248
|
+
for ix, item in enumerate(results):
|
|
249
|
+
if isinstance(item, dict) and 'results' in item:
|
|
250
|
+
results[ix] = item['results']
|
|
251
|
+
elif 'results' in results:
|
|
252
|
+
results = results['results']
|
|
253
|
+
|
|
254
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
255
|
+
console.print(Info(message=f'Deduplicating {len(results)} results'))
|
|
256
|
+
|
|
257
|
+
results = flatten(results)
|
|
258
|
+
if IN_CELERY_WORKER_PROCESS and CONFIG.addons.mongodb.enabled:
|
|
259
|
+
console.print(Info(message=f'Extracting uuids from {len(results)} results'))
|
|
260
|
+
uuids = [r._uuid for r in results if hasattr(r, '_uuid')]
|
|
261
|
+
uuids.extend([r for r in results if isinstance(r, str)])
|
|
262
|
+
results = list(set(uuids))
|
|
263
|
+
else:
|
|
264
|
+
results = deduplicate(results, attr='_uuid')
|
|
265
|
+
|
|
266
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
267
|
+
console.print(Info(message=f'Forwarded {len(results)} flattened and deduplicated results'))
|
|
268
|
+
|
|
269
|
+
return results
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.task
|
|
273
|
+
def mark_runner_started(results, runner, enable_hooks=True):
|
|
274
|
+
"""Mark a runner as started and run on_start hooks.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
results (List): Previous results.
|
|
278
|
+
runner (Runner): Secator runner instance.
|
|
279
|
+
enable_hooks (bool): Enable hooks.
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
list: Runner results
|
|
283
|
+
"""
|
|
284
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
285
|
+
console.print(Info(message=f'Runner {runner.unique_name} has started, running mark_started'))
|
|
286
|
+
debug(f'Runner {runner.unique_name} has started, running mark_started', sub='celery')
|
|
287
|
+
if results:
|
|
288
|
+
results = forward_results(results)
|
|
289
|
+
runner.enable_hooks = enable_hooks
|
|
290
|
+
if IN_CELERY_WORKER_PROCESS and CONFIG.addons.mongodb.enabled:
|
|
291
|
+
from secator.hooks.mongodb import get_results
|
|
292
|
+
results = get_results(results)
|
|
293
|
+
for item in results:
|
|
294
|
+
runner.add_result(item, print=False)
|
|
295
|
+
runner.mark_started()
|
|
296
|
+
if IN_CELERY_WORKER_PROCESS and CONFIG.addons.mongodb.enabled:
|
|
297
|
+
return [r._uuid for r in runner.results]
|
|
298
|
+
return runner.results
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@app.task
|
|
302
|
+
def mark_runner_completed(results, runner, enable_hooks=True):
|
|
303
|
+
"""Mark a runner as completed and run on_end hooks.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
results (list): Task results
|
|
307
|
+
runner (Runner): Secator runner instance
|
|
308
|
+
enable_hooks (bool): Enable hooks.
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
list: Final results
|
|
312
|
+
"""
|
|
313
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
314
|
+
console.print(Info(message=f'Runner {runner.unique_name} has finished, running mark_completed'))
|
|
315
|
+
debug(f'Runner {runner.unique_name} has finished, running mark_completed', sub='celery')
|
|
316
|
+
results = forward_results(results)
|
|
317
|
+
runner.enable_hooks = enable_hooks
|
|
318
|
+
if IN_CELERY_WORKER_PROCESS and CONFIG.addons.mongodb.enabled:
|
|
319
|
+
from secator.hooks.mongodb import get_results
|
|
320
|
+
results = get_results(results)
|
|
321
|
+
for item in results:
|
|
322
|
+
runner.add_result(item, print=False)
|
|
323
|
+
runner.mark_completed()
|
|
324
|
+
if IN_CELERY_WORKER_PROCESS and CONFIG.addons.mongodb.enabled:
|
|
325
|
+
return [r._uuid for r in runner.results]
|
|
326
|
+
return runner.results
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
#--------------#
|
|
330
|
+
# Celery utils #
|
|
331
|
+
#--------------#
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def is_celery_worker_alive():
|
|
335
|
+
"""Check if a Celery worker is available."""
|
|
336
|
+
result = app.control.broadcast('ping', reply=True, limit=1, timeout=1)
|
|
337
|
+
result = bool(result)
|
|
338
|
+
if result:
|
|
339
|
+
console.print(Info(message='Celery worker is available, running remotely'))
|
|
340
|
+
else:
|
|
341
|
+
console.print(Info(message='No Celery worker available, running locally'))
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def replace(task_instance, sig):
|
|
346
|
+
"""Replace this task, with a new task inheriting the task id.
|
|
347
|
+
|
|
348
|
+
Execution of the host task ends immediately and no subsequent statements
|
|
349
|
+
will be run.
|
|
350
|
+
|
|
351
|
+
.. versionadded:: 4.0
|
|
352
|
+
|
|
353
|
+
Arguments:
|
|
354
|
+
sig (Signature): signature to replace with.
|
|
355
|
+
visitor (StampingVisitor): Visitor API object.
|
|
356
|
+
|
|
357
|
+
Raises:
|
|
358
|
+
~@Ignore: This is always raised when called in asynchronous context.
|
|
359
|
+
It is best to always use ``return self.replace(...)`` to convey
|
|
360
|
+
to the reader that the task won't continue after being replaced.
|
|
361
|
+
"""
|
|
362
|
+
chord = task_instance.request.chord
|
|
363
|
+
sig.freeze(task_instance.request.id)
|
|
364
|
+
replaced_task_nesting = task_instance.request.get('replaced_task_nesting', 0) + 1
|
|
365
|
+
sig.set(
|
|
366
|
+
chord=chord,
|
|
367
|
+
group_id=task_instance.request.group,
|
|
368
|
+
group_index=task_instance.request.group_index,
|
|
369
|
+
root_id=task_instance.request.root_id,
|
|
370
|
+
replaced_task_nesting=replaced_task_nesting
|
|
371
|
+
)
|
|
372
|
+
import psutil
|
|
373
|
+
import os
|
|
374
|
+
process = psutil.Process(os.getpid())
|
|
375
|
+
length = len(task_instance.request.chain) if task_instance.request.chain else 0
|
|
376
|
+
# console.print(f'Adding {length} chain tasks from request chain')
|
|
377
|
+
for ix, t in enumerate(reversed(task_instance.request.chain or [])):
|
|
378
|
+
console.print(Info(message=f'Adding chain task {t.name} from request chain ({ix + 1}/{length})'))
|
|
379
|
+
chain_task = signature(t, app=task_instance.app)
|
|
380
|
+
chain_task.set(replaced_task_nesting=replaced_task_nesting)
|
|
381
|
+
sig |= chain_task
|
|
382
|
+
del chain_task
|
|
383
|
+
del t
|
|
384
|
+
memory_bytes = process.memory_info().rss
|
|
385
|
+
console.print(Info(message=f'Memory usage: {memory_bytes / 1024 / 1024:.2f} MB (chain task {ix + 1}/{length})'))
|
|
386
|
+
gc.collect()
|
|
387
|
+
return task_instance.on_replace(sig)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def break_task(task, task_opts, results=[]):
|
|
391
|
+
"""Break a task into multiple of the same type."""
|
|
392
|
+
chunks = task.inputs
|
|
393
|
+
if task.input_chunk_size > 1:
|
|
394
|
+
chunks = list(chunker(task.inputs, task.input_chunk_size))
|
|
395
|
+
debug(
|
|
396
|
+
'',
|
|
397
|
+
obj={task.unique_name: 'CHUNKED', 'chunk_size': task.input_chunk_size, 'chunks': len(chunks), 'target_count': len(task.inputs)}, # noqa: E501
|
|
398
|
+
obj_after=False,
|
|
399
|
+
sub='celery.state',
|
|
400
|
+
verbose=True
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Clone opts
|
|
404
|
+
base_opts = task_opts.copy()
|
|
405
|
+
|
|
406
|
+
# Build signatures
|
|
407
|
+
sigs = []
|
|
408
|
+
task.ids_map = {}
|
|
409
|
+
for ix, chunk in enumerate(chunks):
|
|
410
|
+
if not isinstance(chunk, list):
|
|
411
|
+
chunk = [chunk]
|
|
412
|
+
|
|
413
|
+
# Add chunk info to opts
|
|
414
|
+
opts = base_opts.copy()
|
|
415
|
+
opts.update({'chunk': ix + 1, 'chunk_count': len(chunks)})
|
|
416
|
+
debug('', obj={
|
|
417
|
+
task.unique_name: 'CHUNK',
|
|
418
|
+
'chunk': f'{ix + 1} / {len(chunks)}',
|
|
419
|
+
'target_count': len(chunk),
|
|
420
|
+
'targets': chunk
|
|
421
|
+
}, sub='celery.state') # noqa: E501
|
|
422
|
+
|
|
423
|
+
# Construct chunked signature
|
|
424
|
+
opts['has_parent'] = True
|
|
425
|
+
opts['enable_duplicate_check'] = False
|
|
426
|
+
opts['results'] = results
|
|
427
|
+
if 'targets_' in opts:
|
|
428
|
+
del opts['targets_']
|
|
429
|
+
sig = type(task).si(chunk, **opts)
|
|
430
|
+
task_id = sig.freeze().task_id
|
|
431
|
+
full_name = f'{task.name}_{ix + 1}'
|
|
432
|
+
task.add_subtask(task_id, task.name, full_name)
|
|
433
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
434
|
+
info = Info(message=f'Celery chunked task created ({ix + 1} / {len(chunks)}): {task_id}')
|
|
435
|
+
task.add_result(info)
|
|
436
|
+
sigs.append(sig)
|
|
437
|
+
|
|
438
|
+
# Mark main task as async since it's being chunked
|
|
439
|
+
task.sync = False
|
|
440
|
+
task.results = []
|
|
441
|
+
task.uuids = set()
|
|
442
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
443
|
+
console.print(Info(message=f'Task {task.unique_name} is now async, building chord with {len(sigs)} chunks'))
|
|
444
|
+
# console.print(Info(message=f'Results: {results}'))
|
|
445
|
+
|
|
446
|
+
# Build Celery workflow
|
|
447
|
+
workflow = chord(
|
|
448
|
+
tuple(sigs),
|
|
449
|
+
mark_runner_completed.s(runner=task).set(queue='results')
|
|
450
|
+
)
|
|
451
|
+
if IN_CELERY_WORKER_PROCESS:
|
|
452
|
+
console.print(Info(message=f'Task {task.unique_name} chord built with {len(sigs)} chunks, returning workflow'))
|
|
453
|
+
return workflow
|