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