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.

Files changed (49) hide show
  1. secator/celery.py +3 -3
  2. secator/cli.py +119 -77
  3. secator/config.py +88 -58
  4. secator/configs/workflows/subdomain_recon.yaml +2 -2
  5. secator/configs/workflows/url_dirsearch.yaml +1 -1
  6. secator/decorators.py +1 -0
  7. secator/definitions.py +1 -1
  8. secator/installer.py +284 -60
  9. secator/output_types/error.py +3 -3
  10. secator/output_types/exploit.py +11 -7
  11. secator/output_types/info.py +2 -2
  12. secator/output_types/ip.py +1 -1
  13. secator/output_types/port.py +3 -3
  14. secator/output_types/record.py +4 -4
  15. secator/output_types/stat.py +2 -2
  16. secator/output_types/subdomain.py +1 -1
  17. secator/output_types/tag.py +3 -3
  18. secator/output_types/target.py +2 -2
  19. secator/output_types/url.py +11 -11
  20. secator/output_types/user_account.py +6 -6
  21. secator/output_types/vulnerability.py +5 -4
  22. secator/output_types/warning.py +2 -2
  23. secator/report.py +1 -0
  24. secator/runners/_base.py +17 -13
  25. secator/runners/command.py +44 -7
  26. secator/tasks/_categories.py +145 -43
  27. secator/tasks/bbot.py +2 -0
  28. secator/tasks/bup.py +1 -0
  29. secator/tasks/dirsearch.py +2 -2
  30. secator/tasks/dnsxbrute.py +2 -1
  31. secator/tasks/feroxbuster.py +2 -3
  32. secator/tasks/fping.py +1 -1
  33. secator/tasks/grype.py +2 -4
  34. secator/tasks/h8mail.py +1 -1
  35. secator/tasks/katana.py +1 -1
  36. secator/tasks/maigret.py +1 -1
  37. secator/tasks/msfconsole.py +17 -3
  38. secator/tasks/naabu.py +15 -1
  39. secator/tasks/nmap.py +32 -20
  40. secator/tasks/nuclei.py +4 -1
  41. secator/tasks/searchsploit.py +9 -2
  42. secator/tasks/wpscan.py +12 -1
  43. secator/template.py +1 -1
  44. secator/utils.py +151 -62
  45. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/METADATA +50 -45
  46. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/RECORD +49 -49
  47. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/WHEEL +1 -1
  48. {secator-0.7.0.dist-info → secator-0.8.1.dist-info}/entry_points.txt +0 -0
  49. {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 requests
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
- from secator.config import CONFIG
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
- """Install a tool.
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
- Args:
24
- cls: ToolInstaller class.
25
- tool_cls: Tool class (derived from secator.runners.Command).
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
- if not tool_cls.install_github_handle and not tool_cls.install_cmd:
34
- console.print(
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
- if tool_cls.install_github_handle:
39
- success = GithubInstaller.install(tool_cls.install_github_handle)
111
+ @classmethod
112
+ def install(cls, config):
113
+ """Install packages using the correct package manager based on the distribution.
40
114
 
41
- if tool_cls.install_cmd and not success:
42
- success = SourceInstaller.install(tool_cls.install_cmd)
115
+ Args:
116
+ config (dict): A dict of package managers as keys and a list of package names as values.
43
117
 
44
- if success:
45
- console.print(
46
- f'[bold green]:tada: {tool_cls.__name__} installed successfully[/] !')
47
- else:
48
- console.print(
49
- f'[bold red]:exclamation_mark: Failed to install {tool_cls.__name__}.[/]')
50
- return success
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, install_cmd):
158
+ def install(cls, config):
58
159
  """Install from source.
59
160
 
60
161
  Args:
61
162
  cls: ToolInstaller class.
62
- install_cmd (str): Install command.
163
+ config (dict): A dict of distros as keys and a command as value.
63
164
 
64
165
  Returns:
65
- bool: True if install is successful, False otherwise.
166
+ Status: install status.
66
167
  """
67
- ret = Command.execute(install_cmd, cls_attributes={'shell': True})
68
- return ret.return_code == 0
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
- bool: True if install is successful, False otherwise.
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 False
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('[dim red]Could not find a GitHub release matching distribution.[/]')
94
- return False
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
- console.print(f'Downloading and unpacking to {destination}...')
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.raise_for_status()
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
- temp_dir = os.path.join("/tmp", repo_name)
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('[bold red]Binary matching the repository name was not found in the archive.[/]')
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, github_handle=None, version=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
- github_handle (str): Github handle.
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
- # Get latest version
284
- latest_version = None
285
- if not CONFIG.offline_mode:
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 _version.parse(version) < _version.parse(latest_version):
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':
@@ -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 = f'[bold red] {self.message}[/]'
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)
@@ -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 = f'[bold red]⍼[/] \[[bold red]{self.name}'
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 += f' \[[cyan]{tags_str}[/]]'
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 += f' \[[yellow]{str(data)}[/]]'
54
+ s += rf' \[[yellow]{_s(str(data))}[/]]'
55
+ if self.confidence == 'low':
56
+ s = f'[dim]{s}[/]'
53
57
  return rich_to_ansi(s)
@@ -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 = f" ℹ️ {self.message}"
23
+ s = rf"\[[blue]INF[/]] {_s(self.message)}"
24
24
  return rich_to_ansi(s)
@@ -36,5 +36,5 @@ class Ip(OutputType):
36
36
  def __repr__(self) -> str:
37
37
  s = f'💻 [bold white]{self.ip}[/]'
38
38
  if self.host:
39
- s += f' \[[bold magenta]{self.host}[/]]'
39
+ s += rf' \[[bold magenta]{self.host}[/]]'
40
40
  return rich_to_ansi(s)
@@ -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 += f' \[[yellow3]{self.protocol}[/]]'
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 += f' \[[bold purple]{self.service_name}{conf}[/]]'
49
+ s += rf' \[[bold purple]{self.service_name}{conf}[/]]'
50
50
  if self.host:
51
- s += f' \[[cyan]{self.host}[/]]'
51
+ s += rf' \[[cyan]{self.host}[/]]'
52
52
  return rich_to_ansi(s)
@@ -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 = f'🎤 [bold white]{self.name}[/] \[[green]{self.type}[/]]'
31
+ s = rf'🎤 [bold white]{self.name}[/] \[[green]{self.type}[/]]'
32
32
  if self.host:
33
- s += f' \[[magenta]{self.host}[/]]'
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)
@@ -26,8 +26,8 @@ class Stat(OutputType):
26
26
  _sort_by = ('name', 'pid')
27
27
 
28
28
  def __repr__(self) -> str:
29
- s = f'[dim yellow3]📊 {self.name} \[pid={self.pid}] \[cpu={self.cpu:.2f}%] \[memory={self.memory:.2f}%]'
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 += f' \[connections={self.net_conns}]'
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)
@@ -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)
@@ -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)