secator 0.15.1__py3-none-any.whl → 0.16.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 (106) hide show
  1. secator/celery.py +40 -24
  2. secator/celery_signals.py +71 -68
  3. secator/celery_utils.py +43 -27
  4. secator/cli.py +520 -280
  5. secator/cli_helper.py +394 -0
  6. secator/click.py +87 -0
  7. secator/config.py +67 -39
  8. secator/configs/profiles/http_headless.yaml +6 -0
  9. secator/configs/profiles/http_record.yaml +6 -0
  10. secator/configs/profiles/tor.yaml +1 -1
  11. secator/configs/scans/domain.yaml +4 -2
  12. secator/configs/scans/host.yaml +1 -1
  13. secator/configs/scans/network.yaml +1 -4
  14. secator/configs/scans/subdomain.yaml +13 -1
  15. secator/configs/scans/url.yaml +1 -2
  16. secator/configs/workflows/cidr_recon.yaml +6 -4
  17. secator/configs/workflows/code_scan.yaml +1 -1
  18. secator/configs/workflows/host_recon.yaml +29 -3
  19. secator/configs/workflows/subdomain_recon.yaml +67 -16
  20. secator/configs/workflows/url_crawl.yaml +44 -15
  21. secator/configs/workflows/url_dirsearch.yaml +4 -4
  22. secator/configs/workflows/url_fuzz.yaml +25 -17
  23. secator/configs/workflows/url_params_fuzz.yaml +7 -0
  24. secator/configs/workflows/url_vuln.yaml +33 -8
  25. secator/configs/workflows/user_hunt.yaml +2 -1
  26. secator/configs/workflows/wordpress.yaml +5 -3
  27. secator/cve.py +718 -0
  28. secator/decorators.py +0 -454
  29. secator/definitions.py +49 -30
  30. secator/exporters/_base.py +2 -2
  31. secator/exporters/console.py +2 -2
  32. secator/exporters/table.py +4 -3
  33. secator/exporters/txt.py +1 -1
  34. secator/hooks/mongodb.py +2 -4
  35. secator/installer.py +77 -49
  36. secator/loader.py +116 -0
  37. secator/output_types/_base.py +3 -0
  38. secator/output_types/certificate.py +63 -63
  39. secator/output_types/error.py +4 -5
  40. secator/output_types/info.py +2 -2
  41. secator/output_types/ip.py +3 -1
  42. secator/output_types/progress.py +5 -9
  43. secator/output_types/state.py +17 -17
  44. secator/output_types/tag.py +3 -0
  45. secator/output_types/target.py +10 -2
  46. secator/output_types/url.py +19 -7
  47. secator/output_types/vulnerability.py +11 -7
  48. secator/output_types/warning.py +2 -2
  49. secator/report.py +27 -15
  50. secator/rich.py +18 -10
  51. secator/runners/_base.py +446 -233
  52. secator/runners/_helpers.py +133 -24
  53. secator/runners/command.py +182 -102
  54. secator/runners/scan.py +33 -5
  55. secator/runners/task.py +13 -7
  56. secator/runners/workflow.py +105 -72
  57. secator/scans/__init__.py +2 -2
  58. secator/serializers/dataclass.py +20 -20
  59. secator/tasks/__init__.py +4 -4
  60. secator/tasks/_categories.py +39 -27
  61. secator/tasks/arjun.py +9 -5
  62. secator/tasks/bbot.py +53 -21
  63. secator/tasks/bup.py +19 -5
  64. secator/tasks/cariddi.py +24 -3
  65. secator/tasks/dalfox.py +26 -7
  66. secator/tasks/dirsearch.py +10 -4
  67. secator/tasks/dnsx.py +70 -25
  68. secator/tasks/feroxbuster.py +11 -3
  69. secator/tasks/ffuf.py +42 -6
  70. secator/tasks/fping.py +20 -8
  71. secator/tasks/gau.py +3 -1
  72. secator/tasks/gf.py +3 -3
  73. secator/tasks/gitleaks.py +2 -2
  74. secator/tasks/gospider.py +7 -1
  75. secator/tasks/grype.py +5 -4
  76. secator/tasks/h8mail.py +2 -1
  77. secator/tasks/httpx.py +18 -5
  78. secator/tasks/katana.py +35 -15
  79. secator/tasks/maigret.py +4 -4
  80. secator/tasks/mapcidr.py +3 -3
  81. secator/tasks/msfconsole.py +4 -4
  82. secator/tasks/naabu.py +2 -2
  83. secator/tasks/nmap.py +12 -14
  84. secator/tasks/nuclei.py +3 -3
  85. secator/tasks/searchsploit.py +4 -5
  86. secator/tasks/subfinder.py +2 -2
  87. secator/tasks/testssl.py +264 -263
  88. secator/tasks/trivy.py +5 -5
  89. secator/tasks/wafw00f.py +21 -3
  90. secator/tasks/wpprobe.py +90 -83
  91. secator/tasks/wpscan.py +6 -5
  92. secator/template.py +218 -104
  93. secator/thread.py +15 -15
  94. secator/tree.py +196 -0
  95. secator/utils.py +131 -123
  96. secator/utils_test.py +60 -19
  97. secator/workflows/__init__.py +2 -2
  98. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/METADATA +36 -36
  99. secator-0.16.0.dist-info/RECORD +132 -0
  100. secator/configs/profiles/default.yaml +0 -8
  101. secator/configs/workflows/url_nuclei.yaml +0 -11
  102. secator/tasks/dnsxbrute.py +0 -42
  103. secator-0.15.1.dist-info/RECORD +0 -128
  104. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
  105. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
  106. {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/licenses/LICENSE +0 -0
secator/installer.py CHANGED
@@ -24,6 +24,7 @@ from secator.definitions import OPT_NOT_SUPPORTED
24
24
  from secator.output_types import Info, Warning, Error
25
25
  from secator.rich import console
26
26
  from secator.runners import Command
27
+ from secator.utils import get_versions_from_string
27
28
 
28
29
 
29
30
  class InstallerStatus(Enum):
@@ -72,7 +73,7 @@ class ToolInstaller:
72
73
 
73
74
  # Check PATH
74
75
  path_var = os.environ.get('PATH', '')
75
- if not str(CONFIG.dirs.bin) in path_var:
76
+ if str(CONFIG.dirs.bin) not in path_var:
76
77
  console.print(Warning(message=f'Bin directory {CONFIG.dirs.bin} not found in PATH ! Binaries installed by secator will not work')) # noqa: E501
77
78
  console.print(Warning(message=f'Run "export PATH=$PATH:{CONFIG.dirs.bin}" to add the binaries to your PATH'))
78
79
 
@@ -90,11 +91,14 @@ class ToolInstaller:
90
91
  status = gh_status
91
92
 
92
93
  # Install from source
93
- if tool_cls.install_cmd and not gh_status.is_ok():
94
- status = SourceInstaller.install(tool_cls.install_cmd, tool_cls.install_version)
95
- if not status.is_ok():
96
- cls.print_status(status, name)
97
- return status
94
+ if not gh_status.is_ok():
95
+ if not tool_cls.install_cmd:
96
+ status = InstallerStatus.INSTALL_SKIPPED_OK
97
+ else:
98
+ status = SourceInstaller.install(tool_cls.install_cmd, tool_cls.install_version)
99
+ if not status.is_ok():
100
+ cls.print_status(status, name)
101
+ return status
98
102
 
99
103
  # Install post commands
100
104
  if tool_cls.install_post:
@@ -215,6 +219,9 @@ class SourceInstaller:
215
219
  if '[install_version]' in install_cmd:
216
220
  version = version or 'latest'
217
221
  install_cmd = install_cmd.replace('[install_version]', version)
222
+ elif '[install_version_strip]' in install_cmd:
223
+ version = version or 'latest'
224
+ install_cmd = install_cmd.replace('[install_version_strip]', version.lstrip('v'))
218
225
 
219
226
  # Run command
220
227
  ret = Command.execute(install_cmd, cls_attributes={'shell': True}, quiet=False)
@@ -243,7 +250,7 @@ class GithubInstaller:
243
250
  system, arch, os_identifiers, arch_identifiers = cls._get_platform_identifier()
244
251
  download_url = cls._find_matching_asset(release['assets'], os_identifiers, arch_identifiers)
245
252
  if not download_url:
246
- console.print(Error(message=f'Could not find a GitHub release matching distribution (system: {system}, arch: {arch}).')) # noqa: E501
253
+ console.print(Warning(message=f'Could not find a GitHub release matching distribution (system: {system}, arch: {arch}).')) # noqa: E501
247
254
  return InstallerStatus.GITHUB_RELEASE_UNMATCHED_DISTRIBUTION
248
255
 
249
256
  # Download and unpack asset
@@ -415,14 +422,11 @@ def get_version(version_cmd):
415
422
  tuple[str]: Version string, return code.
416
423
  """
417
424
  from secator.runners import Command
418
- import re
419
- regex = r'[0-9]+\.[0-9]+\.?[0-9]*\.?[a-zA-Z]*'
420
425
  ret = Command.execute(version_cmd, quiet=True, print_errors=False)
421
- match = re.findall(regex, ret.output)
422
- if not match:
423
- console.print(Warning(message=f'Failed to find version in version command output. Command: {version_cmd}; Output: {ret.output}; Return code: {ret.return_code}')) # noqa: E501
426
+ versions = get_versions_from_string(ret.output)
427
+ if not versions:
424
428
  return None
425
- return match[0]
429
+ return versions[0]
426
430
 
427
431
 
428
432
  def parse_version(ver):
@@ -437,7 +441,7 @@ def parse_version(ver):
437
441
  return None
438
442
 
439
443
 
440
- def get_version_info(name, version_flag=None, install_github_handle=None, install_cmd=None, version=None):
444
+ def get_version_info(name, version_flag=None, install_github_handle=None, install_cmd=None, install_version=None, version=None, bleeding=False): # noqa: E501
441
445
  """Get version info for a command.
442
446
 
443
447
  Args:
@@ -445,7 +449,9 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
445
449
  version_flag (str): Version flag.
446
450
  install_github_handle (str): Github handle.
447
451
  install_cmd (str): Install command.
452
+ install_version (str): Install version.
448
453
  version (str): Existing version.
454
+ bleeding (bool): Bleeding edge.
449
455
 
450
456
  Return:
451
457
  dict: Version info.
@@ -457,10 +463,13 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
457
463
  'version': version,
458
464
  'version_cmd': None,
459
465
  'latest_version': None,
466
+ 'install_version': None,
460
467
  'location': None,
461
468
  'status': '',
462
469
  'outdated': False,
463
- 'errors': []
470
+ 'bleeding': False,
471
+ 'source': None,
472
+ 'errors': [],
464
473
  }
465
474
 
466
475
  # Get binary path
@@ -472,41 +481,40 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
472
481
  info['location'] = location
473
482
  info['installed'] = True
474
483
 
475
- # Get latest version
484
+ # Get latest / recommanded version
476
485
  latest_version = None
477
- if not CONFIG.offline_mode:
478
- if install_github_handle:
479
- latest_version = GithubInstaller.get_latest_version(install_github_handle)
480
- info['latest_version'] = latest_version
481
- elif install_cmd and install_cmd.startswith('pip'):
482
- req = requests.get(f'https://pypi.python.org/pypi/{name}/json')
483
- version = parse_version('0')
484
- if req.status_code == requests.codes.ok:
485
- j = json.loads(req.text.encode(req.encoding))
486
- releases = j.get('releases', [])
487
- for release in releases:
488
- ver = parse_version(release)
489
- if ver and not ver.is_prerelease:
490
- version = max(version, ver)
491
- latest_version = str(version)
492
- info['latest_version'] = latest_version
493
- elif install_cmd and install_cmd.startswith('sudo apt install'):
494
- ret = Command.execute(f'apt-cache madison {name}', quiet=True)
495
- if ret.return_code == 0:
496
- output = ret.output.split(' | ')
497
- if len(output) > 1:
498
- ver = parse_version(output[1].strip())
499
- if ver:
500
- latest_version = str(ver)
501
- info['latest_version'] = latest_version
486
+ if install_version and not bleeding:
487
+ ver = parse_version(install_version)
488
+ info['latest_version'] = str(ver)
489
+ info['install_version'] = str(ver)
490
+ info['source'] = 'supported'
491
+ latest_version = str(ver)
492
+ else:
493
+ latest_version = None
494
+ if not CONFIG.offline_mode:
495
+ if install_github_handle:
496
+ latest_version = GithubInstaller.get_latest_version(install_github_handle)
497
+ info['latest_version'] = latest_version
498
+ info['source'] = 'github'
499
+ elif install_cmd and install_cmd.startswith('pip'):
500
+ req = requests.get(f'https://pypi.python.org/pypi/{name}/json')
501
+ version = parse_version('0')
502
+ if req.status_code == requests.codes.ok:
503
+ j = json.loads(req.text.encode(req.encoding))
504
+ releases = j.get('releases', [])
505
+ for release in releases:
506
+ ver = parse_version(release)
507
+ if ver and not ver.is_prerelease and not ver.is_postrelease and not ver.is_devrelease:
508
+ version = max(version, ver)
509
+ latest_version = str(version)
510
+ info['source'] = 'pypi'
502
511
  else:
503
- error = f'Failed to get latest version for {name}. Command: apt-cache madison {name}'
504
- info['errors'].append(error)
505
- console.print(Warning(message=error))
512
+ info['errors'].append('Cannot get latest version for query method (github, pip) is available')
513
+ info['latest_version'] = f'v{latest_version}' if install_version and install_version.startswith('v') else latest_version # noqa: E501
506
514
 
507
515
  # Get current version
508
516
  version_flag = None if version_flag == OPT_NOT_SUPPORTED else version_flag
509
- if version_flag:
517
+ if version_flag and not version:
510
518
  version_cmd = f'{name} {version_flag}'
511
519
  info['version_cmd'] = version_cmd
512
520
  version = get_version(version_cmd)
@@ -518,11 +526,20 @@ def get_version_info(name, version_flag=None, install_github_handle=None, instal
518
526
 
519
527
  # Check if up-to-date
520
528
  if version and latest_version:
521
- if parse_version(version) < parse_version(latest_version):
529
+ outdated = parse_version(version) < parse_version(latest_version)
530
+ equal = parse_version(version) == parse_version(latest_version)
531
+ if outdated:
522
532
  info['status'] = 'outdated'
523
533
  info['outdated'] = True
524
- else:
534
+ elif equal:
525
535
  info['status'] = 'latest'
536
+ else:
537
+ info['status'] = 'bleeding'
538
+ info['bleeding'] = True
539
+ if install_version:
540
+ info['errors'].append(f'Version {version} is greather than the recommended version {latest_version}')
541
+ else:
542
+ info['errors'].append(f'Version {version} is greather than the latest version {latest_version}')
526
543
  elif not version:
527
544
  info['status'] = 'current unknown'
528
545
  elif not latest_version:
@@ -547,6 +564,7 @@ def get_distro_config():
547
564
 
548
565
  if system == "Linux":
549
566
  distrib = distro.like() or distro.id()
567
+ distrib = distrib.split(' ')[0] if distrib else None
550
568
 
551
569
  if distrib in ["ubuntu", "debian", "linuxmint", "popos", "kali"]:
552
570
  installer = "apt install -y --no-install-recommends"
@@ -593,22 +611,32 @@ def get_distro_config():
593
611
  def fmt_health_table_row(version_info, category=None):
594
612
  name = version_info['name']
595
613
  version = version_info['version']
614
+ if version:
615
+ version = version.lstrip('v')
596
616
  status = version_info['status']
597
617
  installed = version_info['installed']
598
618
  latest_version = version_info['latest_version']
619
+ if latest_version:
620
+ latest_version = latest_version.lstrip('v')
621
+ source = version_info.get('source')
599
622
  name_str = f'[magenta]{name:<13}[/]'
600
623
 
601
624
  # Format version row
602
625
  _version = version or ''
603
626
  _version = f'[bold green]{_version:<10}[/]'
604
627
  if status == 'latest':
605
- _version += ' [bold green](latest)[/]'
628
+ _version += f' [bold green](latest {source})[/]'
629
+ elif status == 'bleeding':
630
+ msg = f'bleeding >{latest_version} {source}' if source else f'bleeding >{latest_version}'
631
+ _version += f' [bold orange1]({msg})[/]'
606
632
  elif status == 'outdated':
607
633
  _version += ' [bold red](outdated)[/]'
608
634
  if latest_version:
609
- _version += f' [dim](<{latest_version})'
635
+ _version += f' [dim](<{latest_version} {source})[/]'
610
636
  elif status == 'missing':
611
637
  _version = '[bold red]missing[/]'
638
+ elif status == 'missing_ok':
639
+ _version = '[dim green]not installed [/]'
612
640
  elif status == 'ok':
613
641
  _version = '[bold green]ok [/]'
614
642
  elif status == 'version fetch error':
secator/loader.py ADDED
@@ -0,0 +1,116 @@
1
+ from functools import cache
2
+ from pkgutil import iter_modules
3
+ from secator.rich import console
4
+ from secator.config import CONFIG, CONFIGS_FOLDER
5
+ from secator.template import TemplateLoader
6
+ from secator.utils import debug
7
+ from pathlib import Path
8
+ import glob
9
+ import importlib
10
+ import inspect
11
+ import sys
12
+
13
+
14
+ @cache
15
+ def find_templates():
16
+ discover_tasks() # always load tasks first
17
+ results = []
18
+ dirs = [CONFIGS_FOLDER]
19
+ if CONFIG.dirs.templates:
20
+ dirs.append(CONFIG.dirs.templates)
21
+ paths = []
22
+ for dir in dirs:
23
+ config_paths = [
24
+ Path(path)
25
+ for path in glob.glob(str(dir).rstrip('/') + '/**/*.y*ml', recursive=True)
26
+ ]
27
+ debug(f'Found {len(config_paths)} templates in {dir}', sub='template')
28
+ paths.extend(config_paths)
29
+ for path in paths:
30
+ config = TemplateLoader(input=path)
31
+ debug(f'Loaded template from {path}', sub='template')
32
+ results.append(config)
33
+ return results
34
+
35
+
36
+ @cache
37
+ def get_configs_by_type(type):
38
+ if type == 'task':
39
+ tasks = discover_tasks()
40
+ task_config = [TemplateLoader({'name': cls.__name__, 'type': 'task', 'input_types': cls.input_types, 'output_types': [t.get_name() for t in cls.output_types]}) for cls in tasks] # noqa: E501
41
+ return sorted(task_config, key=lambda x: x['name'])
42
+ return sorted([t for t in find_templates() if t.type == type], key=lambda x: x.name)
43
+
44
+
45
+ @cache
46
+ def discover_tasks():
47
+ """Find all secator tasks (internal + external)."""
48
+ return discover_internal_tasks() + discover_external_tasks()
49
+
50
+
51
+ @cache
52
+ def discover_internal_tasks():
53
+ """Find internal secator tasks."""
54
+ from secator.runners import Runner
55
+ package_dir = Path(__file__).resolve().parent / 'tasks'
56
+ task_classes = []
57
+ for (_, module_name, _) in iter_modules([str(package_dir)]):
58
+ if module_name.startswith('_'):
59
+ continue
60
+ try:
61
+ module = importlib.import_module(f'secator.tasks.{module_name}')
62
+ except ImportError as e:
63
+ console.print(f'[bold red]Could not import secator.tasks.{module_name}:[/]')
64
+ console.print(f'\t[bold red]{type(e).__name__}[/]: {str(e)}')
65
+ continue
66
+ for attribute_name in dir(module):
67
+ attribute = getattr(module, attribute_name)
68
+ if inspect.isclass(attribute):
69
+ bases = inspect.getmro(attribute)
70
+ if Runner in bases and hasattr(attribute, '__task__'):
71
+ attribute.__external__ = False
72
+ task_classes.append(attribute)
73
+
74
+ # Sort task_classes by category
75
+ task_classes = sorted(
76
+ task_classes,
77
+ # key=lambda x: (get_command_category(x), x.__name__)
78
+ key=lambda x: x.__name__)
79
+ return task_classes
80
+
81
+
82
+ @cache
83
+ def discover_external_tasks():
84
+ """Find external secator tasks."""
85
+ output = []
86
+ prev_state = sys.dont_write_bytecode
87
+ sys.dont_write_bytecode = True
88
+ for path in CONFIG.dirs.templates.glob('**/*.py'):
89
+ try:
90
+ task_name = path.stem
91
+ module_name = f'secator.tasks.{task_name}'
92
+
93
+ # console.print(f'Importing module {module_name} from {path}')
94
+ spec = importlib.util.spec_from_file_location(module_name, path)
95
+ module = importlib.util.module_from_spec(spec)
96
+ if not spec:
97
+ console.print(f'[bold red]Could not load external module {path.name}: invalid import spec.[/] ({path})')
98
+ continue
99
+ # console.print(f'Adding module "{module_name}" to sys path')
100
+ sys.modules[module_name] = module
101
+
102
+ # console.print(f'Executing module "{module}"')
103
+ spec.loader.exec_module(module)
104
+
105
+ # console.print(f'Checking that {module} contains task {task_name}')
106
+ if not hasattr(module, task_name):
107
+ console.print(f'[bold orange1]Could not load external task "{task_name}" from module {path.name}[/] ({path})')
108
+ continue
109
+ cls = getattr(module, task_name)
110
+ console.print(f'[bold green]Successfully loaded external task "{task_name}"[/] ({path})')
111
+ cls.__external__ = True
112
+ output.append(cls)
113
+ except Exception as e:
114
+ console.print(f'[bold red]Could not load external module {path.name}. Reason: {str(e)}.[/] ({path})')
115
+ sys.dont_write_bytecode = prev_state
116
+ return output
@@ -12,6 +12,9 @@ class OutputType:
12
12
  _table_fields = []
13
13
  _sort_by = ()
14
14
 
15
+ def __str__(self):
16
+ return self.__class__.__name__
17
+
15
18
  def __gt__(self, other):
16
19
  if not self.__eq__(other):
17
20
  return False
@@ -8,71 +8,71 @@ from secator.definitions import CERTIFICATE_STATUS_UNKNOWN
8
8
 
9
9
  @dataclass
10
10
  class Certificate(OutputType):
11
- host: str
12
- fingerprint_sha256: str = field(default='')
13
- ip: str = field(default='', compare=False)
14
- raw_value: str = field(default='', compare=False)
15
- subject_cn: str = field(default='', compare=False)
16
- subject_an: list[str] = field(default_factory=list, compare=False)
17
- not_before: datetime = field(default=None, compare=False)
18
- not_after: datetime = field(default=None, compare=False)
19
- issuer_dn: str = field(default='', compare=False)
20
- issuer_cn: str = field(default='', compare=False)
21
- issuer: str = field(default='', compare=False)
22
- self_signed: bool = field(default=True, compare=False)
23
- trusted: bool = field(default=False, compare=False)
24
- status: str = field(default=CERTIFICATE_STATUS_UNKNOWN, compare=False)
25
- keysize: int = field(default=None, compare=False)
26
- serial_number: str = field(default='', compare=False)
27
- ciphers: list[str] = field(default_factory=list, compare=False)
28
- # parent_certificate: 'Certificate' = None # noqa: F821
29
- _source: str = field(default='', repr=True)
30
- _type: str = field(default='certificate', repr=True)
31
- _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
32
- _uuid: str = field(default='', repr=True, compare=False)
33
- _context: dict = field(default_factory=dict, repr=True, compare=False)
34
- _tagged: bool = field(default=False, repr=True, compare=False)
35
- _duplicate: bool = field(default=False, repr=True, compare=False)
36
- _related: list = field(default_factory=list, compare=False)
37
- _table_fields = ['ip', 'host']
38
- _sort_by = ('ip',)
11
+ host: str
12
+ fingerprint_sha256: str = field(default='')
13
+ ip: str = field(default='', compare=False)
14
+ raw_value: str = field(default='', compare=False)
15
+ subject_cn: str = field(default='', compare=False)
16
+ subject_an: list[str] = field(default_factory=list, compare=False)
17
+ not_before: datetime = field(default=None, compare=False)
18
+ not_after: datetime = field(default=None, compare=False)
19
+ issuer_dn: str = field(default='', compare=False)
20
+ issuer_cn: str = field(default='', compare=False)
21
+ issuer: str = field(default='', compare=False)
22
+ self_signed: bool = field(default=True, compare=False)
23
+ trusted: bool = field(default=False, compare=False)
24
+ status: str = field(default=CERTIFICATE_STATUS_UNKNOWN, compare=False)
25
+ keysize: int = field(default=None, compare=False)
26
+ serial_number: str = field(default='', compare=False)
27
+ ciphers: list[str] = field(default_factory=list, compare=False)
28
+ # parent_certificate: 'Certificate' = None # noqa: F821
29
+ _source: str = field(default='', repr=True)
30
+ _type: str = field(default='certificate', repr=True)
31
+ _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
32
+ _uuid: str = field(default='', repr=True, compare=False)
33
+ _context: dict = field(default_factory=dict, repr=True, compare=False)
34
+ _tagged: bool = field(default=False, repr=True, compare=False)
35
+ _duplicate: bool = field(default=False, repr=True, compare=False)
36
+ _related: list = field(default_factory=list, compare=False)
37
+ _table_fields = ['ip', 'host']
38
+ _sort_by = ('ip',)
39
39
 
40
- def __str__(self) -> str:
41
- return self.subject_cn
40
+ def __str__(self) -> str:
41
+ return self.subject_cn
42
42
 
43
- def is_expired(self) -> bool:
44
- if self.not_after:
45
- return self.not_after < datetime.now()
46
- return True
43
+ def is_expired(self) -> bool:
44
+ if self.not_after:
45
+ return self.not_after < datetime.now()
46
+ return True
47
47
 
48
- def is_expired_soon(self, months: int = 1) -> bool:
49
- if self.not_after:
50
- return self.not_after < datetime.now() + timedelta(days=months * 30)
51
- return True
48
+ def is_expired_soon(self, months: int = 1) -> bool:
49
+ if self.not_after:
50
+ return self.not_after < datetime.now() + timedelta(days=months * 30)
51
+ return True
52
52
 
53
- @staticmethod
54
- def format_date(date):
55
- if date:
56
- return date.strftime("%m/%d/%Y")
57
- return '?'
53
+ @staticmethod
54
+ def format_date(date):
55
+ if date:
56
+ return date.strftime("%m/%d/%Y")
57
+ return '?'
58
58
 
59
- def __repr__(self) -> str:
60
- s = f'📜 [bold white]{self.host}[/]'
61
- s += f' [cyan]{self.status}[/]'
62
- s += rf' [white]\[fingerprint={self.fingerprint_sha256[:10]}][/]'
63
- if self.subject_cn:
64
- s += rf' [white]\[cn={self.subject_cn}][/]'
65
- if self.subject_an:
66
- s += rf' [white]\[an={", ".join(self.subject_an)}][/]'
67
- if self.issuer:
68
- s += rf' [white]\[issuer={self.issuer}][/]'
69
- elif self.issuer_cn:
70
- s += rf' [white]\[issuer_cn={self.issuer_cn}][/]'
71
- expiry_date = Certificate.format_date(self.not_after)
72
- if self.is_expired():
73
- s += f' [red]expired since {expiry_date}[/red]'
74
- elif self.is_expired_soon(months=2):
75
- s += f' [yellow]expires <2 months[/yellow], [yellow]valid until {expiry_date}[/yellow]'
76
- else:
77
- s += f' [green]not expired[/green], [yellow]valid until {expiry_date}[/yellow]'
78
- return rich_to_ansi(s)
59
+ def __repr__(self) -> str:
60
+ s = f'📜 [bold white]{self.host}[/]'
61
+ s += f' [cyan]{self.status}[/]'
62
+ s += rf' [white]\[fingerprint={self.fingerprint_sha256[:10]}][/]'
63
+ if self.subject_cn:
64
+ s += rf' [white]\[cn={self.subject_cn}][/]'
65
+ if self.subject_an:
66
+ s += rf' [white]\[an={", ".join(self.subject_an)}][/]'
67
+ if self.issuer:
68
+ s += rf' [white]\[issuer={self.issuer}][/]'
69
+ elif self.issuer_cn:
70
+ s += rf' [white]\[issuer_cn={self.issuer_cn}][/]'
71
+ expiry_date = Certificate.format_date(self.not_after)
72
+ if self.is_expired():
73
+ s += f' [red]expired since {expiry_date}[/red]'
74
+ elif self.is_expired_soon(months=2):
75
+ s += f' [yellow]expires <2 months[/yellow], [yellow]valid until {expiry_date}[/yellow]'
76
+ else:
77
+ s += f' [green]not expired[/green], [yellow]valid until {expiry_date}[/yellow]'
78
+ return rich_to_ansi(s)
@@ -22,20 +22,19 @@ class Error(OutputType):
22
22
 
23
23
  def from_exception(e, **kwargs):
24
24
  errtype = type(e).__name__
25
- message = errtype
26
25
  if str(e):
27
- message += f': {str(e)}'
26
+ errtype += f': {str(e)}'
27
+ message = kwargs.pop('message', errtype)
28
28
  traceback = traceback_as_string(e) if errtype not in ['KeyboardInterrupt', 'GreenletExit'] else ''
29
- error = Error(message=message, traceback=traceback, **kwargs)
29
+ error = Error(message=_s(message), traceback=traceback, **kwargs)
30
30
  return error
31
31
 
32
32
  def __str__(self):
33
33
  return self.message
34
34
 
35
35
  def __repr__(self):
36
- s = rf"\[[bold red]ERR[/]] {_s(self.message)}"
36
+ s = rf"\[[bold red]ERR[/]] {self.message}"
37
37
  if self.traceback:
38
- s += ':'
39
38
  traceback_pretty = ' ' + _s(self.traceback).replace('\n', '\n ')
40
39
  if self.traceback_title:
41
40
  traceback_pretty = f' {self.traceback_title}:\n{traceback_pretty}'
@@ -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, rich_escape as _s
4
+ from secator.utils import rich_to_ansi
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 = rf"\[[blue]INF[/]] {_s(self.message)}"
23
+ s = rf"\[[blue]INF[/]] {self.message}"
24
24
  return rich_to_ansi(s)
@@ -15,7 +15,7 @@ class IpProtocol(str, Enum):
15
15
  @dataclass
16
16
  class Ip(OutputType):
17
17
  ip: str
18
- host: str = ''
18
+ host: str = field(default='', repr=True, compare=False)
19
19
  alive: bool = False
20
20
  protocol: str = field(default=IpProtocol.IPv4)
21
21
  _source: str = field(default='', repr=True)
@@ -37,4 +37,6 @@ class Ip(OutputType):
37
37
  s = f'💻 [bold white]{self.ip}[/]'
38
38
  if self.host:
39
39
  s += rf' \[[bold magenta]{self.host}[/]]'
40
+ if self.alive:
41
+ s += r' [bold green]🟢[/]'
40
42
  return rich_to_ansi(s)
@@ -2,14 +2,12 @@ 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, format_object
6
6
 
7
7
 
8
8
  @dataclass
9
9
  class Progress(OutputType):
10
- duration: str
11
10
  percent: int = 0
12
- errors: list = field(default_factory=list)
13
11
  extra_data: dict = field(default_factory=dict)
14
12
  _source: str = field(default='', repr=True)
15
13
  _type: str = field(default='progress', repr=True)
@@ -20,7 +18,7 @@ class Progress(OutputType):
20
18
  _duplicate: bool = field(default=False, repr=True, compare=False)
21
19
  _related: list = field(default_factory=list, compare=False)
22
20
 
23
- _table_fields = ['percent', 'duration']
21
+ _table_fields = ['percent']
24
22
  _sort_by = ('percent',)
25
23
 
26
24
  def __post_init__(self):
@@ -32,9 +30,7 @@ class Progress(OutputType):
32
30
  return f'{self.percent}%'
33
31
 
34
32
  def __repr__(self) -> str:
35
- s = f'[dim]⏳ {self.percent}% ' + '█' * (self.percent // 10) + '[/]'
36
- if self.errors:
37
- s += f' [dim red]errors={self.errors}[/]'
38
- ed = ' '.join([f'{k}={v}' for k, v in self.extra_data.items() if k != 'startedAt' and v])
39
- s += f' [dim yellow]{ed}[/]'
33
+ s = f'[dim]⏳ [bold]{self.percent}%[/] ' + '█' * (self.percent // 10) + '[/]'
34
+ ed = format_object(self.extra_data, color='yellow3', skip_keys=['startedAt'])
35
+ s += f'[dim]{ed}[/]'
40
36
  return rich_to_ansi(s)
@@ -7,23 +7,23 @@ from secator.utils import rich_to_ansi
7
7
 
8
8
  @dataclass
9
9
  class State(OutputType):
10
- """Represents the state of a Celery task."""
10
+ """Represents the state of a Celery task."""
11
11
 
12
- task_id: str
13
- state: str
14
- _type: str = field(default='state', repr=True)
15
- _source: str = field(default='', repr=True)
16
- _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
17
- _uuid: str = field(default='', repr=True, compare=False)
18
- _context: dict = field(default_factory=dict, repr=True, compare=False)
19
- _tagged: bool = field(default=False, repr=True, compare=False)
20
- _duplicate: bool = field(default=False, repr=True, compare=False)
21
- _related: list = field(default_factory=list, compare=False)
22
- _icon = '📊'
23
- _color = 'bright_blue'
12
+ task_id: str
13
+ state: str
14
+ _type: str = field(default='state', repr=True)
15
+ _source: str = field(default='', repr=True)
16
+ _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
17
+ _uuid: str = field(default='', repr=True, compare=False)
18
+ _context: dict = field(default_factory=dict, repr=True, compare=False)
19
+ _tagged: bool = field(default=False, repr=True, compare=False)
20
+ _duplicate: bool = field(default=False, repr=True, compare=False)
21
+ _related: list = field(default_factory=list, compare=False)
22
+ _icon = '📊'
23
+ _color = 'bright_blue'
24
24
 
25
- def __str__(self) -> str:
26
- return f"Task {self.task_id} is {self.state}"
25
+ def __str__(self) -> str:
26
+ return f"Task {self.task_id} is {self.state}"
27
27
 
28
- def __repr__(self) -> str:
29
- return rich_to_ansi(f"{self._icon} [bold {self._color}]{self.state}[/] {self.task_id}")
28
+ def __repr__(self) -> str:
29
+ return rich_to_ansi(f"{self._icon} [bold {self._color}]{self.state}[/] {self.task_id}")
@@ -10,6 +10,7 @@ class Tag(OutputType):
10
10
  name: str
11
11
  match: str
12
12
  extra_data: dict = field(default_factory=dict, repr=True, compare=False)
13
+ stored_response_path: str = field(default='', compare=False)
13
14
  _source: str = field(default='', repr=True)
14
15
  _type: str = field(default='tag', repr=True)
15
16
  _timestamp: int = field(default_factory=lambda: time.time(), compare=False)
@@ -32,6 +33,8 @@ class Tag(OutputType):
32
33
  s = f'🏷️ [bold magenta]{self.name}[/]'
33
34
  s += f' found @ [bold]{_s(self.match)}[/]'
34
35
  ed = ''
36
+ if self.stored_response_path:
37
+ s += rf' [link=file://{self.stored_response_path}]:incoming_envelope:[/]'
35
38
  if self.extra_data:
36
39
  for k, v in self.extra_data.items():
37
40
  sep = ' '