secator 0.7.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.

Files changed (49) hide show
  1. secator/celery.py +3 -3
  2. secator/cli.py +106 -76
  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 +277 -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 +18 -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.0.dist-info}/METADATA +50 -45
  46. {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/RECORD +49 -49
  47. {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/WHEEL +1 -1
  48. {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/entry_points.txt +0 -0
  49. {secator-0.7.0.dist-info → secator-0.8.0.dist-info}/licenses/LICENSE +0 -0
secator/installer.py CHANGED
@@ -1,71 +1,182 @@
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
+ 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
- """Install a tool.
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
- Args:
24
- cls: ToolInstaller class.
25
- tool_cls: Tool class (derived from secator.runners.Command).
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
- 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
107
+ class PackageInstaller:
108
+ """Install system packages."""
37
109
 
38
- if tool_cls.install_github_handle:
39
- success = GithubInstaller.install(tool_cls.install_github_handle)
110
+ @classmethod
111
+ def install(cls, config):
112
+ """Install packages using the correct package manager based on the distribution.
40
113
 
41
- if tool_cls.install_cmd and not success:
42
- success = SourceInstaller.install(tool_cls.install_cmd)
114
+ Args:
115
+ config (dict): A dict of package managers as keys and a list of package names as values.
43
116
 
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
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, install_cmd):
157
+ def install(cls, config):
58
158
  """Install from source.
59
159
 
60
160
  Args:
61
161
  cls: ToolInstaller class.
62
- install_cmd (str): Install command.
162
+ config (dict): A dict of distros as keys and a command as value.
63
163
 
64
164
  Returns:
65
- bool: True if install is successful, False otherwise.
165
+ Status: install status.
66
166
  """
67
- ret = Command.execute(install_cmd, cls_attributes={'shell': True})
68
- return ret.return_code == 0
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
- bool: True if install is successful, False otherwise.
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 False
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('[dim red]Could not find a GitHub release matching distribution.[/]')
94
- return False
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
- console.print(f'Downloading and unpacking to {destination}...')
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.raise_for_status()
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
- temp_dir = os.path.join("/tmp", repo_name)
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('[bold red]Binary matching the repository name was not found in the archive.[/]')
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, github_handle=None, version=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
- github_handle (str): Github handle.
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
- # 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
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 _version.parse(version) < _version.parse(latest_version):
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':
@@ -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)