secator 0.4.0__tar.gz → 0.5.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 (168) hide show
  1. {secator-0.4.0 → secator-0.5.0}/CHANGELOG.md +23 -0
  2. {secator-0.4.0 → secator-0.5.0}/PKG-INFO +1 -1
  3. {secator-0.4.0 → secator-0.5.0}/pyproject.toml +1 -1
  4. {secator-0.4.0 → secator-0.5.0}/secator/cli.py +83 -27
  5. {secator-0.4.0 → secator-0.5.0}/secator/config.py +6 -3
  6. {secator-0.4.0 → secator-0.5.0}/secator/definitions.py +22 -50
  7. {secator-0.4.0 → secator-0.5.0}/secator/output_types/exploit.py +3 -0
  8. {secator-0.4.0 → secator-0.5.0}/secator/output_types/vulnerability.py +0 -6
  9. {secator-0.4.0 → secator-0.5.0}/secator/report.py +1 -4
  10. {secator-0.4.0 → secator-0.5.0}/secator/runners/_base.py +14 -10
  11. secator-0.5.0/secator/tasks/__init__.py +8 -0
  12. {secator-0.4.0 → secator-0.5.0}/secator/tasks/nmap.py +6 -1
  13. {secator-0.4.0 → secator-0.5.0}/secator/tasks/searchsploit.py +42 -4
  14. {secator-0.4.0 → secator-0.5.0}/secator/utils.py +29 -13
  15. {secator-0.4.0 → secator-0.5.0}/secator/utils_test.py +13 -0
  16. secator-0.5.0/tests/fixtures/ls.py +36 -0
  17. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_offline.py +3 -8
  18. secator-0.5.0/tests/unit/test_template.py +53 -0
  19. secator-0.4.0/secator/tasks/__init__.py +0 -10
  20. {secator-0.4.0 → secator-0.5.0}/.flake8 +0 -0
  21. {secator-0.4.0 → secator-0.5.0}/.gitignore +0 -0
  22. {secator-0.4.0 → secator-0.5.0}/CONTRIBUTING.md +0 -0
  23. {secator-0.4.0 → secator-0.5.0}/Dockerfile +0 -0
  24. {secator-0.4.0 → secator-0.5.0}/LICENSE +0 -0
  25. {secator-0.4.0 → secator-0.5.0}/README.md +0 -0
  26. {secator-0.4.0 → secator-0.5.0}/SECURITY.md +0 -0
  27. {secator-0.4.0 → secator-0.5.0}/cloudbuild.yaml +0 -0
  28. {secator-0.4.0 → secator-0.5.0}/images/aliases.cast +0 -0
  29. {secator-0.4.0 → secator-0.5.0}/images/aliases.gif +0 -0
  30. {secator-0.4.0 → secator-0.5.0}/images/demo.gif +0 -0
  31. {secator-0.4.0 → secator-0.5.0}/images/demo.tap +0 -0
  32. {secator-0.4.0 → secator-0.5.0}/images/fmt.cast +0 -0
  33. {secator-0.4.0 → secator-0.5.0}/images/fmt.gif +0 -0
  34. {secator-0.4.0 → secator-0.5.0}/images/help.png +0 -0
  35. {secator-0.4.0 → secator-0.5.0}/images/input.cast +0 -0
  36. {secator-0.4.0 → secator-0.5.0}/images/input.gif +0 -0
  37. {secator-0.4.0 → secator-0.5.0}/images/pipe.cast +0 -0
  38. {secator-0.4.0 → secator-0.5.0}/images/pipe.gif +0 -0
  39. {secator-0.4.0 → secator-0.5.0}/images/short_demo.cast +0 -0
  40. {secator-0.4.0 → secator-0.5.0}/images/short_demo.gif +0 -0
  41. {secator-0.4.0 → secator-0.5.0}/scripts/download_cves.sh +0 -0
  42. {secator-0.4.0 → secator-0.5.0}/scripts/install.sh +0 -0
  43. {secator-0.4.0 → secator-0.5.0}/scripts/install_asciinema.sh +0 -0
  44. {secator-0.4.0 → secator-0.5.0}/scripts/install_go.sh +0 -0
  45. {secator-0.4.0 → secator-0.5.0}/scripts/install_ruby.sh +0 -0
  46. {secator-0.4.0 → secator-0.5.0}/scripts/msf/exploit_cve.rc +0 -0
  47. {secator-0.4.0 → secator-0.5.0}/scripts/msf/ftp_anonymous.rc +0 -0
  48. {secator-0.4.0 → secator-0.5.0}/scripts/msf/ftp_version.rc +0 -0
  49. {secator-0.4.0 → secator-0.5.0}/scripts/msf/ftp_vsftpd_234_backdoor.rc +0 -0
  50. {secator-0.4.0 → secator-0.5.0}/scripts/msf/redis.rc +0 -0
  51. {secator-0.4.0 → secator-0.5.0}/scripts/msfinstall.sh +0 -0
  52. {secator-0.4.0 → secator-0.5.0}/scripts/stories/STORY.md +0 -0
  53. {secator-0.4.0 → secator-0.5.0}/scripts/stories/aliases.sh +0 -0
  54. {secator-0.4.0 → secator-0.5.0}/scripts/stories/demo.sh +0 -0
  55. {secator-0.4.0 → secator-0.5.0}/scripts/stories/fmt.sh +0 -0
  56. {secator-0.4.0 → secator-0.5.0}/scripts/stories/input.sh +0 -0
  57. {secator-0.4.0 → secator-0.5.0}/scripts/stories/pipe.sh +0 -0
  58. {secator-0.4.0 → secator-0.5.0}/scripts/stories/short_demo.sh +0 -0
  59. {secator-0.4.0 → secator-0.5.0}/secator/.gitignore +0 -0
  60. {secator-0.4.0 → secator-0.5.0}/secator/__init__.py +0 -0
  61. {secator-0.4.0 → secator-0.5.0}/secator/celery.py +0 -0
  62. {secator-0.4.0 → secator-0.5.0}/secator/configs/__init__.py +0 -0
  63. {secator-0.4.0 → secator-0.5.0}/secator/configs/profiles/__init__.py +0 -0
  64. {secator-0.4.0 → secator-0.5.0}/secator/configs/profiles/aggressive.yaml +0 -0
  65. {secator-0.4.0 → secator-0.5.0}/secator/configs/profiles/default.yaml +0 -0
  66. {secator-0.4.0 → secator-0.5.0}/secator/configs/profiles/stealth.yaml +0 -0
  67. {secator-0.4.0 → secator-0.5.0}/secator/configs/scans/__init__.py +0 -0
  68. {secator-0.4.0 → secator-0.5.0}/secator/configs/scans/domain.yaml +0 -0
  69. {secator-0.4.0 → secator-0.5.0}/secator/configs/scans/host.yaml +0 -0
  70. {secator-0.4.0 → secator-0.5.0}/secator/configs/scans/network.yaml +0 -0
  71. {secator-0.4.0 → secator-0.5.0}/secator/configs/scans/subdomain.yaml +0 -0
  72. {secator-0.4.0 → secator-0.5.0}/secator/configs/scans/url.yaml +0 -0
  73. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/__init__.py +0 -0
  74. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/cidr_recon.yaml +0 -0
  75. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/code_scan.yaml +0 -0
  76. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/host_recon.yaml +0 -0
  77. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/port_scan.yaml +0 -0
  78. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/subdomain_recon.yaml +0 -0
  79. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/url_crawl.yaml +0 -0
  80. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/url_dirsearch.yaml +0 -0
  81. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/url_fuzz.yaml +0 -0
  82. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/url_nuclei.yaml +0 -0
  83. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/url_vuln.yaml +0 -0
  84. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/user_hunt.yaml +0 -0
  85. {secator-0.4.0 → secator-0.5.0}/secator/configs/workflows/wordpress.yaml +0 -0
  86. {secator-0.4.0 → secator-0.5.0}/secator/decorators.py +0 -0
  87. {secator-0.4.0 → secator-0.5.0}/secator/exporters/__init__.py +0 -0
  88. {secator-0.4.0 → secator-0.5.0}/secator/exporters/_base.py +0 -0
  89. {secator-0.4.0 → secator-0.5.0}/secator/exporters/csv.py +0 -0
  90. {secator-0.4.0 → secator-0.5.0}/secator/exporters/gdrive.py +0 -0
  91. {secator-0.4.0 → secator-0.5.0}/secator/exporters/json.py +0 -0
  92. {secator-0.4.0 → secator-0.5.0}/secator/exporters/table.py +0 -0
  93. {secator-0.4.0 → secator-0.5.0}/secator/exporters/txt.py +0 -0
  94. {secator-0.4.0 → secator-0.5.0}/secator/hooks/__init__.py +0 -0
  95. {secator-0.4.0 → secator-0.5.0}/secator/hooks/mongodb.py +0 -0
  96. {secator-0.4.0 → secator-0.5.0}/secator/installer.py +0 -0
  97. {secator-0.4.0 → secator-0.5.0}/secator/output_types/__init__.py +0 -0
  98. {secator-0.4.0 → secator-0.5.0}/secator/output_types/_base.py +0 -0
  99. {secator-0.4.0 → secator-0.5.0}/secator/output_types/ip.py +0 -0
  100. {secator-0.4.0 → secator-0.5.0}/secator/output_types/port.py +0 -0
  101. {secator-0.4.0 → secator-0.5.0}/secator/output_types/progress.py +0 -0
  102. {secator-0.4.0 → secator-0.5.0}/secator/output_types/record.py +0 -0
  103. {secator-0.4.0 → secator-0.5.0}/secator/output_types/subdomain.py +0 -0
  104. {secator-0.4.0 → secator-0.5.0}/secator/output_types/tag.py +0 -0
  105. {secator-0.4.0 → secator-0.5.0}/secator/output_types/target.py +0 -0
  106. {secator-0.4.0 → secator-0.5.0}/secator/output_types/url.py +0 -0
  107. {secator-0.4.0 → secator-0.5.0}/secator/output_types/user_account.py +0 -0
  108. {secator-0.4.0 → secator-0.5.0}/secator/rich.py +0 -0
  109. {secator-0.4.0 → secator-0.5.0}/secator/runners/__init__.py +0 -0
  110. {secator-0.4.0 → secator-0.5.0}/secator/runners/_helpers.py +0 -0
  111. {secator-0.4.0 → secator-0.5.0}/secator/runners/command.py +0 -0
  112. {secator-0.4.0 → secator-0.5.0}/secator/runners/scan.py +0 -0
  113. {secator-0.4.0 → secator-0.5.0}/secator/runners/task.py +0 -0
  114. {secator-0.4.0 → secator-0.5.0}/secator/runners/workflow.py +0 -0
  115. {secator-0.4.0 → secator-0.5.0}/secator/serializers/__init__.py +0 -0
  116. {secator-0.4.0 → secator-0.5.0}/secator/serializers/dataclass.py +0 -0
  117. {secator-0.4.0 → secator-0.5.0}/secator/serializers/json.py +0 -0
  118. {secator-0.4.0 → secator-0.5.0}/secator/serializers/regex.py +0 -0
  119. {secator-0.4.0 → secator-0.5.0}/secator/tasks/_categories.py +0 -0
  120. {secator-0.4.0 → secator-0.5.0}/secator/tasks/cariddi.py +0 -0
  121. {secator-0.4.0 → secator-0.5.0}/secator/tasks/dalfox.py +0 -0
  122. {secator-0.4.0 → secator-0.5.0}/secator/tasks/dirsearch.py +0 -0
  123. {secator-0.4.0 → secator-0.5.0}/secator/tasks/dnsx.py +0 -0
  124. {secator-0.4.0 → secator-0.5.0}/secator/tasks/dnsxbrute.py +0 -0
  125. {secator-0.4.0 → secator-0.5.0}/secator/tasks/feroxbuster.py +0 -0
  126. {secator-0.4.0 → secator-0.5.0}/secator/tasks/ffuf.py +0 -0
  127. {secator-0.4.0 → secator-0.5.0}/secator/tasks/fping.py +0 -0
  128. {secator-0.4.0 → secator-0.5.0}/secator/tasks/gau.py +0 -0
  129. {secator-0.4.0 → secator-0.5.0}/secator/tasks/gf.py +0 -0
  130. {secator-0.4.0 → secator-0.5.0}/secator/tasks/gospider.py +0 -0
  131. {secator-0.4.0 → secator-0.5.0}/secator/tasks/grype.py +0 -0
  132. {secator-0.4.0 → secator-0.5.0}/secator/tasks/h8mail.py +0 -0
  133. {secator-0.4.0 → secator-0.5.0}/secator/tasks/httpx.py +0 -0
  134. {secator-0.4.0 → secator-0.5.0}/secator/tasks/katana.py +0 -0
  135. {secator-0.4.0 → secator-0.5.0}/secator/tasks/maigret.py +0 -0
  136. {secator-0.4.0 → secator-0.5.0}/secator/tasks/mapcidr.py +0 -0
  137. {secator-0.4.0 → secator-0.5.0}/secator/tasks/msfconsole.py +0 -0
  138. {secator-0.4.0 → secator-0.5.0}/secator/tasks/naabu.py +0 -0
  139. {secator-0.4.0 → secator-0.5.0}/secator/tasks/nuclei.py +0 -0
  140. {secator-0.4.0 → secator-0.5.0}/secator/tasks/subfinder.py +0 -0
  141. {secator-0.4.0 → secator-0.5.0}/secator/tasks/wpscan.py +0 -0
  142. {secator-0.4.0 → secator-0.5.0}/secator/template.py +0 -0
  143. {secator-0.4.0 → secator-0.5.0}/tests/__init__.py +0 -0
  144. {secator-0.4.0 → secator-0.5.0}/tests/fixtures/h8mail_breach.txt +0 -0
  145. {secator-0.4.0 → secator-0.5.0}/tests/fixtures/msfconsole_input.rc +0 -0
  146. {secator-0.4.0 → secator-0.5.0}/tests/fixtures/nmap_output.xml +0 -0
  147. {secator-0.4.0 → secator-0.5.0}/tests/integration/__init__.py +0 -0
  148. {secator-0.4.0 → secator-0.5.0}/tests/integration/inputs.py +0 -0
  149. {secator-0.4.0 → secator-0.5.0}/tests/integration/outputs.py +0 -0
  150. {secator-0.4.0 → secator-0.5.0}/tests/integration/setup.sh +0 -0
  151. {secator-0.4.0 → secator-0.5.0}/tests/integration/teardown.sh +0 -0
  152. {secator-0.4.0 → secator-0.5.0}/tests/integration/test_scans.py +0 -0
  153. {secator-0.4.0 → secator-0.5.0}/tests/integration/test_tasks.py +0 -0
  154. {secator-0.4.0 → secator-0.5.0}/tests/integration/test_worker.py +0 -0
  155. {secator-0.4.0 → secator-0.5.0}/tests/integration/test_workflows.py +0 -0
  156. {secator-0.4.0 → secator-0.5.0}/tests/integration/wordlist.txt +0 -0
  157. {secator-0.4.0 → secator-0.5.0}/tests/integration/wordlist_dns.txt +0 -0
  158. {secator-0.4.0 → secator-0.5.0}/tests/integration/wordpress_toolbox/Dockerfile +0 -0
  159. {secator-0.4.0 → secator-0.5.0}/tests/integration/wordpress_toolbox/Makefile +0 -0
  160. {secator-0.4.0 → secator-0.5.0}/tests/performance/__init__.py +0 -0
  161. {secator-0.4.0 → secator-0.5.0}/tests/performance/loadtester.py +0 -0
  162. {secator-0.4.0 → secator-0.5.0}/tests/unit/__init__.py +0 -0
  163. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_celery.py +0 -0
  164. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_config.py +0 -0
  165. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_scans.py +0 -0
  166. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_serializers.py +0 -0
  167. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_tasks.py +0 -0
  168. {secator-0.4.0 → secator-0.5.0}/tests/unit/test_workflows.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.0](https://github.com/freelabz/secator/compare/v0.4.1...v0.5.0) (2024-05-03)
4
+
5
+
6
+ ### Features
7
+
8
+ * add searchsploit output fields ([#278](https://github.com/freelabz/secator/issues/278)) ([00872c4](https://github.com/freelabz/secator/commit/00872c4a7f9b1ec76ee1bfd7a00919d53cbdb30a))
9
+ * **cli:** add report list / export commands ([#367](https://github.com/freelabz/secator/issues/367)) ([ab396a3](https://github.com/freelabz/secator/commit/ab396a3098c6d4c46cf9c9b29bd5c54579421646))
10
+ * **config:** load external tasks from template dir ([#373](https://github.com/freelabz/secator/issues/373)) ([0c63c02](https://github.com/freelabz/secator/commit/0c63c02c8eca477a6752f4af466c4303801019de))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **cli:** catch JSON parse errors ([#378](https://github.com/freelabz/secator/issues/378)) ([5e3d7f2](https://github.com/freelabz/secator/commit/5e3d7f2d2938a857e7599a429a6cfabf3b12347b))
16
+ * **nmap:** resolve -sS tcp syn stealth issue ([#376](https://github.com/freelabz/secator/issues/376)) ([a3efc65](https://github.com/freelabz/secator/commit/a3efc651dfa4d8fa34d611b9aea2e156352fdc45))
17
+
18
+ ## [0.4.1](https://github.com/freelabz/secator/compare/v0.4.0...v0.4.1) (2024-04-30)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * failed addons import ([#368](https://github.com/freelabz/secator/issues/368)) ([aee7ede](https://github.com/freelabz/secator/commit/aee7edeee1e96292e637b9161034f0d628a1f386))
24
+ * load dotenv before config import ([#370](https://github.com/freelabz/secator/issues/370)) ([ba2ea8e](https://github.com/freelabz/secator/commit/ba2ea8e3624dda7268d3788c0541fc0d37195358))
25
+
3
26
  ## [0.4.0](https://github.com/freelabz/secator/compare/v0.3.6...v0.4.0) (2024-04-27)
4
27
 
5
28
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: secator
3
- Version: 0.4.0
3
+ Version: 0.5.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.4.0"
7
+ version = "0.5.0"
8
8
  authors = [{ name = "FreeLabz", email = "sales@freelabz.com" }]
9
9
  readme = "README.md"
10
10
  description = "The pentester's swiss knife."
@@ -20,7 +20,8 @@ from secator.decorators import OrderedGroup, register_runner
20
20
  from secator.definitions import ADDONS_ENABLED, ASCII, DEV_PACKAGE, OPT_NOT_SUPPORTED, VERSION
21
21
  from secator.installer import ToolInstaller, fmt_health_table_row, get_health_table, get_version_info
22
22
  from secator.rich import console
23
- from secator.runners import Command
23
+ from secator.runners import Command, Runner
24
+ from secator.report import Report
24
25
  from secator.serializers.dataclass import loads_dataclass
25
26
  from secator.utils import debug, detect_host, discover_tasks, flatten, print_results_table, print_version
26
27
 
@@ -536,17 +537,72 @@ def report():
536
537
 
537
538
  @report.command('show')
538
539
  @click.argument('json_path')
540
+ @click.option('-o', '--output', type=str, default='console', help='Format')
539
541
  @click.option('-e', '--exclude-fields', type=str, default='', help='List of fields to exclude (comma-separated)')
540
- def report_show(json_path, exclude_fields):
541
- """Show a JSON report as a nicely-formatted table."""
542
+ def report_show(json_path, output, exclude_fields):
543
+ """Show a JSON report."""
542
544
  with open(json_path, 'r') as f:
543
545
  report = loads_dataclass(f.read())
544
546
  results = flatten(list(report['results'].values()))
545
- exclude_fields = exclude_fields.split(',')
546
- print_results_table(
547
- results,
548
- title=report['info']['title'],
549
- exclude_fields=exclude_fields)
547
+ if output == 'console':
548
+ for result in results:
549
+ console.print(result)
550
+ elif output == 'table':
551
+ exclude_fields = exclude_fields.split(',')
552
+ print_results_table(
553
+ results,
554
+ title=report['info']['title'],
555
+ exclude_fields=exclude_fields)
556
+
557
+
558
+ @report.command('list')
559
+ @click.option('-ws', '--workspace', type=str)
560
+ def report_list(workspace):
561
+ reports_dir = CONFIG.dirs.reports
562
+ json_reports = reports_dir.glob("**/**/report.json")
563
+ ws_reports = {}
564
+ for path in json_reports:
565
+ ws, runner, number = str(path).split('/')[-4:-1]
566
+ if ws not in ws_reports:
567
+ ws_reports[ws] = []
568
+ with open(path, 'r') as f:
569
+ try:
570
+ content = json.loads(f.read())
571
+ data = {'path': path, 'name': content['info']['name'], 'runner': runner}
572
+ ws_reports[ws].append(data)
573
+ except json.JSONDecodeError as e:
574
+ console.print(f'[bold red]Could not load {path}: {str(e)}')
575
+
576
+ for ws in ws_reports:
577
+ if workspace and not ws == workspace:
578
+ continue
579
+ console.print(f'[bold gold3]{ws}:')
580
+ for data in sorted(ws_reports[ws], key=lambda x: x['path']):
581
+ console.print(f' • {data["path"]} ([bold blue]{data["name"]}[/] [dim]{data["runner"][:-1]}[/])')
582
+
583
+
584
+ @report.command('export')
585
+ @click.argument('json_path', type=str)
586
+ @click.option('--output-folder', '-of', type=str)
587
+ @click.option('-output', '-o', type=str)
588
+ def report_export(json_path, output_folder, output):
589
+ with open(json_path, 'r') as f:
590
+ data = loads_dataclass(f.read())
591
+ flatten(list(data['results'].values()))
592
+
593
+ runner_instance = DotMap({
594
+ "config": {
595
+ "name": data['info']['name']
596
+ },
597
+ "workspace_name": json_path.split('/')[-4],
598
+ "reports_folder": output_folder or Path.cwd(),
599
+ "data": data,
600
+ "results": flatten(list(data['results'].values()))
601
+ })
602
+ exporters = Runner.resolve_exporters(output)
603
+ report = Report(runner_instance, title=data['info']['title'], exporters=exporters)
604
+ report.data = data
605
+ report.send()
550
606
 
551
607
 
552
608
  #--------#
@@ -592,6 +648,24 @@ def health(json, debug):
592
648
  table.add_row(*row)
593
649
  status['secator'] = info
594
650
 
651
+ # Check addons
652
+ console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
653
+ table = get_health_table()
654
+ with Live(table, console=console):
655
+ for addon in ['worker', 'google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
656
+ addon_var = ADDONS_ENABLED[addon]
657
+ info = {
658
+ 'name': addon,
659
+ 'version': None,
660
+ 'status': 'ok' if addon_var else 'missing',
661
+ 'latest_version': None,
662
+ 'installed': addon_var,
663
+ 'location': None
664
+ }
665
+ row = fmt_health_table_row(info, 'addons')
666
+ table.add_row(*row)
667
+ status['addons'][addon] = info
668
+
595
669
  # Check languages
596
670
  console.print('\n:wrench: [bold gold3]Checking installed languages ...[/]')
597
671
  version_cmds = {'go': 'version', 'python3': '--version', 'ruby': '--version'}
@@ -616,24 +690,6 @@ def health(json, debug):
616
690
  table.add_row(*row)
617
691
  status['tools'][tool.__name__] = info
618
692
 
619
- # # Check addons
620
- console.print('\n:wrench: [bold gold3]Checking installed addons ...[/]')
621
- table = get_health_table()
622
- with Live(table, console=console):
623
- for addon in ['worker', 'google', 'mongodb', 'redis', 'dev', 'trace', 'build']:
624
- addon_var = ADDONS_ENABLED[addon]
625
- info = {
626
- 'name': addon,
627
- 'version': None,
628
- 'status': 'ok' if addon_var else 'missing',
629
- 'latest_version': None,
630
- 'installed': addon_var,
631
- 'location': None
632
- }
633
- row = fmt_health_table_row(info, 'addons')
634
- table.add_row(*row)
635
- status['addons'][addon] = info
636
-
637
693
  # Print JSON health
638
694
  if json:
639
695
  import json as _json
@@ -1052,5 +1108,5 @@ def integration(tasks, workflows, scans, test, debug):
1052
1108
  @test.command()
1053
1109
  def coverage():
1054
1110
  """Run coverage report."""
1055
- cmd = f'{sys.executable} -m coverage report -m --omit=*/site-packages/*,*/tests/*'
1111
+ cmd = f'{sys.executable} -m coverage report -m --omit=*/site-packages/*,*/tests/*,*/templates/*'
1056
1112
  run_test(cmd, 'coverage')
@@ -6,11 +6,14 @@ from typing_extensions import Annotated, Self
6
6
 
7
7
  import requests
8
8
  import yaml
9
+ from dotenv import find_dotenv, load_dotenv
9
10
  from dotmap import DotMap
10
11
  from pydantic import AfterValidator, BaseModel, model_validator, ValidationError
11
12
 
12
13
  from secator.rich import console, console_stdout
13
14
 
15
+ load_dotenv(find_dotenv(usecwd=True), override=False)
16
+
14
17
  Directory = Annotated[Path, AfterValidator(lambda v: v.expanduser())]
15
18
  StrExpandHome = Annotated[str, AfterValidator(lambda v: v.replace('~', str(Path.home())))]
16
19
 
@@ -245,9 +248,9 @@ class Config(DotMap):
245
248
  value = float(value)
246
249
  elif isinstance(existing_value, Path):
247
250
  value = Path(value)
248
- except ValueError as e:
249
- from secator.utils import debug
250
- debug(f'Could not cast value {value} to expected type {type(existing_value).__name__}: {str(e)}', sub='config')
251
+ except ValueError:
252
+ # from secator.utils import debug
253
+ # debug(f'Could not cast value {value} to expected type {type(existing_value).__name__}: {str(e)}', sub='config')
251
254
  pass
252
255
  finally:
253
256
  target[final_key] = value
@@ -2,12 +2,10 @@
2
2
 
3
3
  import os
4
4
 
5
- from dotenv import find_dotenv, load_dotenv
6
5
  from importlib.metadata import version
7
6
 
8
7
  from secator.config import CONFIG, ROOT_FOLDER
9
8
 
10
- load_dotenv(find_dotenv(usecwd=True), override=False)
11
9
 
12
10
  # Globals
13
11
  VERSION = version('secator')
@@ -105,56 +103,30 @@ WORDLIST = 'wordlist'
105
103
  WORDS = 'words'
106
104
 
107
105
 
106
+ def is_importable(module_to_import):
107
+ import importlib
108
+ try:
109
+ importlib.import_module(module_to_import)
110
+ return True
111
+ except ModuleNotFoundError:
112
+ return False
113
+ except Exception as e:
114
+ print(f'Failed trying to import {module_to_import}: {str(e)}')
115
+ return False
116
+
117
+
108
118
  ADDONS_ENABLED = {}
109
119
 
110
- # Check worker addon
111
- try:
112
- import eventlet # noqa: F401
113
- ADDONS_ENABLED['worker'] = True
114
- except ModuleNotFoundError:
115
- ADDONS_ENABLED['worker'] = False
116
-
117
- # Check google addon
118
- try:
119
- import gspread # noqa: F401
120
- ADDONS_ENABLED['google'] = True
121
- except ModuleNotFoundError:
122
- ADDONS_ENABLED['google'] = False
123
-
124
- # Check mongodb addon
125
- try:
126
- import pymongo # noqa: F401
127
- ADDONS_ENABLED['mongodb'] = True
128
- except ModuleNotFoundError:
129
- ADDONS_ENABLED['mongodb'] = False
130
-
131
- # Check redis addon
132
- try:
133
- import redis # noqa: F401
134
- ADDONS_ENABLED['redis'] = True
135
- except ModuleNotFoundError:
136
- ADDONS_ENABLED['redis'] = False
137
-
138
- # Check dev addon
139
- try:
140
- import flake8 # noqa: F401
141
- ADDONS_ENABLED['dev'] = True
142
- except ModuleNotFoundError:
143
- ADDONS_ENABLED['dev'] = False
144
-
145
- # Check build addon
146
- try:
147
- import hatch # noqa: F401
148
- ADDONS_ENABLED['build'] = True
149
- except ModuleNotFoundError:
150
- ADDONS_ENABLED['build'] = False
151
-
152
- # Check trace addon
153
- try:
154
- import memray # noqa: F401
155
- ADDONS_ENABLED['trace'] = True
156
- except ModuleNotFoundError:
157
- ADDONS_ENABLED['trace'] = False
120
+ for addon, module in [
121
+ ('worker', 'eventlet'),
122
+ ('google', 'gspread'),
123
+ ('mongodb', 'pymongo'),
124
+ ('redis', 'redis'),
125
+ ('dev', 'flake8'),
126
+ ('trace', 'memray'),
127
+ ('build', 'hatch')
128
+ ]:
129
+ ADDONS_ENABLED[addon] = is_importable(module)
158
130
 
159
131
  # Check dev package
160
132
  if os.path.exists(f'{ROOT_FOLDER}/pyproject.toml'):
@@ -34,6 +34,9 @@ class Exploit(OutputType):
34
34
  ]
35
35
  _sort_by = ('matched_at', 'name')
36
36
 
37
+ def __str__(self):
38
+ return self.name
39
+
37
40
  def __repr__(self):
38
41
  s = f'[bold red]⍼[/] \[[bold red]{self.name}'
39
42
  if self.reference:
@@ -89,11 +89,5 @@ class Vulnerability(OutputType):
89
89
  s = f'[dim]{s}[/]'
90
90
  return rich_to_ansi(s)
91
91
 
92
- # def __gt__(self, other):
93
- # # favor httpx over other url info tools
94
- # if self._source == 'httpx' and other._source != 'httpx':
95
- # return True
96
- # return super().__gt__(other)
97
-
98
92
  def __str__(self):
99
93
  return self.matched_at + ' -> ' + self.name
@@ -1,7 +1,7 @@
1
1
  import operator
2
2
 
3
3
  from secator.output_types import OUTPUT_TYPES, OutputType
4
- from secator.utils import merge_opts, get_file_timestamp, print_results_table
4
+ from secator.utils import merge_opts, get_file_timestamp
5
5
  from secator.rich import console
6
6
 
7
7
 
@@ -22,9 +22,6 @@ class Report:
22
22
  self.workspace_name = runner.workspace_name
23
23
  self.output_folder = runner.reports_folder
24
24
 
25
- def as_table(self):
26
- print_results_table(self.results, self.title)
27
-
28
25
  def send(self):
29
26
  for report_cls in self.exporters:
30
27
  try:
@@ -92,7 +92,6 @@ class Runner:
92
92
  self.workspace_name = context.get('workspace_name', 'default')
93
93
  self.run_opts = run_opts.copy()
94
94
  self.sync = run_opts.get('sync', True)
95
- self.exporters = self.resolve_exporters()
96
95
  self.done = False
97
96
  self.start_time = datetime.fromtimestamp(time())
98
97
  self.last_updated = None
@@ -109,6 +108,10 @@ class Runner:
109
108
  self.uuids = []
110
109
  self.celery_result = None
111
110
 
111
+ # Determine exporters
112
+ exporters_str = self.run_opts.get('output') or self.default_exporters
113
+ self.exporters = Runner.resolve_exporters(exporters_str)
114
+
112
115
  # Determine report folder
113
116
  default_reports_folder_base = f'{CONFIG.dirs.reports}/{self.workspace_name}/{self.config.type}s'
114
117
  _id = get_task_folder_id(default_reports_folder_base)
@@ -294,7 +297,7 @@ class Runner:
294
297
  debug('running duplicate check', id=self.config.name, sub='runner.mark_duplicates')
295
298
  dupe_count = 0
296
299
  for item in self.results:
297
- debug('running duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=5)
300
+ # debug('running duplicate check', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=5)
298
301
  others = [f for f in self.results if f == item and f._uuid != item._uuid]
299
302
  if others:
300
303
  main = max(item, *others)
@@ -303,6 +306,7 @@ class Runner:
303
306
  main._related.extend([dupe._uuid for dupe in dupes])
304
307
  main._related = list(dict.fromkeys(main._related))
305
308
  if main._uuid != item._uuid:
309
+ debug(f'found {len(others)} duplicates for', obj=item.toDict(), obj_breaklines=True, sub='runner.mark_duplicates', level=5) # noqa: E501
306
310
  item._duplicate = True
307
311
  item = self.run_hooks('on_item', item)
308
312
  if item._uuid not in main._related:
@@ -390,19 +394,19 @@ class Runner:
390
394
  return False
391
395
  return True
392
396
 
393
- def resolve_exporters(self):
397
+ @staticmethod
398
+ def resolve_exporters(exporters):
394
399
  """Resolve exporters from output options."""
395
- output = self.run_opts.get('output') or self.default_exporters
396
- if not output or output in ['false', 'False']:
400
+ if not exporters or exporters in ['false', 'False']:
397
401
  return []
398
- if isinstance(output, str):
399
- output = output.split(',')
400
- exporters = [
402
+ if isinstance(exporters, str):
403
+ exporters = exporters.split(',')
404
+ classes = [
401
405
  import_dynamic(f'secator.exporters.{o.capitalize()}Exporter', 'Exporter')
402
- for o in output
406
+ for o in exporters
403
407
  if o
404
408
  ]
405
- return [e for e in exporters if e]
409
+ return [cls for cls in classes if cls]
406
410
 
407
411
  def log_start(self):
408
412
  """Log runner start."""
@@ -0,0 +1,8 @@
1
+ from secator.utils import discover_tasks
2
+ TASKS = discover_tasks()
3
+ __all__ = [
4
+ cls.__name__
5
+ for cls in TASKS
6
+ ]
7
+ for cls in TASKS:
8
+ exec(f'from .{cls.__name__} import {cls.__name__}')
@@ -51,7 +51,8 @@ class nmap(VulnMulti):
51
51
 
52
52
  # Nmap opts
53
53
  PORTS: '-p',
54
- 'output_path': '-oX'
54
+ 'output_path': '-oX',
55
+ 'tcp_syn_stealth': '-sS'
55
56
  }
56
57
  opt_value_map = {
57
58
  PORTS: lambda x: ','.join([str(p) for p in x]) if isinstance(x, list) else x
@@ -73,6 +74,10 @@ class nmap(VulnMulti):
73
74
  output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.xml'
74
75
  self.output_path = output_path
75
76
  self.cmd += f' -oX {self.output_path}'
77
+ tcp_syn_stealth = self.get_opt_value('tcp_syn_stealth')
78
+ if tcp_syn_stealth:
79
+ self.cmd = f'sudo {self.cmd}'
80
+ self.cmd = self.cmd.replace('-sT', '')
76
81
 
77
82
  def yielder(self):
78
83
  yield from super().yielder()
@@ -1,3 +1,5 @@
1
+ import re
2
+
1
3
  from secator.decorators import task
2
4
  from secator.definitions import (CVES, EXTRA_DATA, ID, MATCHED_AT, NAME,
3
5
  PROVIDER, REFERENCE, TAGS, OPT_NOT_SUPPORTED)
@@ -5,6 +7,9 @@ from secator.output_types import Exploit
5
7
  from secator.runners import Command
6
8
 
7
9
 
10
+ SEARCHSPLOIT_TITLE_REGEX = re.compile(r'^((?:[a-zA-Z\-_!\.()]+\d?\s?)+)\.?\s*(.*)$')
11
+
12
+
8
13
  @task()
9
14
  class searchsploit(Command):
10
15
  """Exploit-DB command line search tool."""
@@ -19,12 +24,15 @@ class searchsploit(Command):
19
24
  output_types = [Exploit]
20
25
  output_map = {
21
26
  Exploit: {
22
- NAME: lambda x: '-'.join(x['Title'].split('-')[1:]).strip(),
23
- PROVIDER: lambda x: 'EDB',
27
+ NAME: 'Title',
24
28
  ID: 'EDB-ID',
29
+ PROVIDER: lambda x: 'EDB',
25
30
  CVES: lambda x: [c for c in x['Codes'].split(';') if c.startswith('CVE-')],
26
31
  REFERENCE: lambda x: f'https://exploit-db.com/exploits/{x["EDB-ID"]}',
27
- EXTRA_DATA: lambda x: {'verified': x['Verified']}
32
+ TAGS: lambda x: searchsploit.tags_extractor(x),
33
+ EXTRA_DATA: lambda x: {
34
+ k.lower().replace('date_', ''): v for k, v in x.items() if k not in ['Title', 'EDB-ID', 'Codes', 'Tags', 'Source'] and v != '' # noqa: E501
35
+ }
28
36
  }
29
37
  }
30
38
  install_cmd = 'sudo git clone https://gitlab.com/exploit-database/exploitdb.git /opt/exploitdb || true && sudo ln -sf /opt/exploitdb/searchsploit /usr/local/bin/searchsploit' # noqa: E501
@@ -34,6 +42,18 @@ class searchsploit(Command):
34
42
  input_chunk_size = 1
35
43
  profile = 'io'
36
44
 
45
+ @staticmethod
46
+ def tags_extractor(item):
47
+ tags = []
48
+ for tag in item['Tags'].split(','):
49
+ _tag = '_'.join(
50
+ tag.lower().replace('-', '_',).replace('(', '').replace(')', '').split(' ')
51
+ )
52
+ if not _tag:
53
+ continue
54
+ tags.append(tag)
55
+ return tags
56
+
37
57
  @staticmethod
38
58
  def before_init(self):
39
59
  _in = self.input
@@ -49,5 +69,23 @@ class searchsploit(Command):
49
69
  def on_item_pre_convert(self, item):
50
70
  if self.matched_at:
51
71
  item[MATCHED_AT] = self.matched_at
52
- item[TAGS] = [self.input.replace('\'', '')]
72
+ return item
73
+
74
+ @staticmethod
75
+ def on_item(self, item):
76
+ match = SEARCHSPLOIT_TITLE_REGEX.match(item.name)
77
+ # if not match:
78
+ # self._print(f'[bold red]{item.name} ({item.reference}) did not match SEARCHSPLOIT_TITLE_REGEX. Please report this issue.[/]') # noqa: E501
79
+ if match:
80
+ group = match.groups()
81
+ product = '-'.join(group[0].strip().split(' '))
82
+ if len(group[1]) > 1:
83
+ versions, title = tuple(group[1].split(' - '))
84
+ item.name = title
85
+ product_info = [f'{product.lower()} {v.strip()}' for v in versions.split('/')]
86
+ item.tags = product_info + item.tags
87
+ # else:
88
+ # self._print(f'[bold red]{item.name} ({item.reference}) did not quite match SEARCHSPLOIT_TITLE_REGEX. Please report this issue.[/]') # noqa: E501
89
+ input_tag = '-'.join(self.input.replace('\'', '').split(' '))
90
+ item.tags = [input_tag] + item.tags
53
91
  return item
@@ -1,4 +1,5 @@
1
1
  import inspect
2
+ import importlib
2
3
  import itertools
3
4
  import logging
4
5
  import operator
@@ -8,7 +9,7 @@ import select
8
9
  import sys
9
10
  import warnings
10
11
  from datetime import datetime
11
- from importlib import import_module
12
+
12
13
  from inspect import isclass
13
14
  from pathlib import Path
14
15
  from pkgutil import iter_modules
@@ -138,7 +139,7 @@ def discover_internal_tasks():
138
139
  if module_name.startswith('_'):
139
140
  continue
140
141
  try:
141
- module = import_module(f'secator.tasks.{module_name}')
142
+ module = importlib.import_module(f'secator.tasks.{module_name}')
142
143
  except ImportError as e:
143
144
  console.print(f'[bold red]Could not import secator.tasks.{module_name}:[/]')
144
145
  console.print(f'\t[bold red]{type(e).__name__}[/]: {str(e)}')
@@ -160,17 +161,32 @@ def discover_internal_tasks():
160
161
 
161
162
  def discover_external_tasks():
162
163
  """Find external secator tasks."""
163
- if not os.path.exists('config.secator'):
164
- return []
165
- with open('config.secator', 'r') as f:
166
- classes = f.read().splitlines()
167
164
  output = []
168
- for cls_path in classes:
169
- cls = import_dynamic(cls_path, cls_root='Command')
170
- if not cls:
171
- continue
172
- # logger.warning(f'Added external tool {cls_path}')
173
- output.append(cls)
165
+ sys.dont_write_bytecode = True
166
+ for path in CONFIG.dirs.templates.glob('**/*.py'):
167
+ try:
168
+ task_name = path.stem
169
+ module_name = f'secator.tasks.{task_name}'
170
+
171
+ # console.print(f'Importing module {module_name} from {path}')
172
+ spec = importlib.util.spec_from_file_location(module_name, path)
173
+ module = importlib.util.module_from_spec(spec)
174
+ # console.print(f'Adding module "{module_name}" to sys path')
175
+ sys.modules[module_name] = module
176
+
177
+ # console.print(f'Executing module "{module}"')
178
+ spec.loader.exec_module(module)
179
+
180
+ # console.print(f'Checking that {module} contains task {task_name}')
181
+ if not hasattr(module, task_name):
182
+ console.print(f'[bold orange1]Could not load external task "{task_name}" from module {path.name}[/] ({path})')
183
+ continue
184
+ cls = getattr(module, task_name)
185
+ console.print(f'[bold green]Successfully loaded external task "{task_name}"[/] ({path})')
186
+ output.append(cls)
187
+ except Exception as e:
188
+ console.print(f'[bold red]Could not load external module {path.name}. Reason: {str(e)}.[/] ({path})')
189
+ sys.dont_write_bytecode = False
174
190
  return output
175
191
 
176
192
 
@@ -194,7 +210,7 @@ def import_dynamic(cls_path, cls_root='Command'):
194
210
  """
195
211
  try:
196
212
  package, name = cls_path.rsplit(".", maxsplit=1)
197
- cls = getattr(import_module(package), name)
213
+ cls = getattr(importlib.import_module(package), name)
198
214
  root_cls = inspect.getmro(cls)[-2]
199
215
  if root_cls.__name__ == cls_root:
200
216
  return cls
@@ -1,6 +1,7 @@
1
1
  import contextlib
2
2
  import json
3
3
  import os
4
+ import sys
4
5
  import unittest.mock
5
6
 
6
7
  from fp.fp import FreeProxy
@@ -182,3 +183,15 @@ class CommandOutputTester: # Mixin for unittest.TestCase
182
183
  raise
183
184
 
184
185
  console.print('[bold green] ok[/]')
186
+
187
+
188
+ def clear_modules():
189
+ """Clear all secator modules imports.
190
+ See https://stackoverflow.com/questions/7460363/re-import-module-under-test-to-lose-context for context.
191
+ """
192
+ keys_to_delete = []
193
+ for k, _ in sys.modules.items():
194
+ if k.startswith('secator'):
195
+ keys_to_delete.append(k)
196
+ for k in keys_to_delete:
197
+ del sys.modules[k]
@@ -0,0 +1,36 @@
1
+ from secator.runners import Command
2
+ from secator.decorators import task
3
+ from secator.output_types import Vulnerability
4
+
5
+
6
+ @task()
7
+ class ls(Command):
8
+ cmd = 'ls -al'
9
+ output_types = [Vulnerability]
10
+ output_map = {
11
+ Vulnerability: {}
12
+ }
13
+
14
+ @staticmethod
15
+ def item_loader(self, line):
16
+ fields = ['permissions', 'link_count', 'owner', 'group', 'size', 'month', 'day', 'hour', 'path']
17
+ result = [c for c in line.split(' ') if c]
18
+ if len(result) != len(fields):
19
+ return None
20
+ data = {}
21
+ for ix, value in enumerate(result):
22
+ data[fields[ix]] = value
23
+
24
+ # Output vulnerabilities
25
+ permissions = data['permissions']
26
+ path = data['path']
27
+ full_path = f'{self.input}/{path}'
28
+ if permissions[-2] == 'w': # found a vulnerability !
29
+ yield Vulnerability(
30
+ name='World-writeable path',
31
+ severity='high',
32
+ confidence='high',
33
+ provider='ls',
34
+ matched_at=full_path,
35
+ extra_data={k: v for k, v in data.items() if k != 'path'}
36
+ )