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.
Files changed (150) hide show
  1. secator/.gitignore +162 -0
  2. secator/__init__.py +0 -0
  3. secator/celery.py +453 -0
  4. secator/celery_signals.py +138 -0
  5. secator/celery_utils.py +320 -0
  6. secator/cli.py +2035 -0
  7. secator/cli_helper.py +395 -0
  8. secator/click.py +87 -0
  9. secator/config.py +670 -0
  10. secator/configs/__init__.py +0 -0
  11. secator/configs/profiles/__init__.py +0 -0
  12. secator/configs/profiles/aggressive.yaml +8 -0
  13. secator/configs/profiles/all_ports.yaml +7 -0
  14. secator/configs/profiles/full.yaml +31 -0
  15. secator/configs/profiles/http_headless.yaml +7 -0
  16. secator/configs/profiles/http_record.yaml +8 -0
  17. secator/configs/profiles/insane.yaml +8 -0
  18. secator/configs/profiles/paranoid.yaml +8 -0
  19. secator/configs/profiles/passive.yaml +11 -0
  20. secator/configs/profiles/polite.yaml +8 -0
  21. secator/configs/profiles/sneaky.yaml +8 -0
  22. secator/configs/profiles/tor.yaml +5 -0
  23. secator/configs/scans/__init__.py +0 -0
  24. secator/configs/scans/domain.yaml +31 -0
  25. secator/configs/scans/host.yaml +23 -0
  26. secator/configs/scans/network.yaml +30 -0
  27. secator/configs/scans/subdomain.yaml +27 -0
  28. secator/configs/scans/url.yaml +19 -0
  29. secator/configs/workflows/__init__.py +0 -0
  30. secator/configs/workflows/cidr_recon.yaml +48 -0
  31. secator/configs/workflows/code_scan.yaml +29 -0
  32. secator/configs/workflows/domain_recon.yaml +46 -0
  33. secator/configs/workflows/host_recon.yaml +95 -0
  34. secator/configs/workflows/subdomain_recon.yaml +120 -0
  35. secator/configs/workflows/url_bypass.yaml +15 -0
  36. secator/configs/workflows/url_crawl.yaml +98 -0
  37. secator/configs/workflows/url_dirsearch.yaml +62 -0
  38. secator/configs/workflows/url_fuzz.yaml +68 -0
  39. secator/configs/workflows/url_params_fuzz.yaml +66 -0
  40. secator/configs/workflows/url_secrets_hunt.yaml +23 -0
  41. secator/configs/workflows/url_vuln.yaml +91 -0
  42. secator/configs/workflows/user_hunt.yaml +29 -0
  43. secator/configs/workflows/wordpress.yaml +38 -0
  44. secator/cve.py +718 -0
  45. secator/decorators.py +7 -0
  46. secator/definitions.py +168 -0
  47. secator/exporters/__init__.py +14 -0
  48. secator/exporters/_base.py +3 -0
  49. secator/exporters/console.py +10 -0
  50. secator/exporters/csv.py +37 -0
  51. secator/exporters/gdrive.py +123 -0
  52. secator/exporters/json.py +16 -0
  53. secator/exporters/table.py +36 -0
  54. secator/exporters/txt.py +28 -0
  55. secator/hooks/__init__.py +0 -0
  56. secator/hooks/gcs.py +80 -0
  57. secator/hooks/mongodb.py +281 -0
  58. secator/installer.py +694 -0
  59. secator/loader.py +128 -0
  60. secator/output_types/__init__.py +49 -0
  61. secator/output_types/_base.py +108 -0
  62. secator/output_types/certificate.py +78 -0
  63. secator/output_types/domain.py +50 -0
  64. secator/output_types/error.py +42 -0
  65. secator/output_types/exploit.py +58 -0
  66. secator/output_types/info.py +24 -0
  67. secator/output_types/ip.py +47 -0
  68. secator/output_types/port.py +55 -0
  69. secator/output_types/progress.py +36 -0
  70. secator/output_types/record.py +36 -0
  71. secator/output_types/stat.py +41 -0
  72. secator/output_types/state.py +29 -0
  73. secator/output_types/subdomain.py +45 -0
  74. secator/output_types/tag.py +69 -0
  75. secator/output_types/target.py +38 -0
  76. secator/output_types/url.py +112 -0
  77. secator/output_types/user_account.py +41 -0
  78. secator/output_types/vulnerability.py +101 -0
  79. secator/output_types/warning.py +30 -0
  80. secator/report.py +140 -0
  81. secator/rich.py +130 -0
  82. secator/runners/__init__.py +14 -0
  83. secator/runners/_base.py +1240 -0
  84. secator/runners/_helpers.py +218 -0
  85. secator/runners/celery.py +18 -0
  86. secator/runners/command.py +1178 -0
  87. secator/runners/python.py +126 -0
  88. secator/runners/scan.py +87 -0
  89. secator/runners/task.py +81 -0
  90. secator/runners/workflow.py +168 -0
  91. secator/scans/__init__.py +29 -0
  92. secator/serializers/__init__.py +8 -0
  93. secator/serializers/dataclass.py +39 -0
  94. secator/serializers/json.py +45 -0
  95. secator/serializers/regex.py +25 -0
  96. secator/tasks/__init__.py +8 -0
  97. secator/tasks/_categories.py +487 -0
  98. secator/tasks/arjun.py +113 -0
  99. secator/tasks/arp.py +53 -0
  100. secator/tasks/arpscan.py +70 -0
  101. secator/tasks/bbot.py +372 -0
  102. secator/tasks/bup.py +118 -0
  103. secator/tasks/cariddi.py +193 -0
  104. secator/tasks/dalfox.py +87 -0
  105. secator/tasks/dirsearch.py +84 -0
  106. secator/tasks/dnsx.py +186 -0
  107. secator/tasks/feroxbuster.py +93 -0
  108. secator/tasks/ffuf.py +135 -0
  109. secator/tasks/fping.py +85 -0
  110. secator/tasks/gau.py +102 -0
  111. secator/tasks/getasn.py +60 -0
  112. secator/tasks/gf.py +36 -0
  113. secator/tasks/gitleaks.py +96 -0
  114. secator/tasks/gospider.py +84 -0
  115. secator/tasks/grype.py +109 -0
  116. secator/tasks/h8mail.py +75 -0
  117. secator/tasks/httpx.py +167 -0
  118. secator/tasks/jswhois.py +36 -0
  119. secator/tasks/katana.py +203 -0
  120. secator/tasks/maigret.py +87 -0
  121. secator/tasks/mapcidr.py +42 -0
  122. secator/tasks/msfconsole.py +179 -0
  123. secator/tasks/naabu.py +85 -0
  124. secator/tasks/nmap.py +487 -0
  125. secator/tasks/nuclei.py +151 -0
  126. secator/tasks/search_vulns.py +225 -0
  127. secator/tasks/searchsploit.py +109 -0
  128. secator/tasks/sshaudit.py +299 -0
  129. secator/tasks/subfinder.py +48 -0
  130. secator/tasks/testssl.py +283 -0
  131. secator/tasks/trivy.py +130 -0
  132. secator/tasks/trufflehog.py +240 -0
  133. secator/tasks/urlfinder.py +100 -0
  134. secator/tasks/wafw00f.py +106 -0
  135. secator/tasks/whois.py +34 -0
  136. secator/tasks/wpprobe.py +116 -0
  137. secator/tasks/wpscan.py +202 -0
  138. secator/tasks/x8.py +94 -0
  139. secator/tasks/xurlfind3r.py +83 -0
  140. secator/template.py +294 -0
  141. secator/thread.py +24 -0
  142. secator/tree.py +196 -0
  143. secator/utils.py +922 -0
  144. secator/utils_test.py +297 -0
  145. secator/workflows/__init__.py +29 -0
  146. secator-0.22.0.dist-info/METADATA +447 -0
  147. secator-0.22.0.dist-info/RECORD +150 -0
  148. secator-0.22.0.dist-info/WHEEL +4 -0
  149. secator-0.22.0.dist-info/entry_points.txt +2 -0
  150. secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/utils_test.py ADDED
@@ -0,0 +1,297 @@
1
+ import contextlib
2
+ import json
3
+ import os
4
+ import re
5
+ import sys
6
+ import unittest.mock
7
+
8
+ from fp.fp import FreeProxy
9
+
10
+ from secator.definitions import (CIDR_RANGE, DELAY, DEPTH, EMAIL,
11
+ FOLLOW_REDIRECT, HEADER, HOST, IP, MATCH_CODES,
12
+ METHOD, PROXY, RATE_LIMIT, RETRIES,
13
+ THREADS, TIMEOUT, URL, USER_AGENT, USERNAME, PATH,
14
+ DOCKER_IMAGE, GIT_REPOSITORY)
15
+ from secator.loader import get_configs_by_type
16
+ from secator.output_types import EXECUTION_TYPES, STAT_TYPES
17
+ from secator.runners import Command, Task
18
+ from secator.rich import console
19
+ from secator.utils import load_fixture, debug, traceback_as_string
20
+
21
+ #---------#
22
+ # GLOBALS #
23
+ #---------#
24
+ USE_PROXY = bool(int(os.environ.get('USE_PROXY', '0')))
25
+ TEST_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/tests/'
26
+ FIXTURES_DIR = f'{TEST_DIR}/fixtures'
27
+ USE_PROXY = bool(int(os.environ.get('USE_PROXY', '0')))
28
+ TASKS = get_configs_by_type('task')
29
+ WORKFLOWS = get_configs_by_type('workflow')
30
+ SCANS = get_configs_by_type('scan')
31
+
32
+
33
+ #------------#
34
+ # TEST TASKS #
35
+ #------------#
36
+ TEST_TASKS = os.environ.get('TEST_TASKS', '')
37
+ if TEST_TASKS:
38
+ TEST_TASKS = [config for config in TASKS if config.name in TEST_TASKS.split(',')]
39
+ else:
40
+ TEST_TASKS = TASKS
41
+
42
+ #----------------#
43
+ # TEST WORKFLOWS #
44
+ #----------------#
45
+ TEST_WORKFLOWS = os.environ.get('TEST_WORKFLOWS', '')
46
+ if TEST_WORKFLOWS:
47
+ TEST_WORKFLOWS = [config for config in WORKFLOWS if config.name in TEST_WORKFLOWS.split(',')]
48
+ else:
49
+ TEST_WORKFLOWS = WORKFLOWS
50
+
51
+ #------------#
52
+ # TEST SCANS #
53
+ #------------#
54
+ TEST_SCANS = os.environ.get('TEST_SCANS', '')
55
+ if TEST_SCANS:
56
+ TEST_SCANS = [config for config in SCANS if config.name in TEST_SCANS.split(',')]
57
+ else:
58
+ TEST_SCANS = SCANS
59
+
60
+ #-------------------#
61
+ # TEST INPUTS_TASKS #
62
+ #-------------------#
63
+ INPUTS_TASKS = {
64
+ URL: 'https://fake.com',
65
+ HOST: 'fake.com',
66
+ USERNAME: 'test',
67
+ IP: '192.168.1.23',
68
+ CIDR_RANGE: '192.168.1.0/24',
69
+ EMAIL: 'fake@fake.com',
70
+ PATH: '.',
71
+ DOCKER_IMAGE: 'redis:latest',
72
+ GIT_REPOSITORY: 'https://github.com/freelabz/secator',
73
+ 'gf': 'http://testphp.vulnweb.com/hpp?pp=1',
74
+ 'maigret': 'Linus__Torvalds',
75
+ 'searchsploit': 'apache',
76
+ 'search_vulns': 'apache 2.4.39',
77
+ }
78
+
79
+ #---------------------#
80
+ # TEST FIXTURES_TASKS #
81
+ #---------------------#
82
+ FIXTURES_TASKS = {
83
+ Task.get_task_class(task.name): load_fixture(f'{task.name}_output', FIXTURES_DIR)
84
+ for task in TASKS
85
+ if task.name in [t.name for t in TEST_TASKS]
86
+ }
87
+
88
+ #-----------#
89
+ # TEST OPTS #
90
+ #-----------#
91
+ META_OPTS = {
92
+ HEADER: 'User-Agent: Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1;; Hello: World',
93
+ DELAY: 0,
94
+ DEPTH: 2,
95
+ FOLLOW_REDIRECT: True,
96
+ METHOD: 'GET',
97
+ MATCH_CODES: '200',
98
+ PROXY: FreeProxy(timeout=0.5).get() if USE_PROXY else False,
99
+ RATE_LIMIT: 10000,
100
+ RETRIES: 0,
101
+ THREADS: 50,
102
+ TIMEOUT: 1,
103
+ USER_AGENT: 'Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1',
104
+
105
+ # Individual tasks options
106
+ 'bup.mode': 'http_methods',
107
+ 'gf.pattern': 'xss',
108
+ 'nmap.output_path': load_fixture('nmap_output', FIXTURES_DIR, only_path=True, ext='.xml'), # nmap XML fixture
109
+ 'nmap.tcp_connect': True,
110
+ 'nmap.version_detection': True,
111
+ 'nmap.skip_host_discovery': True,
112
+ 'msfconsole.resource': load_fixture('msfconsole_input', FIXTURES_DIR, only_path=True),
113
+ 'dirsearch.output_path': load_fixture('dirsearch_output', FIXTURES_DIR, only_path=True),
114
+ 'gitleaks_output_path': load_fixture('gitleaks_output', FIXTURES_DIR, only_path=True),
115
+ 'maigret.output_path': load_fixture('maigret_output', FIXTURES_DIR, only_path=True),
116
+ 'nuclei.template_id': 'prometheus-metrics',
117
+ 'wpscan.output_path': load_fixture('wpscan_output', FIXTURES_DIR, only_path=True),
118
+ 'h8mail.output_path': load_fixture('h8mail_output', FIXTURES_DIR, only_path=True),
119
+ 'h8mail.local_breach': load_fixture('h8mail_breach', FIXTURES_DIR, only_path=True),
120
+ 'wpprobe.output_path': load_fixture('wpprobe_output', FIXTURES_DIR, only_path=True),
121
+ 'arjun.output_path': load_fixture('arjun_output', FIXTURES_DIR, only_path=True),
122
+ 'arjun.wordlist': False,
123
+ 'trivy.output_path': load_fixture('trivy_output', FIXTURES_DIR, only_path=True),
124
+ 'wafw00f.output_path': load_fixture('wafw00f_output', FIXTURES_DIR, only_path=True),
125
+ 'testssl.output_path': load_fixture('testssl_output', FIXTURES_DIR, only_path=True),
126
+ 'ssh_audit.output_path': load_fixture('ssh_audit_output', FIXTURES_DIR, only_path=True),
127
+ 'x8.wordlist': 'http_params'
128
+ }
129
+
130
+
131
+ def mock_subprocess_popen(output_list):
132
+ mock_process = unittest.mock.MagicMock()
133
+ mock_process.wait.return_value = 0
134
+ mock_process.stdout.readline.side_effect = output_list
135
+ mock_process.pid = None
136
+ mock_process.returncode = 0
137
+
138
+ def mock_popen(*args, **kwargs):
139
+ return mock_process
140
+
141
+ return unittest.mock.patch('subprocess.Popen', mock_popen)
142
+
143
+
144
+ @contextlib.contextmanager
145
+ def mock_command(cls, inputs=[], opts={}, fixture=None, method=''):
146
+ mocks = []
147
+ if isinstance(fixture, dict):
148
+ fixture = [fixture]
149
+
150
+ is_list = isinstance(fixture, list)
151
+ supports_list = next((loader for loader in cls.item_loaders if getattr(loader, 'list', False)), None)
152
+ if is_list:
153
+ if supports_list:
154
+ mocks.append(json.dumps(fixture))
155
+ else:
156
+ for item in fixture:
157
+ if isinstance(item, dict):
158
+ mocks.append(json.dumps(item))
159
+ else:
160
+ mocks.append(item)
161
+ else:
162
+ mocks.append(fixture)
163
+
164
+ with mock_subprocess_popen(mocks):
165
+ command = cls(inputs, **opts)
166
+ if method == 'run':
167
+ yield cls(inputs, **opts).run()
168
+ elif method == 'si':
169
+ yield cls.si([], inputs, **opts)
170
+ elif method in ['s', 'delay']:
171
+ yield getattr(cls, method)(inputs, **opts)
172
+ else:
173
+ yield command
174
+
175
+
176
+ class CommandOutputTester: # Mixin for unittest.TestCase
177
+
178
+ @staticmethod
179
+ def get_item_str(item):
180
+ return f"Item: {repr(item)}\nItem dict: {json.dumps(item.toDict(), default=str, indent=2)}"
181
+
182
+ def _test_runner_output(
183
+ self,
184
+ runner,
185
+ expected_output_keys=[],
186
+ expected_output_types=[],
187
+ expected_results=[],
188
+ expected_status='SUCCESS',
189
+ empty_results_allowed=False,
190
+ additional_checks=[]):
191
+
192
+ console.print(f'\t[dim]Testing {runner.config.type} {runner.name} ...[/]', end='')
193
+ debug('', sub='unittest')
194
+ debug('-' * 10 + f' RUNNER {runner.name} STARTING ' + '-' * 10, sub='unittest')
195
+
196
+ if not runner.inputs:
197
+ console.print('[dim gold3] skipped (no inputs defined).[/]')
198
+ return
199
+
200
+ if not expected_results and not expected_output_keys and not expected_output_types:
201
+ console.print('[dim gold3] (no outputs defined).[/]', end='')
202
+
203
+ try:
204
+ debug(f'{runner.name} starting command: {runner.cmd}', sub='unittest') if isinstance(runner, Command) else None
205
+
206
+ # Run runner
207
+ results = runner.run()
208
+ results_str = "\n".join([repr(r) for r in results])
209
+ debug(f'{runner.name} yielded results\n{results_str}', sub='unittest')
210
+ debug(f'{runner.name} yielded results\n{json.dumps([r.toDict() for r in results], default=str, indent=2)}', sub='unittest.dict', verbose=True) # noqa: E501
211
+
212
+ debug('-' * 10 + f' RUNNER {runner.name} TESTS ' + '-' * 10, sub='unittest')
213
+
214
+ # Add execution types to allowed output types
215
+ expected_output_types.extend(EXECUTION_TYPES + STAT_TYPES)
216
+
217
+ # Check return code
218
+ if isinstance(runner, Command):
219
+ if not runner.ignore_return_code:
220
+ debug(f'{runner.name} should have a 0 return code', sub='unittest')
221
+ self.assertEqual(runner.return_code, 0, f'{runner.name} should have a 0 return code. Runner return code: {runner.return_code}') # noqa: E501
222
+
223
+ # Check results not empty
224
+ if not empty_results_allowed:
225
+ debug(f'{runner.name} should return at least 1 result', sub='unittest')
226
+ self.assertGreater(len(results), 0, f'{runner.name} should return at least 1 result')
227
+
228
+ # Check status
229
+ debug(f'{runner.name} should have the status {expected_status}.', sub='unittest')
230
+ self.assertEqual(runner.status, expected_status, f'{runner.name} should have the status {expected_status}. Errors: {runner.errors}') # noqa: E501
231
+
232
+ # Check results
233
+ failures = []
234
+ debug('-' * 10 + f' RUNNER {runner.name} ITEM TESTS ' + '-' * 10, sub='unittest')
235
+ for item in results:
236
+ item_str = self.get_item_str(item)
237
+ debug('--' * 5, sub='unittest')
238
+ debug(f'{runner.name} item {repr(item)}', sub='unittest')
239
+ debug(f'{runner.name} item [{item.toDict()}]', sub='unittest.item', verbose=True)
240
+
241
+ if expected_output_types:
242
+ debug(f'{runner.name} item should have an output type in {[_._type for _ in expected_output_types]}', sub='unittest') # noqa: E501
243
+ self.assertIn(type(item), expected_output_types, f'{runner.name}: item has an unexpected output type "{type(item)}". Expected types: {expected_output_types}.\n{item_str}') # noqa: E501
244
+
245
+ if expected_output_keys:
246
+ keys = [k for k in list(item.keys()) if not k.startswith('_')]
247
+ debug(f'{runner.name} item should have output keys {keys}', sub='unittest')
248
+ self.assertEqual(
249
+ set(keys).difference(set(expected_output_keys)),
250
+ set(),
251
+ f'{runner.name}: item is missing expected keys {set(expected_output_keys)}.\nItem keys: {keys}.\n{item_str}') # noqa: E501
252
+
253
+ if additional_checks and item.__class__ in additional_checks.get('output_types', {}):
254
+ config = additional_checks['output_types'][item.__class__]
255
+ runner_regex = config.get('runner', '*')
256
+ if not re.match(runner_regex, runner.name):
257
+ continue
258
+ checks = config.get('checks', [])
259
+ for check in checks:
260
+ error = check['error']
261
+ info = check['info']
262
+ func = check['function']
263
+ debug(f'{runner.name} item {info}', sub='unittest')
264
+ try:
265
+ result = func(item)
266
+ if not result:
267
+ failures.append(f'ERROR ({runner.name}): {error}.\n{item_str}')
268
+ except Exception as e:
269
+ failures.append(f'ERROR ({runner.name}): {error}.\n{item_str}\n{traceback_as_string(e)}')
270
+
271
+ # Additional checks failures
272
+ if failures:
273
+ self.fail("\n\n" + "\n\n".join(failures))
274
+
275
+ # Check if runner results in expected results
276
+ if expected_results:
277
+ for result in expected_results:
278
+ debug(f'{runner.name} item should be in expected results {result}.', sub='unittest')
279
+ self.assertIn(result, results, f'{runner.name}: {result} should be in runner results.') # noqa: E501
280
+
281
+ except Exception:
282
+ console.print('[dim red] failed[/]')
283
+ raise
284
+
285
+ console.print('[dim green] ok[/]')
286
+
287
+
288
+ def clear_modules():
289
+ """Clear all secator modules imports.
290
+ See https://stackoverflow.com/questions/7460363/re-import-module-under-test-to-lose-context for context.
291
+ """
292
+ keys_to_delete = []
293
+ for k, _ in sys.modules.items():
294
+ if k.startswith('secator'):
295
+ keys_to_delete.append(k)
296
+ for k in keys_to_delete:
297
+ del sys.modules[k]
@@ -0,0 +1,29 @@
1
+ from secator.loader import get_configs_by_type
2
+ from secator.runners import Workflow
3
+
4
+
5
+ class DynamicWorkflow(Workflow):
6
+ def __init__(self, config):
7
+ self.config = config
8
+
9
+ def __call__(self, targets, **kwargs):
10
+ hooks = kwargs.pop('hooks', {})
11
+ results = kwargs.pop('results', [])
12
+ context = kwargs.pop('context', {})
13
+ super().__init__(
14
+ config=self.config,
15
+ inputs=targets,
16
+ results=results,
17
+ hooks=hooks,
18
+ context=context,
19
+ run_opts=kwargs)
20
+ return self
21
+
22
+
23
+ DYNAMIC_WORKFLOWS = {}
24
+ for workflow in get_configs_by_type('workflow'):
25
+ instance = DynamicWorkflow(workflow)
26
+ DYNAMIC_WORKFLOWS[workflow.name] = instance
27
+
28
+ globals().update(DYNAMIC_WORKFLOWS)
29
+ __all__ = list(DYNAMIC_WORKFLOWS)