secator 0.6.0__py3-none-any.whl → 0.8.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.
Potentially problematic release.
This version of secator might be problematic. Click here for more details.
- secator/celery.py +160 -185
- secator/celery_utils.py +268 -0
- secator/cli.py +427 -176
- secator/config.py +114 -68
- secator/configs/workflows/host_recon.yaml +5 -3
- secator/configs/workflows/port_scan.yaml +7 -3
- secator/configs/workflows/subdomain_recon.yaml +2 -2
- secator/configs/workflows/url_bypass.yaml +10 -0
- secator/configs/workflows/url_dirsearch.yaml +1 -1
- secator/configs/workflows/url_vuln.yaml +1 -1
- secator/decorators.py +170 -92
- secator/definitions.py +11 -4
- secator/exporters/__init__.py +7 -5
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +27 -19
- secator/exporters/gdrive.py +16 -11
- secator/exporters/json.py +3 -1
- secator/exporters/table.py +30 -2
- secator/exporters/txt.py +20 -16
- secator/hooks/gcs.py +53 -0
- secator/hooks/mongodb.py +53 -27
- secator/installer.py +277 -60
- secator/output_types/__init__.py +29 -11
- secator/output_types/_base.py +11 -1
- secator/output_types/error.py +36 -0
- secator/output_types/exploit.py +12 -8
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +8 -1
- secator/output_types/port.py +9 -2
- secator/output_types/progress.py +5 -0
- secator/output_types/record.py +5 -3
- secator/output_types/stat.py +33 -0
- secator/output_types/subdomain.py +1 -1
- secator/output_types/tag.py +8 -6
- secator/output_types/target.py +2 -2
- secator/output_types/url.py +14 -11
- secator/output_types/user_account.py +6 -6
- secator/output_types/vulnerability.py +8 -6
- secator/output_types/warning.py +24 -0
- secator/report.py +56 -23
- secator/rich.py +44 -39
- secator/runners/_base.py +629 -638
- secator/runners/_helpers.py +5 -91
- secator/runners/celery.py +18 -0
- secator/runners/command.py +404 -214
- secator/runners/scan.py +8 -24
- secator/runners/task.py +21 -55
- secator/runners/workflow.py +41 -40
- secator/scans/__init__.py +28 -0
- secator/serializers/dataclass.py +6 -0
- secator/serializers/json.py +10 -5
- secator/serializers/regex.py +12 -4
- secator/tasks/_categories.py +147 -42
- secator/tasks/bbot.py +295 -0
- secator/tasks/bup.py +99 -0
- secator/tasks/cariddi.py +38 -49
- secator/tasks/dalfox.py +3 -0
- secator/tasks/dirsearch.py +14 -25
- secator/tasks/dnsx.py +49 -30
- secator/tasks/dnsxbrute.py +4 -1
- secator/tasks/feroxbuster.py +10 -20
- secator/tasks/ffuf.py +3 -2
- secator/tasks/fping.py +4 -4
- secator/tasks/gau.py +5 -0
- secator/tasks/gf.py +2 -2
- secator/tasks/gospider.py +4 -0
- secator/tasks/grype.py +11 -13
- secator/tasks/h8mail.py +32 -42
- secator/tasks/httpx.py +58 -21
- secator/tasks/katana.py +19 -23
- secator/tasks/maigret.py +27 -25
- secator/tasks/mapcidr.py +2 -3
- secator/tasks/msfconsole.py +22 -19
- secator/tasks/naabu.py +18 -2
- secator/tasks/nmap.py +82 -55
- secator/tasks/nuclei.py +13 -3
- secator/tasks/searchsploit.py +26 -11
- secator/tasks/subfinder.py +5 -1
- secator/tasks/wpscan.py +91 -94
- secator/template.py +61 -45
- secator/thread.py +24 -0
- secator/utils.py +417 -78
- secator/utils_test.py +48 -23
- secator/workflows/__init__.py +28 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/METADATA +59 -48
- secator-0.8.0.dist-info/RECORD +115 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
- secator-0.6.0.dist-info/RECORD +0 -101
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
- {secator-0.6.0.dist-info → secator-0.8.0.dist-info}/licenses/LICENSE +0 -0
secator/installer.py
CHANGED
|
@@ -1,71 +1,182 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
1
|
+
import distro
|
|
2
|
+
import getpass
|
|
3
3
|
import os
|
|
4
4
|
import platform
|
|
5
|
+
import re
|
|
5
6
|
import shutil
|
|
6
7
|
import tarfile
|
|
7
8
|
import zipfile
|
|
8
9
|
import io
|
|
9
10
|
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import requests
|
|
17
|
+
|
|
10
18
|
from rich.table import Table
|
|
11
19
|
|
|
20
|
+
from secator.config import CONFIG
|
|
21
|
+
from secator.definitions import OPT_NOT_SUPPORTED
|
|
22
|
+
from secator.output_types import Info, Warning, Error
|
|
12
23
|
from secator.rich import console
|
|
13
24
|
from secator.runners import Command
|
|
14
|
-
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class InstallerStatus(Enum):
|
|
28
|
+
SUCCESS = 'SUCCESS'
|
|
29
|
+
INSTALL_FAILED = 'INSTALL_FAILED'
|
|
30
|
+
INSTALL_NOT_SUPPORTED = 'INSTALL_NOT_SUPPORTED'
|
|
31
|
+
INSTALL_SKIPPED_OK = 'INSTALL_SKIPPED_OK'
|
|
32
|
+
GITHUB_LATEST_RELEASE_NOT_FOUND = 'GITHUB_LATEST_RELEASE_NOT_FOUND'
|
|
33
|
+
GITHUB_RELEASE_NOT_FOUND = 'RELEASE_NOT_FOUND'
|
|
34
|
+
GITHUB_RELEASE_FAILED_DOWNLOAD = 'GITHUB_RELEASE_FAILED_DOWNLOAD'
|
|
35
|
+
GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE = 'GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE'
|
|
36
|
+
UNKNOWN_DISTRIBUTION = 'UNKNOWN_DISTRIBUTION'
|
|
37
|
+
UNKNOWN = 'UNKNOWN'
|
|
38
|
+
|
|
39
|
+
def is_ok(self):
|
|
40
|
+
return self.value in ['SUCCESS', 'INSTALL_SKIPPED_OK']
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class Distribution:
|
|
45
|
+
pm_install_command: str
|
|
46
|
+
pm_name: str
|
|
47
|
+
name: str
|
|
15
48
|
|
|
16
49
|
|
|
17
50
|
class ToolInstaller:
|
|
51
|
+
status = InstallerStatus
|
|
18
52
|
|
|
19
53
|
@classmethod
|
|
20
54
|
def install(cls, tool_cls):
|
|
21
|
-
|
|
55
|
+
name = tool_cls.__name__
|
|
56
|
+
console.print(Info(message=f'Installing {name}'))
|
|
57
|
+
status = InstallerStatus.UNKNOWN
|
|
58
|
+
|
|
59
|
+
# Fail if not supported
|
|
60
|
+
if not any(_ for _ in [
|
|
61
|
+
tool_cls.install_pre,
|
|
62
|
+
tool_cls.install_github_handle,
|
|
63
|
+
tool_cls.install_cmd,
|
|
64
|
+
tool_cls.install_post]):
|
|
65
|
+
return InstallerStatus.INSTALL_NOT_SUPPORTED
|
|
66
|
+
|
|
67
|
+
# Install pre-required packages
|
|
68
|
+
if tool_cls.install_pre:
|
|
69
|
+
status = PackageInstaller.install(tool_cls.install_pre)
|
|
70
|
+
if not status.is_ok():
|
|
71
|
+
cls.print_status(status, name)
|
|
72
|
+
return status
|
|
73
|
+
|
|
74
|
+
# Install binaries from GH
|
|
75
|
+
gh_status = InstallerStatus.UNKNOWN
|
|
76
|
+
if tool_cls.install_github_handle and not CONFIG.security.force_source_install:
|
|
77
|
+
gh_status = GithubInstaller.install(tool_cls.install_github_handle)
|
|
78
|
+
status = gh_status
|
|
79
|
+
|
|
80
|
+
# Install from source
|
|
81
|
+
if tool_cls.install_cmd and not gh_status.is_ok():
|
|
82
|
+
status = SourceInstaller.install(tool_cls.install_cmd)
|
|
83
|
+
if not status.is_ok():
|
|
84
|
+
cls.print_status(status, name)
|
|
85
|
+
return status
|
|
86
|
+
|
|
87
|
+
# Install post commands
|
|
88
|
+
if tool_cls.install_post:
|
|
89
|
+
post_status = SourceInstaller.install(tool_cls.install_post)
|
|
90
|
+
if not post_status.is_ok():
|
|
91
|
+
cls.print_status(post_status, name)
|
|
92
|
+
return post_status
|
|
93
|
+
|
|
94
|
+
cls.print_status(status, name)
|
|
95
|
+
return status
|
|
22
96
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
97
|
+
@classmethod
|
|
98
|
+
def print_status(cls, status, name):
|
|
99
|
+
if status.is_ok():
|
|
100
|
+
console.print(Info(message=f'{name} installed successfully!'))
|
|
101
|
+
elif status == InstallerStatus.INSTALL_NOT_SUPPORTED:
|
|
102
|
+
console.print(Error(message=f'{name} install is not supported yet. Please install manually'))
|
|
103
|
+
else:
|
|
104
|
+
console.print(Error(message=f'Failed to install {name}: {status}'))
|
|
26
105
|
|
|
27
|
-
Returns:
|
|
28
|
-
bool: True if install is successful, False otherwise.
|
|
29
|
-
"""
|
|
30
|
-
console.print(f'[bold gold3]:wrench: Installing {tool_cls.__name__}')
|
|
31
|
-
success = False
|
|
32
106
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
f'[bold red]{tool_cls.__name__} install is not supported yet. Please install it manually.[/]')
|
|
36
|
-
return False
|
|
107
|
+
class PackageInstaller:
|
|
108
|
+
"""Install system packages."""
|
|
37
109
|
|
|
38
|
-
|
|
39
|
-
|
|
110
|
+
@classmethod
|
|
111
|
+
def install(cls, config):
|
|
112
|
+
"""Install packages using the correct package manager based on the distribution.
|
|
40
113
|
|
|
41
|
-
|
|
42
|
-
|
|
114
|
+
Args:
|
|
115
|
+
config (dict): A dict of package managers as keys and a list of package names as values.
|
|
43
116
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
117
|
+
Returns:
|
|
118
|
+
InstallerStatus: installer status.
|
|
119
|
+
"""
|
|
120
|
+
# Init status
|
|
121
|
+
distribution = get_distro_config()
|
|
122
|
+
if distribution.pm_install_command == 'unknown':
|
|
123
|
+
return InstallerStatus.UNKNOWN_DISTRIBUTION
|
|
124
|
+
|
|
125
|
+
console.print(
|
|
126
|
+
Info(message=f'Detected distribution "{distribution.name}", using package manager "{distribution.pm_name}"'))
|
|
127
|
+
|
|
128
|
+
# Construct package list
|
|
129
|
+
pkg_list = []
|
|
130
|
+
for managers, packages in config.items():
|
|
131
|
+
if distribution.pm_name in managers.split("|") or managers == '*':
|
|
132
|
+
pkg_list.extend(packages)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# Installer cmd
|
|
136
|
+
cmd = distribution.pm_install_command
|
|
137
|
+
if getpass.getuser() != 'root':
|
|
138
|
+
cmd = f'sudo {cmd}'
|
|
139
|
+
|
|
140
|
+
if pkg_list:
|
|
141
|
+
for pkg in pkg_list:
|
|
142
|
+
if ':' in pkg:
|
|
143
|
+
pdistro, pkg = pkg.split(':')
|
|
144
|
+
if pdistro != distribution.name:
|
|
145
|
+
continue
|
|
146
|
+
console.print(Info(message=f'Installing package {pkg}'))
|
|
147
|
+
status = SourceInstaller.install(f'{cmd} {pkg}')
|
|
148
|
+
if not status.is_ok():
|
|
149
|
+
return status
|
|
150
|
+
return InstallerStatus.SUCCESS
|
|
51
151
|
|
|
52
152
|
|
|
53
153
|
class SourceInstaller:
|
|
54
154
|
"""Install a tool from source."""
|
|
55
155
|
|
|
56
156
|
@classmethod
|
|
57
|
-
def install(cls,
|
|
157
|
+
def install(cls, config):
|
|
58
158
|
"""Install from source.
|
|
59
159
|
|
|
60
160
|
Args:
|
|
61
161
|
cls: ToolInstaller class.
|
|
62
|
-
|
|
162
|
+
config (dict): A dict of distros as keys and a command as value.
|
|
63
163
|
|
|
64
164
|
Returns:
|
|
65
|
-
|
|
165
|
+
Status: install status.
|
|
66
166
|
"""
|
|
67
|
-
|
|
68
|
-
|
|
167
|
+
install_cmd = None
|
|
168
|
+
if isinstance(config, str):
|
|
169
|
+
install_cmd = config
|
|
170
|
+
else:
|
|
171
|
+
distribution = get_distro_config()
|
|
172
|
+
for distros, command in config.items():
|
|
173
|
+
if distribution.name in distros.split("|") or distros == '*':
|
|
174
|
+
install_cmd = command
|
|
175
|
+
break
|
|
176
|
+
if not install_cmd:
|
|
177
|
+
return InstallerStatus.INSTALL_SKIPPED_OK
|
|
178
|
+
ret = Command.execute(install_cmd, cls_attributes={'shell': True}, quiet=False)
|
|
179
|
+
return InstallerStatus.SUCCESS if ret.return_code == 0 else InstallerStatus.INSTALL_FAILED
|
|
69
180
|
|
|
70
181
|
|
|
71
182
|
class GithubInstaller:
|
|
@@ -79,24 +190,23 @@ class GithubInstaller:
|
|
|
79
190
|
github_handle (str): A GitHub handle {user}/{repo}
|
|
80
191
|
|
|
81
192
|
Returns:
|
|
82
|
-
|
|
193
|
+
InstallerStatus: status.
|
|
83
194
|
"""
|
|
84
195
|
_, repo = tuple(github_handle.split('/'))
|
|
85
196
|
latest_release = cls.get_latest_release(github_handle)
|
|
86
197
|
if not latest_release:
|
|
87
|
-
return
|
|
198
|
+
return InstallerStatus.GITHUB_LATEST_RELEASE_NOT_FOUND
|
|
88
199
|
|
|
89
200
|
# Find the right asset to download
|
|
90
201
|
os_identifiers, arch_identifiers = cls._get_platform_identifier()
|
|
91
202
|
download_url = cls._find_matching_asset(latest_release['assets'], os_identifiers, arch_identifiers)
|
|
92
203
|
if not download_url:
|
|
93
|
-
console.print('
|
|
94
|
-
return
|
|
204
|
+
console.print(Error(message='Could not find a GitHub release matching distribution.'))
|
|
205
|
+
return InstallerStatus.GITHUB_RELEASE_NOT_FOUND
|
|
95
206
|
|
|
96
207
|
# Download and unpack asset
|
|
97
|
-
console.print(f'Found release URL: {download_url}')
|
|
98
|
-
cls._download_and_unpack(download_url, CONFIG.dirs.bin, repo)
|
|
99
|
-
return True
|
|
208
|
+
console.print(Info(message=f'Found release URL: {download_url}'))
|
|
209
|
+
return cls._download_and_unpack(download_url, CONFIG.dirs.bin, repo)
|
|
100
210
|
|
|
101
211
|
@classmethod
|
|
102
212
|
def get_latest_release(cls, github_handle):
|
|
@@ -121,7 +231,7 @@ class GithubInstaller:
|
|
|
121
231
|
latest_release = response.json()
|
|
122
232
|
return latest_release
|
|
123
233
|
except requests.RequestException as e:
|
|
124
|
-
console.print(f'Failed to fetch latest release for {github_handle}: {str(e)}')
|
|
234
|
+
console.print(Warning(message=f'Failed to fetch latest release for {github_handle}: {str(e)}'))
|
|
125
235
|
return None
|
|
126
236
|
|
|
127
237
|
@classmethod
|
|
@@ -181,13 +291,25 @@ class GithubInstaller:
|
|
|
181
291
|
|
|
182
292
|
@classmethod
|
|
183
293
|
def _download_and_unpack(cls, url, destination, repo_name):
|
|
184
|
-
"""Download and unpack a release asset.
|
|
185
|
-
|
|
294
|
+
"""Download and unpack a release asset.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
cls (Runner): Task class.
|
|
298
|
+
url (str): GitHub release URL.
|
|
299
|
+
destination (str): Local destination.
|
|
300
|
+
repo_name (str): GitHub repository name.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
InstallerStatus: install status.
|
|
304
|
+
"""
|
|
305
|
+
console.print(Info(message=f'Downloading and unpacking to {destination}...'))
|
|
186
306
|
response = requests.get(url, timeout=5)
|
|
187
|
-
response.
|
|
307
|
+
if not response.status_code == 200:
|
|
308
|
+
return InstallerStatus.GITHUB_RELEASE_FAILED_DOWNLOAD
|
|
188
309
|
|
|
189
310
|
# Create a temporary directory to extract the archive
|
|
190
|
-
|
|
311
|
+
date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
312
|
+
temp_dir = os.path.join("/tmp", f'{repo_name}_{date_str}')
|
|
191
313
|
os.makedirs(temp_dir, exist_ok=True)
|
|
192
314
|
|
|
193
315
|
if url.endswith('.zip'):
|
|
@@ -202,8 +324,10 @@ class GithubInstaller:
|
|
|
202
324
|
if binary_path:
|
|
203
325
|
os.chmod(binary_path, 0o755) # Make it executable
|
|
204
326
|
shutil.move(binary_path, os.path.join(destination, repo_name)) # Move the binary
|
|
327
|
+
return InstallerStatus.SUCCESS
|
|
205
328
|
else:
|
|
206
|
-
console.print('
|
|
329
|
+
console.print(Error(message='Binary matching the repository name was not found in the archive.'))
|
|
330
|
+
return InstallerStatus.GITHUB_BINARY_NOT_FOUND_IN_ARCHIVE
|
|
207
331
|
|
|
208
332
|
@classmethod
|
|
209
333
|
def _find_binary_in_directory(cls, directory, binary_name):
|
|
@@ -235,31 +359,46 @@ def get_version(version_cmd):
|
|
|
235
359
|
version_cmd (str): Command to get the version.
|
|
236
360
|
|
|
237
361
|
Returns:
|
|
238
|
-
str: Version string.
|
|
362
|
+
tuple[str]: Version string, return code.
|
|
239
363
|
"""
|
|
240
364
|
from secator.runners import Command
|
|
241
365
|
import re
|
|
242
366
|
regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
|
|
243
367
|
ret = Command.execute(version_cmd, quiet=True, print_errors=False)
|
|
368
|
+
return_code = ret.return_code
|
|
369
|
+
if not return_code == 0:
|
|
370
|
+
return '', ret.return_code
|
|
244
371
|
match = re.findall(regex, ret.output)
|
|
245
372
|
if not match:
|
|
246
|
-
return ''
|
|
247
|
-
return match[0]
|
|
373
|
+
return '', return_code
|
|
374
|
+
return match[0], return_code
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def parse_version(ver):
|
|
378
|
+
from packaging import version as _version
|
|
379
|
+
try:
|
|
380
|
+
return _version.parse(ver)
|
|
381
|
+
except _version.InvalidVersion:
|
|
382
|
+
version_regex = re.compile(r'(\d+\.\d+(?:\.\d+)?)')
|
|
383
|
+
match = version_regex.search(ver)
|
|
384
|
+
if match:
|
|
385
|
+
return _version.parse(match.group(1))
|
|
386
|
+
return None
|
|
248
387
|
|
|
249
388
|
|
|
250
|
-
def get_version_info(name, version_flag=None,
|
|
389
|
+
def get_version_info(name, version_flag=None, install_github_handle=None, install_cmd=None, version=None):
|
|
251
390
|
"""Get version info for a command.
|
|
252
391
|
|
|
253
392
|
Args:
|
|
254
393
|
name (str): Command name.
|
|
255
394
|
version_flag (str): Version flag.
|
|
256
|
-
|
|
395
|
+
install_github_handle (str): Github handle.
|
|
396
|
+
install_cmd (str): Install command.
|
|
257
397
|
version (str): Existing version.
|
|
258
398
|
|
|
259
399
|
Return:
|
|
260
400
|
dict: Version info.
|
|
261
401
|
"""
|
|
262
|
-
from packaging import version as _version
|
|
263
402
|
from secator.installer import GithubInstaller
|
|
264
403
|
info = {
|
|
265
404
|
'name': name,
|
|
@@ -274,22 +413,50 @@ def get_version_info(name, version_flag=None, github_handle=None, version=None):
|
|
|
274
413
|
location = which(name).output
|
|
275
414
|
info['location'] = location
|
|
276
415
|
|
|
416
|
+
# Get latest version
|
|
417
|
+
latest_version = None
|
|
418
|
+
if not CONFIG.offline_mode:
|
|
419
|
+
if install_github_handle:
|
|
420
|
+
latest_version = GithubInstaller.get_latest_version(install_github_handle)
|
|
421
|
+
info['latest_version'] = latest_version
|
|
422
|
+
elif install_cmd and install_cmd.startswith('pip'):
|
|
423
|
+
req = requests.get(f'https://pypi.python.org/pypi/{name}/json')
|
|
424
|
+
version = parse_version('0')
|
|
425
|
+
if req.status_code == requests.codes.ok:
|
|
426
|
+
j = json.loads(req.text.encode(req.encoding))
|
|
427
|
+
releases = j.get('releases', [])
|
|
428
|
+
for release in releases:
|
|
429
|
+
ver = parse_version(release)
|
|
430
|
+
if ver and not ver.is_prerelease:
|
|
431
|
+
version = max(version, ver)
|
|
432
|
+
latest_version = str(version)
|
|
433
|
+
info['latest_version'] = latest_version
|
|
434
|
+
elif install_cmd and install_cmd.startswith('sudo apt install'):
|
|
435
|
+
ret = Command.execute(f'apt-cache madison {name}', quiet=True)
|
|
436
|
+
if ret.return_code == 0:
|
|
437
|
+
output = ret.output.split(' | ')
|
|
438
|
+
if len(output) > 1:
|
|
439
|
+
ver = parse_version(output[1].strip())
|
|
440
|
+
if ver:
|
|
441
|
+
latest_version = str(ver)
|
|
442
|
+
info['latest_version'] = latest_version
|
|
443
|
+
|
|
277
444
|
# Get current version
|
|
445
|
+
version_ret = 1
|
|
446
|
+
version_flag = None if version_flag == OPT_NOT_SUPPORTED else version_flag
|
|
278
447
|
if version_flag:
|
|
279
448
|
version_cmd = f'{name} {version_flag}'
|
|
280
|
-
version = get_version(version_cmd)
|
|
449
|
+
version, version_ret = get_version(version_cmd)
|
|
281
450
|
info['version'] = version
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
latest_version = GithubInstaller.get_latest_version(github_handle)
|
|
287
|
-
info['latest_version'] = latest_version
|
|
451
|
+
if version_ret != 0: # version command error
|
|
452
|
+
info['installed'] = False
|
|
453
|
+
info['status'] = 'missing'
|
|
454
|
+
return info
|
|
288
455
|
|
|
289
456
|
if location:
|
|
290
457
|
info['installed'] = True
|
|
291
458
|
if version and latest_version:
|
|
292
|
-
if
|
|
459
|
+
if parse_version(version) < parse_version(latest_version):
|
|
293
460
|
info['status'] = 'outdated'
|
|
294
461
|
else:
|
|
295
462
|
info['status'] = 'latest'
|
|
@@ -298,18 +465,66 @@ def get_version_info(name, version_flag=None, github_handle=None, version=None):
|
|
|
298
465
|
elif not latest_version:
|
|
299
466
|
info['status'] = 'latest unknown'
|
|
300
467
|
if CONFIG.offline_mode:
|
|
301
|
-
info['status'] += ' [dim orange1]\[offline][/]'
|
|
468
|
+
info['status'] += r' [dim orange1]\[offline][/]'
|
|
302
469
|
else:
|
|
303
470
|
info['status'] = 'missing'
|
|
304
471
|
|
|
305
472
|
return info
|
|
306
473
|
|
|
307
474
|
|
|
475
|
+
def get_distro_config():
|
|
476
|
+
"""Detects the system's package manager based on the OS distribution and return the default installation command."""
|
|
477
|
+
|
|
478
|
+
# If explicitely set by the user, use that one
|
|
479
|
+
package_manager_variable = os.environ.get('SECATOR_PACKAGE_MANAGER')
|
|
480
|
+
if package_manager_variable:
|
|
481
|
+
return package_manager_variable
|
|
482
|
+
cmd = "unknown"
|
|
483
|
+
system = platform.system()
|
|
484
|
+
distrib = system
|
|
485
|
+
|
|
486
|
+
if system == "Linux":
|
|
487
|
+
distrib = distro.id()
|
|
488
|
+
|
|
489
|
+
if distrib in ["ubuntu", "debian", "linuxmint", "popos", "kali"]:
|
|
490
|
+
cmd = "apt install -y"
|
|
491
|
+
elif distrib in ["arch", "manjaro", "endeavouros"]:
|
|
492
|
+
cmd = "pacman -S --noconfirm"
|
|
493
|
+
elif distrib in ["alpine"]:
|
|
494
|
+
cmd = "apk add"
|
|
495
|
+
elif distrib in ["fedora"]:
|
|
496
|
+
cmd = "dnf install -y"
|
|
497
|
+
elif distrib in ["centos", "rhel", "rocky", "alma"]:
|
|
498
|
+
cmd = "yum -y"
|
|
499
|
+
elif distrib in ["opensuse", "sles"]:
|
|
500
|
+
cmd = "zypper -n"
|
|
501
|
+
|
|
502
|
+
elif system == "Darwin": # macOS
|
|
503
|
+
cmd = "brew install"
|
|
504
|
+
|
|
505
|
+
elif system == "Windows":
|
|
506
|
+
if shutil.which("winget"):
|
|
507
|
+
cmd = "winget install --disable-interactivity"
|
|
508
|
+
elif shutil.which("choco"):
|
|
509
|
+
cmd = "choco install"
|
|
510
|
+
else:
|
|
511
|
+
cmd = "scoop" # Alternative package manager for Windows
|
|
512
|
+
|
|
513
|
+
manager = cmd.split(' ')[0]
|
|
514
|
+
config = Distribution(
|
|
515
|
+
pm_install_command=cmd,
|
|
516
|
+
pm_name=manager,
|
|
517
|
+
name=distrib
|
|
518
|
+
)
|
|
519
|
+
return config
|
|
520
|
+
|
|
521
|
+
|
|
308
522
|
def fmt_health_table_row(version_info, category=None):
|
|
309
523
|
name = version_info['name']
|
|
310
524
|
version = version_info['version']
|
|
311
525
|
status = version_info['status']
|
|
312
526
|
installed = version_info['installed']
|
|
527
|
+
latest_version = version_info['latest_version']
|
|
313
528
|
name_str = f'[magenta]{name:<13}[/]'
|
|
314
529
|
|
|
315
530
|
# Format version row
|
|
@@ -319,6 +534,8 @@ def fmt_health_table_row(version_info, category=None):
|
|
|
319
534
|
_version += ' [bold green](latest)[/]'
|
|
320
535
|
elif status == 'outdated':
|
|
321
536
|
_version += ' [bold red](outdated)[/]'
|
|
537
|
+
if latest_version:
|
|
538
|
+
_version += f' [dim](<{latest_version})'
|
|
322
539
|
elif status == 'missing':
|
|
323
540
|
_version = '[bold red]missing[/]'
|
|
324
541
|
elif status == 'ok':
|
secator/output_types/__init__.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
__all__ = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
'Error',
|
|
3
|
+
'OutputType',
|
|
4
|
+
'Info',
|
|
5
|
+
'Ip',
|
|
6
|
+
'Port',
|
|
7
|
+
'Progress',
|
|
8
|
+
'Record',
|
|
9
|
+
'Stat',
|
|
10
|
+
'Subdomain',
|
|
11
|
+
'Url',
|
|
12
|
+
'UserAccount',
|
|
13
|
+
'Vulnerability',
|
|
14
|
+
'Warning',
|
|
10
15
|
]
|
|
11
|
-
from secator.output_types._base import OutputType
|
|
12
|
-
from secator.output_types.progress import Progress
|
|
16
|
+
from secator.output_types._base import OutputType
|
|
17
|
+
from secator.output_types.progress import Progress
|
|
13
18
|
from secator.output_types.ip import Ip
|
|
14
19
|
from secator.output_types.exploit import Exploit
|
|
15
20
|
from secator.output_types.port import Port
|
|
@@ -20,5 +25,18 @@ from secator.output_types.url import Url
|
|
|
20
25
|
from secator.output_types.user_account import UserAccount
|
|
21
26
|
from secator.output_types.vulnerability import Vulnerability
|
|
22
27
|
from secator.output_types.record import Record
|
|
28
|
+
from secator.output_types.info import Info
|
|
29
|
+
from secator.output_types.warning import Warning
|
|
30
|
+
from secator.output_types.error import Error
|
|
31
|
+
from secator.output_types.stat import Stat
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
EXECUTION_TYPES = [
|
|
34
|
+
Target, Progress, Info, Warning, Error
|
|
35
|
+
]
|
|
36
|
+
STAT_TYPES = [
|
|
37
|
+
Stat
|
|
38
|
+
]
|
|
39
|
+
FINDING_TYPES = [
|
|
40
|
+
Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability
|
|
41
|
+
]
|
|
42
|
+
OUTPUT_TYPES = FINDING_TYPES + EXECUTION_TYPES + STAT_TYPES
|
secator/output_types/_base.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
from dataclasses import _MISSING_TYPE, dataclass, fields
|
|
4
|
+
from secator.definitions import DEBUG
|
|
5
|
+
from secator.rich import console
|
|
4
6
|
|
|
5
7
|
logger = logging.getLogger(__name__)
|
|
6
8
|
|
|
@@ -66,7 +68,15 @@ class OutputType:
|
|
|
66
68
|
if key in output_map:
|
|
67
69
|
mapped_key = output_map[key]
|
|
68
70
|
if callable(mapped_key):
|
|
69
|
-
|
|
71
|
+
try:
|
|
72
|
+
mapped_val = mapped_key(item)
|
|
73
|
+
except Exception as e:
|
|
74
|
+
mapped_val = None
|
|
75
|
+
if DEBUG > 1:
|
|
76
|
+
console.print_exception(show_locals=True)
|
|
77
|
+
raise TypeError(
|
|
78
|
+
f'Fail to transform value for "{key}" using output_map function. Exception: '
|
|
79
|
+
f'{type(e).__name__}: {str(e)}')
|
|
70
80
|
else:
|
|
71
81
|
mapped_val = item.get(mapped_key)
|
|
72
82
|
new_item[key] = mapped_val
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import time
|
|
3
|
+
from secator.output_types import OutputType
|
|
4
|
+
from secator.utils import rich_to_ansi, traceback_as_string, rich_escape as _s
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Error(OutputType):
|
|
9
|
+
message: str
|
|
10
|
+
traceback: str = field(default='', compare=False)
|
|
11
|
+
_source: str = field(default='', repr=True)
|
|
12
|
+
_type: str = field(default='error', repr=True)
|
|
13
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
14
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
15
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
16
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
17
|
+
_related: list = field(default_factory=list, compare=False)
|
|
18
|
+
|
|
19
|
+
_table_fields = ['message', 'traceback']
|
|
20
|
+
_sort_by = ('_timestamp',)
|
|
21
|
+
|
|
22
|
+
def from_exception(e, **kwargs):
|
|
23
|
+
message = type(e).__name__
|
|
24
|
+
if str(e):
|
|
25
|
+
message += f': {str(e)}'
|
|
26
|
+
return Error(message=message, traceback=traceback_as_string(e), **kwargs)
|
|
27
|
+
|
|
28
|
+
def __str__(self):
|
|
29
|
+
return self.message
|
|
30
|
+
|
|
31
|
+
def __repr__(self):
|
|
32
|
+
s = rf"\[[bold red]ERR[/]] {_s(self.message)}"
|
|
33
|
+
if self.traceback:
|
|
34
|
+
traceback_pretty = ' ' + self.traceback.replace('\n', '\n ')
|
|
35
|
+
s += f'\n[dim]{_s(traceback_pretty)}[/]'
|
|
36
|
+
return rich_to_ansi(s)
|
secator/output_types/exploit.py
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import time
|
|
2
2
|
from dataclasses import dataclass, field
|
|
3
3
|
from secator.output_types import OutputType
|
|
4
|
-
from secator.utils import rich_to_ansi
|
|
4
|
+
from secator.utils import rich_to_ansi, rich_escape as _s
|
|
5
5
|
from secator.definitions import MATCHED_AT, NAME, ID, EXTRA_DATA, REFERENCE
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
9
|
class Exploit(OutputType):
|
|
10
10
|
name: str
|
|
11
|
-
id: str
|
|
12
11
|
provider: str
|
|
12
|
+
id: str
|
|
13
13
|
matched_at: str = ''
|
|
14
14
|
ip: str = ''
|
|
15
|
+
confidence: str = 'low'
|
|
16
|
+
cvss_score: float = 0
|
|
15
17
|
reference: str = ''
|
|
16
18
|
cves: list = field(default_factory=list, compare=False)
|
|
17
19
|
tags: list = field(default_factory=list, compare=False)
|
|
18
20
|
extra_data: dict = field(default_factory=dict, compare=False)
|
|
19
21
|
_source: str = field(default='', repr=True)
|
|
20
|
-
_type: str = field(default='
|
|
22
|
+
_type: str = field(default='exploit', repr=True)
|
|
21
23
|
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
22
24
|
_uuid: str = field(default='', repr=True, compare=False)
|
|
23
25
|
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
@@ -38,16 +40,18 @@ class Exploit(OutputType):
|
|
|
38
40
|
return self.name
|
|
39
41
|
|
|
40
42
|
def __repr__(self):
|
|
41
|
-
s =
|
|
43
|
+
s = rf'[bold red]⍼[/] \[[bold red]{self.name}'
|
|
42
44
|
if self.reference:
|
|
43
|
-
s += f' [link={self.reference}]🡕[/link]'
|
|
45
|
+
s += f' [link={_s(self.reference)}]🡕[/link]'
|
|
44
46
|
s += '[/]]'
|
|
45
47
|
if self.matched_at:
|
|
46
|
-
s += f' {self.matched_at}'
|
|
48
|
+
s += f' {_s(self.matched_at)}'
|
|
47
49
|
if self.tags:
|
|
48
50
|
tags_str = ', '.join(self.tags)
|
|
49
|
-
s +=
|
|
51
|
+
s += rf' \[[cyan]{tags_str}[/]]'
|
|
50
52
|
if self.extra_data:
|
|
51
53
|
data = ', '.join([f'{k}:{v}' for k, v in self.extra_data.items()])
|
|
52
|
-
s +=
|
|
54
|
+
s += rf' \[[yellow]{_s(str(data))}[/]]'
|
|
55
|
+
if self.confidence == 'low':
|
|
56
|
+
s = f'[dim]{s}[/]'
|
|
53
57
|
return rich_to_ansi(s)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import time
|
|
3
|
+
from secator.output_types import OutputType
|
|
4
|
+
from secator.utils import rich_to_ansi, rich_escape as _s
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Info(OutputType):
|
|
9
|
+
message: str
|
|
10
|
+
task_id: str = field(default='', compare=False)
|
|
11
|
+
_source: str = field(default='', repr=True)
|
|
12
|
+
_type: str = field(default='info', repr=True)
|
|
13
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
14
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
15
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
16
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
17
|
+
_related: list = field(default_factory=list, compare=False)
|
|
18
|
+
|
|
19
|
+
_table_fields = ['message', 'task_id']
|
|
20
|
+
_sort_by = ('_timestamp',)
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
s = rf"\[[blue]INF[/]] {_s(self.message)}"
|
|
24
|
+
return rich_to_ansi(s)
|