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.
- secator/celery.py +40 -24
- secator/celery_signals.py +71 -68
- secator/celery_utils.py +43 -27
- secator/cli.py +520 -280
- secator/cli_helper.py +394 -0
- secator/click.py +87 -0
- secator/config.py +67 -39
- secator/configs/profiles/http_headless.yaml +6 -0
- secator/configs/profiles/http_record.yaml +6 -0
- secator/configs/profiles/tor.yaml +1 -1
- secator/configs/scans/domain.yaml +4 -2
- secator/configs/scans/host.yaml +1 -1
- secator/configs/scans/network.yaml +1 -4
- secator/configs/scans/subdomain.yaml +13 -1
- secator/configs/scans/url.yaml +1 -2
- secator/configs/workflows/cidr_recon.yaml +6 -4
- secator/configs/workflows/code_scan.yaml +1 -1
- secator/configs/workflows/host_recon.yaml +29 -3
- secator/configs/workflows/subdomain_recon.yaml +67 -16
- secator/configs/workflows/url_crawl.yaml +44 -15
- secator/configs/workflows/url_dirsearch.yaml +4 -4
- secator/configs/workflows/url_fuzz.yaml +25 -17
- secator/configs/workflows/url_params_fuzz.yaml +7 -0
- secator/configs/workflows/url_vuln.yaml +33 -8
- secator/configs/workflows/user_hunt.yaml +2 -1
- secator/configs/workflows/wordpress.yaml +5 -3
- secator/cve.py +718 -0
- secator/decorators.py +0 -454
- secator/definitions.py +49 -30
- secator/exporters/_base.py +2 -2
- secator/exporters/console.py +2 -2
- secator/exporters/table.py +4 -3
- secator/exporters/txt.py +1 -1
- secator/hooks/mongodb.py +2 -4
- secator/installer.py +77 -49
- secator/loader.py +116 -0
- secator/output_types/_base.py +3 -0
- secator/output_types/certificate.py +63 -63
- secator/output_types/error.py +4 -5
- secator/output_types/info.py +2 -2
- secator/output_types/ip.py +3 -1
- secator/output_types/progress.py +5 -9
- secator/output_types/state.py +17 -17
- secator/output_types/tag.py +3 -0
- secator/output_types/target.py +10 -2
- secator/output_types/url.py +19 -7
- secator/output_types/vulnerability.py +11 -7
- secator/output_types/warning.py +2 -2
- secator/report.py +27 -15
- secator/rich.py +18 -10
- secator/runners/_base.py +446 -233
- secator/runners/_helpers.py +133 -24
- secator/runners/command.py +182 -102
- secator/runners/scan.py +33 -5
- secator/runners/task.py +13 -7
- secator/runners/workflow.py +105 -72
- secator/scans/__init__.py +2 -2
- secator/serializers/dataclass.py +20 -20
- secator/tasks/__init__.py +4 -4
- secator/tasks/_categories.py +39 -27
- secator/tasks/arjun.py +9 -5
- secator/tasks/bbot.py +53 -21
- secator/tasks/bup.py +19 -5
- secator/tasks/cariddi.py +24 -3
- secator/tasks/dalfox.py +26 -7
- secator/tasks/dirsearch.py +10 -4
- secator/tasks/dnsx.py +70 -25
- secator/tasks/feroxbuster.py +11 -3
- secator/tasks/ffuf.py +42 -6
- secator/tasks/fping.py +20 -8
- secator/tasks/gau.py +3 -1
- secator/tasks/gf.py +3 -3
- secator/tasks/gitleaks.py +2 -2
- secator/tasks/gospider.py +7 -1
- secator/tasks/grype.py +5 -4
- secator/tasks/h8mail.py +2 -1
- secator/tasks/httpx.py +18 -5
- secator/tasks/katana.py +35 -15
- secator/tasks/maigret.py +4 -4
- secator/tasks/mapcidr.py +3 -3
- secator/tasks/msfconsole.py +4 -4
- secator/tasks/naabu.py +2 -2
- secator/tasks/nmap.py +12 -14
- secator/tasks/nuclei.py +3 -3
- secator/tasks/searchsploit.py +4 -5
- secator/tasks/subfinder.py +2 -2
- secator/tasks/testssl.py +264 -263
- secator/tasks/trivy.py +5 -5
- secator/tasks/wafw00f.py +21 -3
- secator/tasks/wpprobe.py +90 -83
- secator/tasks/wpscan.py +6 -5
- secator/template.py +218 -104
- secator/thread.py +15 -15
- secator/tree.py +196 -0
- secator/utils.py +131 -123
- secator/utils_test.py +60 -19
- secator/workflows/__init__.py +2 -2
- {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/METADATA +36 -36
- secator-0.16.0.dist-info/RECORD +132 -0
- secator/configs/profiles/default.yaml +0 -8
- secator/configs/workflows/url_nuclei.yaml +0 -11
- secator/tasks/dnsxbrute.py +0 -42
- secator-0.15.1.dist-info/RECORD +0 -128
- {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/WHEEL +0 -0
- {secator-0.15.1.dist-info → secator-0.16.0.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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
|
-
|
|
422
|
-
if not
|
|
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
|
|
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
|
-
'
|
|
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
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
504
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
secator/output_types/_base.py
CHANGED
|
@@ -8,71 +8,71 @@ from secator.definitions import CERTIFICATE_STATUS_UNKNOWN
|
|
|
8
8
|
|
|
9
9
|
@dataclass
|
|
10
10
|
class Certificate(OutputType):
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.subject_cn
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
def is_expired(self) -> bool:
|
|
44
|
+
if self.not_after:
|
|
45
|
+
return self.not_after < datetime.now()
|
|
46
|
+
return True
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
@staticmethod
|
|
54
|
+
def format_date(date):
|
|
55
|
+
if date:
|
|
56
|
+
return date.strftime("%m/%d/%Y")
|
|
57
|
+
return '?'
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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)
|
secator/output_types/error.py
CHANGED
|
@@ -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
|
-
|
|
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[/]] {
|
|
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}'
|
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
|
|
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[/]] {
|
|
23
|
+
s = rf"\[[blue]INF[/]] {self.message}"
|
|
24
24
|
return rich_to_ansi(s)
|
secator/output_types/ip.py
CHANGED
|
@@ -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)
|
secator/output_types/progress.py
CHANGED
|
@@ -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'
|
|
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
|
-
|
|
37
|
-
|
|
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)
|
secator/output_types/state.py
CHANGED
|
@@ -7,23 +7,23 @@ from secator.utils import rich_to_ansi
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
9
|
class State(OutputType):
|
|
10
|
-
|
|
10
|
+
"""Represents the state of a Celery task."""
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
25
|
+
def __str__(self) -> str:
|
|
26
|
+
return f"Task {self.task_id} is {self.state}"
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
def __repr__(self) -> str:
|
|
29
|
+
return rich_to_ansi(f"{self._icon} [bold {self._color}]{self.state}[/] {self.task_id}")
|
secator/output_types/tag.py
CHANGED
|
@@ -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 = ' '
|