secator 0.16.5__tar.gz → 0.17.0__tar.gz

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 (213) hide show
  1. {secator-0.16.5 → secator-0.17.0}/CHANGELOG.md +12 -0
  2. {secator-0.16.5 → secator-0.17.0}/PKG-INFO +1 -1
  3. {secator-0.16.5 → secator-0.17.0}/pyproject.toml +1 -1
  4. {secator-0.16.5 → secator-0.17.0}/secator/celery.py +75 -9
  5. {secator-0.16.5 → secator-0.17.0}/secator/celery_signals.py +2 -1
  6. {secator-0.16.5 → secator-0.17.0}/secator/cli.py +7 -1
  7. {secator-0.16.5 → secator-0.17.0}/secator/config.py +1 -0
  8. {secator-0.16.5 → secator-0.17.0}/secator/hooks/gcs.py +11 -1
  9. {secator-0.16.5 → secator-0.17.0}/secator/hooks/mongodb.py +71 -66
  10. {secator-0.16.5 → secator-0.17.0}/secator/installer.py +1 -1
  11. {secator-0.16.5 → secator-0.17.0}/secator/output_types/certificate.py +1 -1
  12. {secator-0.16.5 → secator-0.17.0}/secator/output_types/exploit.py +1 -1
  13. {secator-0.16.5 → secator-0.17.0}/secator/output_types/ip.py +1 -1
  14. {secator-0.16.5 → secator-0.17.0}/secator/output_types/progress.py +1 -1
  15. {secator-0.16.5 → secator-0.17.0}/secator/output_types/record.py +1 -1
  16. {secator-0.16.5 → secator-0.17.0}/secator/output_types/stat.py +1 -1
  17. {secator-0.16.5 → secator-0.17.0}/secator/output_types/state.py +1 -1
  18. {secator-0.16.5 → secator-0.17.0}/secator/output_types/subdomain.py +1 -1
  19. {secator-0.16.5 → secator-0.17.0}/secator/output_types/tag.py +1 -1
  20. {secator-0.16.5 → secator-0.17.0}/secator/output_types/target.py +1 -1
  21. {secator-0.16.5 → secator-0.17.0}/secator/output_types/user_account.py +1 -1
  22. {secator-0.16.5 → secator-0.17.0}/secator/output_types/vulnerability.py +1 -1
  23. {secator-0.16.5 → secator-0.17.0}/secator/runners/command.py +21 -6
  24. {secator-0.16.5 → secator-0.17.0}/secator/tasks/cariddi.py +37 -1
  25. {secator-0.16.5 → secator-0.17.0}/secator/tasks/dalfox.py +2 -2
  26. {secator-0.16.5 → secator-0.17.0}/secator/tasks/dirsearch.py +0 -1
  27. {secator-0.16.5 → secator-0.17.0}/secator/tasks/feroxbuster.py +0 -1
  28. {secator-0.16.5 → secator-0.17.0}/secator/tasks/ffuf.py +0 -1
  29. {secator-0.16.5 → secator-0.17.0}/secator/tasks/katana.py +3 -0
  30. {secator-0.16.5 → secator-0.17.0}/secator/tasks/naabu.py +1 -2
  31. {secator-0.16.5 → secator-0.17.0}/secator/tasks/nuclei.py +4 -1
  32. {secator-0.16.5 → secator-0.17.0}/secator/utils.py +9 -0
  33. {secator-0.16.5 → secator-0.17.0}/tests/integration/outputs.py +2 -2
  34. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_tasks_categories.py +10 -1
  35. {secator-0.16.5 → secator-0.17.0}/.coderabbit.yaml +0 -0
  36. {secator-0.16.5 → secator-0.17.0}/.docker/Dockerfile.alpine +0 -0
  37. {secator-0.16.5 → secator-0.17.0}/.docker/Dockerfile.arch +0 -0
  38. {secator-0.16.5 → secator-0.17.0}/.docker/Dockerfile.debian +0 -0
  39. {secator-0.16.5 → secator-0.17.0}/.docker/Dockerfile.kali +0 -0
  40. {secator-0.16.5 → secator-0.17.0}/.docker/Dockerfile.osx +0 -0
  41. {secator-0.16.5 → secator-0.17.0}/.docker/Dockerfile.ubuntu +0 -0
  42. {secator-0.16.5 → secator-0.17.0}/.docker/build_all.sh +0 -0
  43. {secator-0.16.5 → secator-0.17.0}/.dockerignore +0 -0
  44. {secator-0.16.5 → secator-0.17.0}/.flake8 +0 -0
  45. {secator-0.16.5 → secator-0.17.0}/.gitignore +0 -0
  46. {secator-0.16.5 → secator-0.17.0}/CONTRIBUTING.md +0 -0
  47. {secator-0.16.5 → secator-0.17.0}/Dockerfile +0 -0
  48. {secator-0.16.5 → secator-0.17.0}/LICENSE +0 -0
  49. {secator-0.16.5 → secator-0.17.0}/README.md +0 -0
  50. {secator-0.16.5 → secator-0.17.0}/SECURITY.md +0 -0
  51. {secator-0.16.5 → secator-0.17.0}/cloudbuild.yaml +0 -0
  52. {secator-0.16.5 → secator-0.17.0}/helm/.helmignore +0 -0
  53. {secator-0.16.5 → secator-0.17.0}/helm/Chart.yaml +0 -0
  54. {secator-0.16.5 → secator-0.17.0}/helm/templates/redis-service.yaml +0 -0
  55. {secator-0.16.5 → secator-0.17.0}/helm/templates/redis.yaml +0 -0
  56. {secator-0.16.5 → secator-0.17.0}/helm/templates/secator-manager.yaml +0 -0
  57. {secator-0.16.5 → secator-0.17.0}/helm/templates/secator-worker.yaml +0 -0
  58. {secator-0.16.5 → secator-0.17.0}/helm/values.yaml +0 -0
  59. {secator-0.16.5 → secator-0.17.0}/scripts/download_cves.sh +0 -0
  60. {secator-0.16.5 → secator-0.17.0}/scripts/generate_tools_md_table.py +0 -0
  61. {secator-0.16.5 → secator-0.17.0}/scripts/install.sh +0 -0
  62. {secator-0.16.5 → secator-0.17.0}/scripts/install_asciinema.sh +0 -0
  63. {secator-0.16.5 → secator-0.17.0}/scripts/install_go.sh +0 -0
  64. {secator-0.16.5 → secator-0.17.0}/scripts/install_ruby.sh +0 -0
  65. {secator-0.16.5 → secator-0.17.0}/scripts/msf/exploit_cve.rc +0 -0
  66. {secator-0.16.5 → secator-0.17.0}/scripts/msf/ftp_anonymous.rc +0 -0
  67. {secator-0.16.5 → secator-0.17.0}/scripts/msf/ftp_version.rc +0 -0
  68. {secator-0.16.5 → secator-0.17.0}/scripts/msf/ftp_vsftpd_234_backdoor.rc +0 -0
  69. {secator-0.16.5 → secator-0.17.0}/scripts/msf/redis.rc +0 -0
  70. {secator-0.16.5 → secator-0.17.0}/scripts/stories/STORY.md +0 -0
  71. {secator-0.16.5 → secator-0.17.0}/scripts/stories/aliases.sh +0 -0
  72. {secator-0.16.5 → secator-0.17.0}/scripts/stories/demo.sh +0 -0
  73. {secator-0.16.5 → secator-0.17.0}/scripts/stories/fmt.sh +0 -0
  74. {secator-0.16.5 → secator-0.17.0}/scripts/stories/input.sh +0 -0
  75. {secator-0.16.5 → secator-0.17.0}/scripts/stories/pipe.sh +0 -0
  76. {secator-0.16.5 → secator-0.17.0}/scripts/stories/short_demo.sh +0 -0
  77. {secator-0.16.5 → secator-0.17.0}/scripts/update_tools.sh +0 -0
  78. {secator-0.16.5 → secator-0.17.0}/secator/.gitignore +0 -0
  79. {secator-0.16.5 → secator-0.17.0}/secator/__init__.py +0 -0
  80. {secator-0.16.5 → secator-0.17.0}/secator/celery_utils.py +0 -0
  81. {secator-0.16.5 → secator-0.17.0}/secator/cli_helper.py +0 -0
  82. {secator-0.16.5 → secator-0.17.0}/secator/click.py +0 -0
  83. {secator-0.16.5 → secator-0.17.0}/secator/configs/__init__.py +0 -0
  84. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/__init__.py +0 -0
  85. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/aggressive.yaml +0 -0
  86. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/http_headless.yaml +0 -0
  87. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/http_record.yaml +0 -0
  88. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/insane.yaml +0 -0
  89. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/paranoid.yaml +0 -0
  90. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/polite.yaml +0 -0
  91. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/sneaky.yaml +0 -0
  92. {secator-0.16.5 → secator-0.17.0}/secator/configs/profiles/tor.yaml +0 -0
  93. {secator-0.16.5 → secator-0.17.0}/secator/configs/scans/__init__.py +0 -0
  94. {secator-0.16.5 → secator-0.17.0}/secator/configs/scans/domain.yaml +0 -0
  95. {secator-0.16.5 → secator-0.17.0}/secator/configs/scans/host.yaml +0 -0
  96. {secator-0.16.5 → secator-0.17.0}/secator/configs/scans/network.yaml +0 -0
  97. {secator-0.16.5 → secator-0.17.0}/secator/configs/scans/subdomain.yaml +0 -0
  98. {secator-0.16.5 → secator-0.17.0}/secator/configs/scans/url.yaml +0 -0
  99. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/__init__.py +0 -0
  100. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/cidr_recon.yaml +0 -0
  101. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/code_scan.yaml +0 -0
  102. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/host_recon.yaml +0 -0
  103. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/subdomain_recon.yaml +0 -0
  104. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/url_bypass.yaml +0 -0
  105. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/url_crawl.yaml +0 -0
  106. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/url_dirsearch.yaml +0 -0
  107. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/url_fuzz.yaml +0 -0
  108. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/url_params_fuzz.yaml +0 -0
  109. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/url_vuln.yaml +0 -0
  110. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/user_hunt.yaml +0 -0
  111. {secator-0.16.5 → secator-0.17.0}/secator/configs/workflows/wordpress.yaml +0 -0
  112. {secator-0.16.5 → secator-0.17.0}/secator/cve.py +0 -0
  113. {secator-0.16.5 → secator-0.17.0}/secator/decorators.py +0 -0
  114. {secator-0.16.5 → secator-0.17.0}/secator/definitions.py +0 -0
  115. {secator-0.16.5 → secator-0.17.0}/secator/exporters/__init__.py +0 -0
  116. {secator-0.16.5 → secator-0.17.0}/secator/exporters/_base.py +0 -0
  117. {secator-0.16.5 → secator-0.17.0}/secator/exporters/console.py +0 -0
  118. {secator-0.16.5 → secator-0.17.0}/secator/exporters/csv.py +0 -0
  119. {secator-0.16.5 → secator-0.17.0}/secator/exporters/gdrive.py +0 -0
  120. {secator-0.16.5 → secator-0.17.0}/secator/exporters/json.py +0 -0
  121. {secator-0.16.5 → secator-0.17.0}/secator/exporters/table.py +0 -0
  122. {secator-0.16.5 → secator-0.17.0}/secator/exporters/txt.py +0 -0
  123. {secator-0.16.5 → secator-0.17.0}/secator/hooks/__init__.py +0 -0
  124. {secator-0.16.5 → secator-0.17.0}/secator/loader.py +0 -0
  125. {secator-0.16.5 → secator-0.17.0}/secator/output_types/__init__.py +0 -0
  126. {secator-0.16.5 → secator-0.17.0}/secator/output_types/_base.py +0 -0
  127. {secator-0.16.5 → secator-0.17.0}/secator/output_types/error.py +0 -0
  128. {secator-0.16.5 → secator-0.17.0}/secator/output_types/info.py +0 -0
  129. {secator-0.16.5 → secator-0.17.0}/secator/output_types/port.py +0 -0
  130. {secator-0.16.5 → secator-0.17.0}/secator/output_types/url.py +0 -0
  131. {secator-0.16.5 → secator-0.17.0}/secator/output_types/warning.py +0 -0
  132. {secator-0.16.5 → secator-0.17.0}/secator/report.py +0 -0
  133. {secator-0.16.5 → secator-0.17.0}/secator/rich.py +0 -0
  134. {secator-0.16.5 → secator-0.17.0}/secator/runners/__init__.py +0 -0
  135. {secator-0.16.5 → secator-0.17.0}/secator/runners/_base.py +0 -0
  136. {secator-0.16.5 → secator-0.17.0}/secator/runners/_helpers.py +0 -0
  137. {secator-0.16.5 → secator-0.17.0}/secator/runners/celery.py +0 -0
  138. {secator-0.16.5 → secator-0.17.0}/secator/runners/scan.py +0 -0
  139. {secator-0.16.5 → secator-0.17.0}/secator/runners/task.py +0 -0
  140. {secator-0.16.5 → secator-0.17.0}/secator/runners/workflow.py +0 -0
  141. {secator-0.16.5 → secator-0.17.0}/secator/scans/__init__.py +0 -0
  142. {secator-0.16.5 → secator-0.17.0}/secator/serializers/__init__.py +0 -0
  143. {secator-0.16.5 → secator-0.17.0}/secator/serializers/dataclass.py +0 -0
  144. {secator-0.16.5 → secator-0.17.0}/secator/serializers/json.py +0 -0
  145. {secator-0.16.5 → secator-0.17.0}/secator/serializers/regex.py +0 -0
  146. {secator-0.16.5 → secator-0.17.0}/secator/tasks/__init__.py +0 -0
  147. {secator-0.16.5 → secator-0.17.0}/secator/tasks/_categories.py +0 -0
  148. {secator-0.16.5 → secator-0.17.0}/secator/tasks/arjun.py +0 -0
  149. {secator-0.16.5 → secator-0.17.0}/secator/tasks/bbot.py +0 -0
  150. {secator-0.16.5 → secator-0.17.0}/secator/tasks/bup.py +0 -0
  151. {secator-0.16.5 → secator-0.17.0}/secator/tasks/dnsx.py +0 -0
  152. {secator-0.16.5 → secator-0.17.0}/secator/tasks/fping.py +0 -0
  153. {secator-0.16.5 → secator-0.17.0}/secator/tasks/gau.py +0 -0
  154. {secator-0.16.5 → secator-0.17.0}/secator/tasks/gf.py +0 -0
  155. {secator-0.16.5 → secator-0.17.0}/secator/tasks/gitleaks.py +0 -0
  156. {secator-0.16.5 → secator-0.17.0}/secator/tasks/gospider.py +0 -0
  157. {secator-0.16.5 → secator-0.17.0}/secator/tasks/grype.py +0 -0
  158. {secator-0.16.5 → secator-0.17.0}/secator/tasks/h8mail.py +0 -0
  159. {secator-0.16.5 → secator-0.17.0}/secator/tasks/httpx.py +0 -0
  160. {secator-0.16.5 → secator-0.17.0}/secator/tasks/maigret.py +0 -0
  161. {secator-0.16.5 → secator-0.17.0}/secator/tasks/mapcidr.py +0 -0
  162. {secator-0.16.5 → secator-0.17.0}/secator/tasks/msfconsole.py +0 -0
  163. {secator-0.16.5 → secator-0.17.0}/secator/tasks/nmap.py +0 -0
  164. {secator-0.16.5 → secator-0.17.0}/secator/tasks/searchsploit.py +0 -0
  165. {secator-0.16.5 → secator-0.17.0}/secator/tasks/subfinder.py +0 -0
  166. {secator-0.16.5 → secator-0.17.0}/secator/tasks/testssl.py +0 -0
  167. {secator-0.16.5 → secator-0.17.0}/secator/tasks/trivy.py +0 -0
  168. {secator-0.16.5 → secator-0.17.0}/secator/tasks/wafw00f.py +0 -0
  169. {secator-0.16.5 → secator-0.17.0}/secator/tasks/wpprobe.py +0 -0
  170. {secator-0.16.5 → secator-0.17.0}/secator/tasks/wpscan.py +0 -0
  171. {secator-0.16.5 → secator-0.17.0}/secator/template.py +0 -0
  172. {secator-0.16.5 → secator-0.17.0}/secator/thread.py +0 -0
  173. {secator-0.16.5 → secator-0.17.0}/secator/tree.py +0 -0
  174. {secator-0.16.5 → secator-0.17.0}/secator/utils_test.py +0 -0
  175. {secator-0.16.5 → secator-0.17.0}/secator/workflows/__init__.py +0 -0
  176. {secator-0.16.5 → secator-0.17.0}/tests/__init__.py +0 -0
  177. {secator-0.16.5 → secator-0.17.0}/tests/fixtures/h8mail_breach.txt +0 -0
  178. {secator-0.16.5 → secator-0.17.0}/tests/fixtures/ls.py +0 -0
  179. {secator-0.16.5 → secator-0.17.0}/tests/fixtures/msfconsole_input.rc +0 -0
  180. {secator-0.16.5 → secator-0.17.0}/tests/fixtures/nmap_output.xml +0 -0
  181. {secator-0.16.5 → secator-0.17.0}/tests/integration/__init__.py +0 -0
  182. {secator-0.16.5 → secator-0.17.0}/tests/integration/all.yaml +0 -0
  183. {secator-0.16.5 → secator-0.17.0}/tests/integration/inputs.py +0 -0
  184. {secator-0.16.5 → secator-0.17.0}/tests/integration/setup.sh +0 -0
  185. {secator-0.16.5 → secator-0.17.0}/tests/integration/teardown.sh +0 -0
  186. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_addons.py +0 -0
  187. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_celery.py +0 -0
  188. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_scans.py +0 -0
  189. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_tasks.py +0 -0
  190. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_worker.py +0 -0
  191. {secator-0.16.5 → secator-0.17.0}/tests/integration/test_workflows.py +0 -0
  192. {secator-0.16.5 → secator-0.17.0}/tests/integration/wordlist.txt +0 -0
  193. {secator-0.16.5 → secator-0.17.0}/tests/integration/wordlist_dns.txt +0 -0
  194. {secator-0.16.5 → secator-0.17.0}/tests/integration/wordpress_toolbox/Dockerfile +0 -0
  195. {secator-0.16.5 → secator-0.17.0}/tests/integration/wordpress_toolbox/Makefile +0 -0
  196. {secator-0.16.5 → secator-0.17.0}/tests/performance/__init__.py +0 -0
  197. {secator-0.16.5 → secator-0.17.0}/tests/performance/loadtester.py +0 -0
  198. {secator-0.16.5 → secator-0.17.0}/tests/performance/test_worker.py +0 -0
  199. {secator-0.16.5 → secator-0.17.0}/tests/template/test_templates.py +0 -0
  200. {secator-0.16.5 → secator-0.17.0}/tests/unit/__init__.py +0 -0
  201. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_celery.py +0 -0
  202. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_cli.py +0 -0
  203. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_command.py +0 -0
  204. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_config.py +0 -0
  205. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_offline.py +0 -0
  206. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_runners.py +0 -0
  207. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_runners_helpers.py +0 -0
  208. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_scans.py +0 -0
  209. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_serializers.py +0 -0
  210. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_tasks.py +0 -0
  211. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_tasks_categories.py +0 -0
  212. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_template.py +0 -0
  213. {secator-0.16.5 → secator-0.17.0}/tests/unit/test_utils.py +0 -0
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.17.0](https://github.com/freelabz/secator/compare/v0.16.5...v0.17.0) (2025-09-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * enforceable memory limits ([#710](https://github.com/freelabz/secator/issues/710)) ([eaefa1f](https://github.com/freelabz/secator/commit/eaefa1f5497af4d6a6020e09d5f954ae4639f20c))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * prod optimizations ([#708](https://github.com/freelabz/secator/issues/708)) ([efb5d3c](https://github.com/freelabz/secator/commit/efb5d3ce637a5de6e23ed157fd85d2a39bef360d))
14
+
3
15
  ## [0.16.5](https://github.com/freelabz/secator/compare/v0.16.4...v0.16.5) (2025-06-25)
4
16
 
5
17
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: secator
3
- Version: 0.16.5
3
+ Version: 0.17.0
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
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
4
4
 
5
5
  [project]
6
6
  name = 'secator'
7
- version = "0.16.5"
7
+ version = "0.17.0"
8
8
  authors = [{ name = 'FreeLabz', email = 'sales@freelabz.com' }]
9
9
  readme = 'README.md'
10
10
  description = "The pentester's swiss knife."
@@ -1,3 +1,4 @@
1
+ import gc
1
2
  import json
2
3
  import logging
3
4
  import os
@@ -5,6 +6,7 @@ import os
5
6
  from time import time
6
7
 
7
8
  from celery import Celery, chord
9
+ from celery.canvas import signature
8
10
  from celery.app import trace
9
11
 
10
12
  from rich.logging import RichHandler
@@ -61,9 +63,10 @@ app.conf.update({
61
63
  'result_backend': CONFIG.celery.result_backend,
62
64
  'result_expires': CONFIG.celery.result_expires,
63
65
  'result_backend_transport_options': json.loads(CONFIG.celery.result_backend_transport_options) if CONFIG.celery.result_backend_transport_options else {}, # noqa: E501
64
- 'result_extended': True,
66
+ 'result_extended': not CONFIG.addons.mongodb.enabled,
65
67
  'result_backend_thread_safe': True,
66
68
  'result_serializer': 'pickle',
69
+ 'result_accept_content': ['application/x-python-serialize'],
67
70
 
68
71
  # Task config
69
72
  'task_acks_late': CONFIG.celery.task_acks_late,
@@ -81,6 +84,11 @@ app.conf.update({
81
84
  'task_store_eager_result': True,
82
85
  'task_send_sent_event': CONFIG.celery.task_send_sent_event,
83
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'],
84
92
 
85
93
  # Worker config
86
94
  # 'worker_direct': True, # TODO: consider enabling this to allow routing to specific workers
@@ -168,6 +176,12 @@ def run_scan(self, args=[], kwargs={}):
168
176
 
169
177
  @app.task(bind=True)
170
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
171
185
  if IN_CELERY_WORKER_PROCESS:
172
186
  quiet = not CONFIG.cli.worker_command_verbose
173
187
  opts.update({
@@ -179,15 +193,13 @@ def run_command(self, results, name, targets, opts={}):
179
193
  'quiet': quiet
180
194
  })
181
195
  routing_key = self.request.delivery_info['routing_key']
196
+ context['routing_key'] = routing_key
182
197
  debug(f'Task "{name}" running with routing key "{routing_key}"', sub='celery.state')
183
198
 
184
199
  # Flatten + dedupe + filter results
185
200
  results = forward_results(results)
186
201
 
187
- # Set Celery request id in context
188
- context = opts.get('context', {})
189
- context['celery_id'] = self.request.id
190
- context['worker_name'] = os.environ.get('WORKER_NAME', 'unknown')
202
+ # Set task opts
191
203
  opts['context'] = context
192
204
  opts['results'] = results
193
205
  opts['sync'] = True
@@ -204,10 +216,13 @@ def run_command(self, results, name, targets, opts={}):
204
216
  # Chunk task if needed
205
217
  if chunk_it:
206
218
  if IN_CELERY_WORKER_PROCESS:
207
- console.print(Info(message=f'Task {name} requires chunking, breaking into {len(targets)} tasks'))
208
- tasks = break_task(task, opts, results=results)
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'))
209
223
  update_state(self, task, force=True)
210
- return self.replace(tasks)
224
+ console.print(Info(message=f'Task {name} updated state, replacing task with Celery chord workflow'))
225
+ return replace(self, workflow)
211
226
 
212
227
  # Update state live
213
228
  for _ in task:
@@ -327,6 +342,52 @@ def is_celery_worker_alive():
327
342
  return result
328
343
 
329
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
+ console.print('Replacing task')
363
+ chord = task_instance.request.chord
364
+ sig.freeze(task_instance.request.id)
365
+ replaced_task_nesting = task_instance.request.get('replaced_task_nesting', 0) + 1
366
+ sig.set(
367
+ chord=chord,
368
+ group_id=task_instance.request.group,
369
+ group_index=task_instance.request.group_index,
370
+ root_id=task_instance.request.root_id,
371
+ replaced_task_nesting=replaced_task_nesting
372
+ )
373
+ import psutil
374
+ import os
375
+ process = psutil.Process(os.getpid())
376
+ length = len(task_instance.request.chain) if task_instance.request.chain else 0
377
+ console.print(f'Adding {length} chain tasks from request chain')
378
+ for ix, t in enumerate(reversed(task_instance.request.chain or [])):
379
+ console.print(f'Adding chain task {t.name} from request chain ({ix + 1}/{length})')
380
+ chain_task = signature(t, app=task_instance.app)
381
+ chain_task.set(replaced_task_nesting=replaced_task_nesting)
382
+ sig |= chain_task
383
+ del chain_task
384
+ del t
385
+ memory_bytes = process.memory_info().rss
386
+ console.print(f'Memory usage: {memory_bytes / 1024 / 1024:.2f} MB (chain task {ix + 1}/{length})')
387
+ gc.collect()
388
+ return task_instance.on_replace(sig)
389
+
390
+
330
391
  def break_task(task, task_opts, results=[]):
331
392
  """Break a task into multiple of the same type."""
332
393
  chunks = task.inputs
@@ -370,16 +431,21 @@ def break_task(task, task_opts, results=[]):
370
431
  task_id = sig.freeze().task_id
371
432
  full_name = f'{task.name}_{ix + 1}'
372
433
  task.add_subtask(task_id, task.name, full_name)
373
- info = Info(message=f'Celery chunked task created: {task_id}')
434
+ info = Info(message=f'Celery chunked task created ({ix + 1} / {len(chunks)}): {task_id}')
374
435
  task.add_result(info)
375
436
  sigs.append(sig)
376
437
 
377
438
  # Mark main task as async since it's being chunked
378
439
  task.sync = False
440
+ task.results = []
441
+ task.uuids = set()
442
+ console.print(Info(message=f'Task {task.unique_name} is now async, building chord with{len(sigs)} chunks'))
443
+ console.print(Info(message=f'Results: {results}'))
379
444
 
380
445
  # Build Celery workflow
381
446
  workflow = chord(
382
447
  tuple(sigs),
383
448
  mark_runner_completed.s(runner=task).set(queue='results')
384
449
  )
450
+ console.print(Info(message=f'Task {task.unique_name} chord built with {len(sigs)} chunks, returning workflow'))
385
451
  return workflow
@@ -92,8 +92,9 @@ def task_postrun_handler(**kwargs):
92
92
 
93
93
  # Get sender name from kwargs
94
94
  sender_name = kwargs['sender'].name
95
+ # console.print(Info(message=f'Task postrun handler --> Sender name: {sender_name}'))
95
96
 
96
- if CONFIG.celery.worker_kill_after_task and sender_name.startswith('secator.'):
97
+ if CONFIG.celery.worker_kill_after_task and (sender_name.startswith('secator.') or sender_name.startswith('api.')):
97
98
  worker_name = os.environ.get('WORKER_NAME', 'unknown')
98
99
  console.print(Info(message=f'Shutdown worker {worker_name} since config celery.worker_kill_after_task is set.'))
99
100
  kill_worker(parent=True)
@@ -134,7 +134,10 @@ for config in SCANS:
134
134
  @click.option('--stop', is_flag=True, help='Stop a worker in dev mode (celery multi).')
135
135
  @click.option('--show', is_flag=True, help='Show command (celery multi).')
136
136
  @click.option('--use-command-runner', is_flag=True, default=False, help='Use command runner to run the command.')
137
- def worker(hostname, concurrency, reload, queue, pool, quiet, loglevel, check, dev, stop, show, use_command_runner):
137
+ @click.option('--without-gossip', is_flag=True)
138
+ @click.option('--without-mingle', is_flag=True)
139
+ @click.option('--without-heartbeat', is_flag=True)
140
+ def worker(hostname, concurrency, reload, queue, pool, quiet, loglevel, check, dev, stop, show, use_command_runner, without_gossip, without_mingle, without_heartbeat): # noqa: E501
138
141
  """Run a worker."""
139
142
 
140
143
  # Check Celery addon is installed
@@ -182,6 +185,9 @@ def worker(hostname, concurrency, reload, queue, pool, quiet, loglevel, check, d
182
185
  cmd += f' -P {pool}' if pool else ''
183
186
  cmd += f' -c {concurrency}' if concurrency else ''
184
187
  cmd += f' -l {loglevel}' if loglevel else ''
188
+ cmd += ' --without-mingle' if without_mingle else ''
189
+ cmd += ' --without-gossip' if without_gossip else ''
190
+ cmd += ' --without-heartbeat' if without_heartbeat else ''
185
191
 
186
192
  if reload:
187
193
  patterns = "celery.py;tasks/*.py;runners/*.py;serializers/*.py;output_types/*.py;hooks/*.py;exporters/*.py"
@@ -100,6 +100,7 @@ class Security(StrictModel):
100
100
  allow_local_file_access: bool = True
101
101
  auto_install_commands: bool = True
102
102
  force_source_install: bool = False
103
+ memory_limit_mb: int = -1
103
104
 
104
105
 
105
106
  class HTTP(StrictModel):
@@ -14,6 +14,16 @@ ITEMS_TO_SEND = {
14
14
  'url': ['screenshot_path', 'stored_response_path']
15
15
  }
16
16
 
17
+ _gcs_client = None
18
+
19
+
20
+ def get_gcs_client():
21
+ """Get or create GCS client"""
22
+ global _gcs_client
23
+ if _gcs_client is None:
24
+ _gcs_client = storage.Client()
25
+ return _gcs_client
26
+
17
27
 
18
28
  def process_item(self, item):
19
29
  if item._type not in ITEMS_TO_SEND.keys():
@@ -39,7 +49,7 @@ def process_item(self, item):
39
49
  def upload_blob(bucket_name, source_file_name, destination_blob_name):
40
50
  """Uploads a file to the bucket."""
41
51
  start_time = time()
42
- storage_client = storage.Client()
52
+ storage_client = get_gcs_client()
43
53
  bucket = storage_client.bucket(bucket_name)
44
54
  blob = bucket.blob(destination_blob_name)
45
55
  with open(source_file_name, 'rb') as f:
@@ -166,85 +166,90 @@ def tag_duplicates(ws_id: str = None, full_scan: bool = False):
166
166
  full_scan (bool): If True, scan all findings, otherwise only untagged findings.
167
167
  """
168
168
  debug(f'running duplicate check on workspace {ws_id}', sub='hooks.mongodb')
169
+ init_time = time.time()
169
170
  client = get_mongodb_client()
170
171
  db = client.main
171
- workspace_query = list(
172
- db.findings.find({'_context.workspace_id': str(ws_id), '_tagged': True}).sort('_timestamp', -1))
173
- untagged_query = {'_context.workspace_id': str(ws_id)}
174
- if not full_scan:
175
- untagged_query['_tagged'] = {'$ne': True}
176
- untagged_query = list(
177
- db.findings.find(untagged_query).sort('_timestamp', -1))
178
- if not untagged_query:
179
- debug('no untagged findings. Skipping.', id=ws_id, sub='hooks.mongodb')
180
- return
181
- debug(f'found {len(untagged_query)} untagged findings', id=ws_id, sub='hooks.mongodb')
172
+ start_time = time.time()
173
+ workspace_query = {'_context.workspace_id': str(ws_id), '_context.workspace_duplicate': False, '_tagged': True}
174
+ untagged_query = {'_context.workspace_id': str(ws_id), '_tagged': {'$ne': True}}
175
+ if full_scan:
176
+ del untagged_query['_tagged']
177
+ workspace_findings = load_findings(list(db.findings.find(workspace_query).sort('_timestamp', -1)))
178
+ untagged_findings = load_findings(list(db.findings.find(untagged_query).sort('_timestamp', -1)))
179
+ debug(
180
+ f'Workspace non-duplicates findings: {len(workspace_findings)}, '
181
+ f'Untagged findings: {len(untagged_findings)}. '
182
+ f'Query time: {time.time() - start_time}s',
183
+ sub='hooks.mongodb'
184
+ )
185
+ start_time = time.time()
186
+ seen = []
187
+ db_updates = {}
182
188
 
183
- untagged_findings = load_findings(untagged_query)
184
- workspace_findings = load_findings(workspace_query)
185
- non_duplicates = []
186
- duplicates = []
187
189
  for item in untagged_findings:
188
- # If already seen in duplicates
189
- seen = [f for f in duplicates if f._uuid == item._uuid]
190
- if seen:
190
+ if item._uuid in seen:
191
191
  continue
192
192
 
193
- # Check for duplicates
194
- tmp_duplicates = []
193
+ debug(
194
+ f'Processing: {repr(item)} ({item._timestamp}) [{item._uuid}]',
195
+ sub='hooks.mongodb',
196
+ verbose=True
197
+ )
198
+
199
+ duplicate_ids = [
200
+ _._uuid
201
+ for _ in untagged_findings
202
+ if _ == item and _._uuid != item._uuid
203
+ ]
204
+ seen.extend(duplicate_ids)
195
205
 
196
- # Check if already present in list of workspace_findings findings, list of duplicates, or untagged_findings
197
- workspace_dupes = [f for f in workspace_findings if f == item and f._uuid != item._uuid]
198
- untagged_dupes = [f for f in untagged_findings if f == item and f._uuid != item._uuid]
199
- seen_dupes = [f for f in duplicates if f == item and f._uuid != item._uuid]
200
- tmp_duplicates.extend(workspace_dupes)
201
- tmp_duplicates.extend(untagged_dupes)
202
- tmp_duplicates.extend(seen_dupes)
203
206
  debug(
204
- f'for item {item._uuid}',
205
- obj={
206
- 'workspace dupes': len(workspace_dupes),
207
- 'untagged dupes': len(untagged_dupes),
208
- 'seen dupes': len(seen_dupes)
209
- },
210
- id=ws_id,
207
+ f'Found {len(duplicate_ids)} duplicates for item',
211
208
  sub='hooks.mongodb',
212
- verbose=True)
213
- tmp_duplicates_ids = list(dict.fromkeys([i._uuid for i in tmp_duplicates]))
214
- debug(f'duplicate ids: {tmp_duplicates_ids}', id=ws_id, sub='hooks.mongodb', verbose=True)
215
-
216
- # Update latest object as non-duplicate
217
- if tmp_duplicates:
218
- duplicates.extend([f for f in tmp_duplicates])
219
- db.findings.update_one({'_id': ObjectId(item._uuid)}, {'$set': {'_related': tmp_duplicates_ids}})
220
- debug(f'adding {item._uuid} as non-duplicate', id=ws_id, sub='hooks.mongodb', verbose=True)
221
- non_duplicates.append(item)
222
- else:
223
- debug(f'adding {item._uuid} as non-duplicate', id=ws_id, sub='hooks.mongodb', verbose=True)
224
- non_duplicates.append(item)
209
+ verbose=True
210
+ )
225
211
 
226
- # debug(f'found {len(duplicates)} total duplicates')
212
+ duplicate_ws = [
213
+ _ for _ in workspace_findings
214
+ if _ == item and _._uuid != item._uuid
215
+ ]
216
+ debug(f' --> Found {len(duplicate_ws)} workspace duplicates for item', sub='hooks.mongodb', verbose=True)
217
+
218
+ related_ids = []
219
+ if duplicate_ws:
220
+ duplicate_ws_ids = [_._uuid for _ in duplicate_ws]
221
+ duplicate_ids.extend(duplicate_ws_ids)
222
+ for related in duplicate_ws:
223
+ related_ids.extend(related._related)
224
+
225
+ debug(f' --> Found {len(duplicate_ids)} total duplicates for item', sub='hooks.mongodb', verbose=True)
226
+
227
+ db_updates[item._uuid] = {
228
+ '_related': duplicate_ids + related_ids,
229
+ '_context.workspace_duplicate': False,
230
+ '_tagged': True
231
+ }
232
+ for uuid in duplicate_ids:
233
+ db_updates[uuid] = {
234
+ '_context.workspace_duplicate': True,
235
+ '_tagged': True
236
+ }
237
+ debug(f'Finished processing untagged findings in {time.time() - start_time}s', sub='hooks.mongodb')
238
+ start_time = time.time()
227
239
 
228
- # Update objects with _tagged and _duplicate fields
229
- duplicates_ids = list(dict.fromkeys([n._uuid for n in duplicates]))
230
- non_duplicates_ids = list(dict.fromkeys([n._uuid for n in non_duplicates]))
240
+ debug(f'Executing {len(db_updates)} database updates', sub='hooks.mongodb')
231
241
 
232
- search = {'_id': {'$in': [ObjectId(d) for d in duplicates_ids]}}
233
- update = {'$set': {'_context.workspace_duplicate': True, '_tagged': True}}
234
- db.findings.update_many(search, update)
242
+ from pymongo import UpdateOne
243
+ if not db_updates:
244
+ debug('no db updates to execute', sub='hooks.mongodb')
245
+ return
235
246
 
236
- search = {'_id': {'$in': [ObjectId(d) for d in non_duplicates_ids]}}
237
- update = {'$set': {'_context.workspace_duplicate': False, '_tagged': True}}
238
- db.findings.update_many(search, update)
239
- debug(
240
- 'completed duplicates check for workspace.',
241
- id=ws_id,
242
- obj={
243
- 'processed': len(untagged_findings),
244
- 'duplicates': len(duplicates_ids),
245
- 'non-duplicates': len(non_duplicates_ids)
246
- },
247
- sub='hooks.mongodb')
247
+ result = db.findings.bulk_write(
248
+ [UpdateOne({'_id': ObjectId(uuid)}, {'$set': update}) for uuid, update in db_updates.items()]
249
+ )
250
+ debug(result, sub='hooks.mongodb')
251
+ debug(f'Finished running db update in {time.time() - start_time}s', sub='hooks.mongodb')
252
+ debug(f'Finished running tag duplicates in {time.time() - init_time}s', sub='hooks.mongodb')
248
253
 
249
254
 
250
255
  HOOKS = {
@@ -395,7 +395,7 @@ class GithubInstaller:
395
395
  for root, _, files in os.walk(directory):
396
396
  for file in files:
397
397
  # Match the file name exactly with the repository name
398
- if file == binary_name:
398
+ if file.startswith(binary_name):
399
399
  return os.path.join(root, file)
400
400
  return None
401
401
 
@@ -26,7 +26,7 @@ class Certificate(OutputType):
26
26
  serial_number: str = field(default='', compare=False)
27
27
  ciphers: list[str] = field(default_factory=list, compare=False)
28
28
  # parent_certificate: 'Certificate' = None # noqa: F821
29
- _source: str = field(default='', repr=True)
29
+ _source: str = field(default='', repr=True, compare=False)
30
30
  _type: str = field(default='certificate', repr=True)
31
31
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
32
32
  _uuid: str = field(default='', repr=True, compare=False)
@@ -18,7 +18,7 @@ class Exploit(OutputType):
18
18
  cves: list = field(default_factory=list, compare=False)
19
19
  tags: list = field(default_factory=list, compare=False)
20
20
  extra_data: dict = field(default_factory=dict, compare=False)
21
- _source: str = field(default='', repr=True)
21
+ _source: str = field(default='', repr=True, compare=False)
22
22
  _type: str = field(default='exploit', repr=True)
23
23
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
24
24
  _uuid: str = field(default='', repr=True, compare=False)
@@ -18,7 +18,7 @@ class Ip(OutputType):
18
18
  host: str = field(default='', repr=True, compare=False)
19
19
  alive: bool = False
20
20
  protocol: str = field(default=IpProtocol.IPv4)
21
- _source: str = field(default='', repr=True)
21
+ _source: str = field(default='', repr=True, compare=False)
22
22
  _type: str = field(default='ip', repr=True)
23
23
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
24
24
  _uuid: str = field(default='', repr=True, compare=False)
@@ -9,7 +9,7 @@ from secator.utils import rich_to_ansi, format_object
9
9
  class Progress(OutputType):
10
10
  percent: int = 0
11
11
  extra_data: dict = field(default_factory=dict)
12
- _source: str = field(default='', repr=True)
12
+ _source: str = field(default='', repr=True, compare=False)
13
13
  _type: str = field(default='progress', repr=True)
14
14
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
15
15
  _uuid: str = field(default='', repr=True, compare=False)
@@ -12,7 +12,7 @@ class Record(OutputType):
12
12
  type: str
13
13
  host: str = ''
14
14
  extra_data: dict = field(default_factory=dict, compare=False)
15
- _source: str = field(default='', repr=True)
15
+ _source: str = field(default='', repr=True, compare=False)
16
16
  _type: str = field(default='record', repr=True)
17
17
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
18
18
  _uuid: str = field(default='', repr=True, compare=False)
@@ -13,7 +13,7 @@ class Stat(OutputType):
13
13
  memory: int
14
14
  net_conns: int = field(default=None, repr=True)
15
15
  extra_data: dict = field(default_factory=dict)
16
- _source: str = field(default='', repr=True)
16
+ _source: str = field(default='', repr=True, compare=False)
17
17
  _type: str = field(default='stat', repr=True)
18
18
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
19
19
  _uuid: str = field(default='', repr=True, compare=False)
@@ -12,7 +12,7 @@ class State(OutputType):
12
12
  task_id: str
13
13
  state: str
14
14
  _type: str = field(default='state', repr=True)
15
- _source: str = field(default='', repr=True)
15
+ _source: str = field(default='', repr=True, compare=False)
16
16
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
17
17
  _uuid: str = field(default='', repr=True, compare=False)
18
18
  _context: dict = field(default_factory=dict, repr=True, compare=False)
@@ -13,7 +13,7 @@ class Subdomain(OutputType):
13
13
  domain: str
14
14
  sources: List[str] = field(default_factory=list, compare=False)
15
15
  extra_data: dict = field(default_factory=dict, compare=False)
16
- _source: str = field(default='', repr=True)
16
+ _source: str = field(default='', repr=True, compare=False)
17
17
  _type: str = field(default='subdomain', repr=True)
18
18
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
19
19
  _uuid: str = field(default='', repr=True, compare=False)
@@ -11,7 +11,7 @@ class Tag(OutputType):
11
11
  match: str
12
12
  extra_data: dict = field(default_factory=dict, repr=True, compare=False)
13
13
  stored_response_path: str = field(default='', compare=False)
14
- _source: str = field(default='', repr=True)
14
+ _source: str = field(default='', repr=True, compare=False)
15
15
  _type: str = field(default='tag', repr=True)
16
16
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
17
17
  _uuid: str = field(default='', repr=True, compare=False)
@@ -9,7 +9,7 @@ from secator.utils import autodetect_type, rich_to_ansi, rich_escape as _s
9
9
  class Target(OutputType):
10
10
  name: str
11
11
  type: str = ''
12
- _source: str = field(default='', repr=True)
12
+ _source: str = field(default='', repr=True, compare=False)
13
13
  _type: str = field(default='target', repr=True)
14
14
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
15
15
  _uuid: str = field(default='', repr=True, compare=False)
@@ -13,7 +13,7 @@ class UserAccount(OutputType):
13
13
  email: str = ''
14
14
  site_name: str = ''
15
15
  extra_data: dict = field(default_factory=dict, compare=False)
16
- _source: str = field(default='', repr=True)
16
+ _source: str = field(default='', repr=True, compare=False)
17
17
  _type: str = field(default='user_account', repr=True)
18
18
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
19
19
  _uuid: str = field(default='', repr=True, compare=False)
@@ -25,7 +25,7 @@ class Vulnerability(OutputType):
25
25
  reference: str = field(default='', compare=False)
26
26
  confidence_nb: int = 0
27
27
  severity_nb: int = 0
28
- _source: str = field(default='', repr=True)
28
+ _source: str = field(default='', repr=True, compare=False)
29
29
  _type: str = field(default='vulnerability', repr=True)
30
30
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
31
31
  _uuid: str = field(default='', repr=True, compare=False)
@@ -18,7 +18,7 @@ from secator.config import CONFIG
18
18
  from secator.output_types import Info, Warning, Error, Stat
19
19
  from secator.runners import Runner
20
20
  from secator.template import TemplateLoader
21
- from secator.utils import debug, rich_escape as _s
21
+ from secator.utils import debug, rich_escape as _s, signal_to_name
22
22
 
23
23
 
24
24
  logger = logging.getLogger(__name__)
@@ -440,6 +440,7 @@ class Command(Runner):
440
440
  # Output and results
441
441
  self.return_code = 0
442
442
  self.killed = False
443
+ self.memory_limit_mb = CONFIG.security.memory_limit_mb
443
444
 
444
445
  # Run the command using subprocess
445
446
  env = os.environ
@@ -449,6 +450,7 @@ class Command(Runner):
449
450
  stdout=subprocess.PIPE,
450
451
  stderr=subprocess.STDOUT,
451
452
  universal_newlines=True,
453
+ preexec_fn=os.setsid,
452
454
  shell=self.shell,
453
455
  env=env,
454
456
  cwd=self.cwd)
@@ -473,6 +475,11 @@ class Command(Runner):
473
475
  except FileNotFoundError as e:
474
476
  yield from self.handle_file_not_found(e)
475
477
 
478
+ except MemoryError as e:
479
+ self.debug(f'{self.unique_name}: {type(e).__name__}.', sub='end')
480
+ self.stop_process(exit_ok=True, sig=signal.SIGTERM)
481
+ yield Warning(message=f'Memory limit {self.memory_limit_mb}MB reached for {self.unique_name}')
482
+
476
483
  except BaseException as e:
477
484
  self.debug(f'{self.unique_name}: {type(e).__name__}.', sub='end')
478
485
  self.stop_process()
@@ -527,7 +534,7 @@ class Command(Runner):
527
534
  if self.last_updated_stat and (time() - self.last_updated_stat) < CONFIG.runners.stat_update_frequency:
528
535
  return
529
536
 
530
- yield from self.stats()
537
+ yield from self.stats(self.memory_limit_mb)
531
538
  self.last_updated_stat = time()
532
539
 
533
540
  def print_description(self):
@@ -565,26 +572,31 @@ class Command(Runner):
565
572
  error = Error.from_exception(exc)
566
573
  yield error
567
574
 
568
- def stop_process(self, exit_ok=False):
575
+ def stop_process(self, exit_ok=False, sig=signal.SIGINT):
569
576
  """Sends SIGINT to running process, if any."""
570
577
  if not self.process:
571
578
  return
572
- self.debug(f'Sending SIGINT to process {self.process.pid}.', sub='error')
573
- self.process.send_signal(signal.SIGINT)
579
+ self.debug(f'Sending signal {signal_to_name(sig)} to process {self.process.pid}.', sub='error')
580
+ if self.process and self.process.pid:
581
+ os.killpg(os.getpgid(self.process.pid), sig)
574
582
  if exit_ok:
575
583
  self.exit_ok = True
576
584
 
577
- def stats(self):
585
+ def stats(self, memory_limit_mb=None):
578
586
  """Gather stats about the current running process, if any."""
579
587
  if not self.process or not self.process.pid:
580
588
  return
581
589
  proc = psutil.Process(self.process.pid)
582
590
  stats = Command.get_process_info(proc, children=True)
591
+ total_mem = 0
583
592
  for info in stats:
584
593
  name = info['name']
585
594
  pid = info['pid']
586
595
  cpu_percent = info['cpu_percent']
587
596
  mem_percent = info['memory_percent']
597
+ mem_rss = round(info['memory_info']['rss'] / 1024 / 1024, 2)
598
+ total_mem += mem_rss
599
+ self.debug(f'{name} {pid} {mem_rss}MB', sub='stats')
588
600
  net_conns = info.get('net_connections') or []
589
601
  extra_data = {k: v for k, v in info.items() if k not in ['cpu_percent', 'memory_percent', 'net_connections']}
590
602
  yield Stat(
@@ -595,6 +607,9 @@ class Command(Runner):
595
607
  net_conns=len(net_conns),
596
608
  extra_data=extra_data
597
609
  )
610
+ self.debug(f'Total mem: {total_mem}MB, memory limit: {memory_limit_mb}', sub='stats')
611
+ if memory_limit_mb and memory_limit_mb != -1 and total_mem > memory_limit_mb:
612
+ raise MemoryError(f'Memory limit {memory_limit_mb}MB reached for {self.unique_name}')
598
613
 
599
614
  @staticmethod
600
615
  def get_process_info(process, children=False):