secator 0.22.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.
- secator/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +453 -0
- secator/celery_signals.py +138 -0
- secator/celery_utils.py +320 -0
- secator/cli.py +2035 -0
- secator/cli_helper.py +395 -0
- secator/click.py +87 -0
- secator/config.py +670 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +8 -0
- secator/configs/profiles/all_ports.yaml +7 -0
- secator/configs/profiles/full.yaml +31 -0
- secator/configs/profiles/http_headless.yaml +7 -0
- secator/configs/profiles/http_record.yaml +8 -0
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/passive.yaml +11 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +31 -0
- secator/configs/scans/host.yaml +23 -0
- secator/configs/scans/network.yaml +30 -0
- secator/configs/scans/subdomain.yaml +27 -0
- secator/configs/scans/url.yaml +19 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +48 -0
- secator/configs/workflows/code_scan.yaml +29 -0
- secator/configs/workflows/domain_recon.yaml +46 -0
- secator/configs/workflows/host_recon.yaml +95 -0
- secator/configs/workflows/subdomain_recon.yaml +120 -0
- secator/configs/workflows/url_bypass.yaml +15 -0
- secator/configs/workflows/url_crawl.yaml +98 -0
- secator/configs/workflows/url_dirsearch.yaml +62 -0
- secator/configs/workflows/url_fuzz.yaml +68 -0
- secator/configs/workflows/url_params_fuzz.yaml +66 -0
- secator/configs/workflows/url_secrets_hunt.yaml +23 -0
- secator/configs/workflows/url_vuln.yaml +91 -0
- secator/configs/workflows/user_hunt.yaml +29 -0
- secator/configs/workflows/wordpress.yaml +38 -0
- secator/cve.py +718 -0
- secator/decorators.py +7 -0
- secator/definitions.py +168 -0
- secator/exporters/__init__.py +14 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +37 -0
- secator/exporters/gdrive.py +123 -0
- secator/exporters/json.py +16 -0
- secator/exporters/table.py +36 -0
- secator/exporters/txt.py +28 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/gcs.py +80 -0
- secator/hooks/mongodb.py +281 -0
- secator/installer.py +694 -0
- secator/loader.py +128 -0
- secator/output_types/__init__.py +49 -0
- secator/output_types/_base.py +108 -0
- secator/output_types/certificate.py +78 -0
- secator/output_types/domain.py +50 -0
- secator/output_types/error.py +42 -0
- secator/output_types/exploit.py +58 -0
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +47 -0
- secator/output_types/port.py +55 -0
- secator/output_types/progress.py +36 -0
- secator/output_types/record.py +36 -0
- secator/output_types/stat.py +41 -0
- secator/output_types/state.py +29 -0
- secator/output_types/subdomain.py +45 -0
- secator/output_types/tag.py +69 -0
- secator/output_types/target.py +38 -0
- secator/output_types/url.py +112 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +101 -0
- secator/output_types/warning.py +30 -0
- secator/report.py +140 -0
- secator/rich.py +130 -0
- secator/runners/__init__.py +14 -0
- secator/runners/_base.py +1240 -0
- secator/runners/_helpers.py +218 -0
- secator/runners/celery.py +18 -0
- secator/runners/command.py +1178 -0
- secator/runners/python.py +126 -0
- secator/runners/scan.py +87 -0
- secator/runners/task.py +81 -0
- secator/runners/workflow.py +168 -0
- secator/scans/__init__.py +29 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +39 -0
- secator/serializers/json.py +45 -0
- secator/serializers/regex.py +25 -0
- secator/tasks/__init__.py +8 -0
- secator/tasks/_categories.py +487 -0
- secator/tasks/arjun.py +113 -0
- secator/tasks/arp.py +53 -0
- secator/tasks/arpscan.py +70 -0
- secator/tasks/bbot.py +372 -0
- secator/tasks/bup.py +118 -0
- secator/tasks/cariddi.py +193 -0
- secator/tasks/dalfox.py +87 -0
- secator/tasks/dirsearch.py +84 -0
- secator/tasks/dnsx.py +186 -0
- secator/tasks/feroxbuster.py +93 -0
- secator/tasks/ffuf.py +135 -0
- secator/tasks/fping.py +85 -0
- secator/tasks/gau.py +102 -0
- secator/tasks/getasn.py +60 -0
- secator/tasks/gf.py +36 -0
- secator/tasks/gitleaks.py +96 -0
- secator/tasks/gospider.py +84 -0
- secator/tasks/grype.py +109 -0
- secator/tasks/h8mail.py +75 -0
- secator/tasks/httpx.py +167 -0
- secator/tasks/jswhois.py +36 -0
- secator/tasks/katana.py +203 -0
- secator/tasks/maigret.py +87 -0
- secator/tasks/mapcidr.py +42 -0
- secator/tasks/msfconsole.py +179 -0
- secator/tasks/naabu.py +85 -0
- secator/tasks/nmap.py +487 -0
- secator/tasks/nuclei.py +151 -0
- secator/tasks/search_vulns.py +225 -0
- secator/tasks/searchsploit.py +109 -0
- secator/tasks/sshaudit.py +299 -0
- secator/tasks/subfinder.py +48 -0
- secator/tasks/testssl.py +283 -0
- secator/tasks/trivy.py +130 -0
- secator/tasks/trufflehog.py +240 -0
- secator/tasks/urlfinder.py +100 -0
- secator/tasks/wafw00f.py +106 -0
- secator/tasks/whois.py +34 -0
- secator/tasks/wpprobe.py +116 -0
- secator/tasks/wpscan.py +202 -0
- secator/tasks/x8.py +94 -0
- secator/tasks/xurlfind3r.py +83 -0
- secator/template.py +294 -0
- secator/thread.py +24 -0
- secator/tree.py +196 -0
- secator/utils.py +922 -0
- secator/utils_test.py +297 -0
- secator/workflows/__init__.py +29 -0
- secator-0.22.0.dist-info/METADATA +447 -0
- secator-0.22.0.dist-info/RECORD +150 -0
- secator-0.22.0.dist-info/WHEEL +4 -0
- secator-0.22.0.dist-info/entry_points.txt +2 -0
- secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/loader.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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({
|
|
41
|
+
'name': cls.__name__,
|
|
42
|
+
'type': 'task',
|
|
43
|
+
'description': cls.__doc__,
|
|
44
|
+
'input_types': cls.input_types,
|
|
45
|
+
'output_types': [t.get_name() for t in cls.output_types],
|
|
46
|
+
'default_inputs': cls.default_inputs,
|
|
47
|
+
'proxychains': getattr(cls, 'proxychains', True),
|
|
48
|
+
'proxy_socks5': getattr(cls, 'proxy_socks5', True),
|
|
49
|
+
'proxy_http': getattr(cls, 'proxy_http', True),
|
|
50
|
+
'default_cmd': getattr(cls, 'cmd', None),
|
|
51
|
+
'install_cmd': getattr(cls, 'install_cmd', None),
|
|
52
|
+
}) for cls in tasks] # noqa: E501
|
|
53
|
+
return sorted(task_config, key=lambda x: x['name'])
|
|
54
|
+
return sorted([t for t in find_templates() if t.type == type], key=lambda x: x.name)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@cache
|
|
58
|
+
def discover_tasks():
|
|
59
|
+
"""Find all secator tasks (internal + external)."""
|
|
60
|
+
return discover_internal_tasks() + discover_external_tasks()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@cache
|
|
64
|
+
def discover_internal_tasks():
|
|
65
|
+
"""Find internal secator tasks."""
|
|
66
|
+
from secator.runners import Runner
|
|
67
|
+
package_dir = Path(__file__).resolve().parent / 'tasks'
|
|
68
|
+
task_classes = []
|
|
69
|
+
for (_, module_name, _) in iter_modules([str(package_dir)]):
|
|
70
|
+
if module_name.startswith('_'):
|
|
71
|
+
continue
|
|
72
|
+
try:
|
|
73
|
+
module = importlib.import_module(f'secator.tasks.{module_name}')
|
|
74
|
+
except ImportError as e:
|
|
75
|
+
console.print(f'[bold red]Could not import secator.tasks.{module_name}:[/]')
|
|
76
|
+
console.print(f'\t[bold red]{type(e).__name__}[/]: {str(e)}')
|
|
77
|
+
continue
|
|
78
|
+
for attribute_name in dir(module):
|
|
79
|
+
attribute = getattr(module, attribute_name)
|
|
80
|
+
if inspect.isclass(attribute):
|
|
81
|
+
bases = inspect.getmro(attribute)
|
|
82
|
+
if Runner in bases and hasattr(attribute, '__task__'):
|
|
83
|
+
attribute.__external__ = False
|
|
84
|
+
task_classes.append(attribute)
|
|
85
|
+
|
|
86
|
+
# Sort task_classes by category
|
|
87
|
+
task_classes = sorted(
|
|
88
|
+
task_classes,
|
|
89
|
+
# key=lambda x: (get_command_category(x), x.__name__)
|
|
90
|
+
key=lambda x: x.__name__)
|
|
91
|
+
return task_classes
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@cache
|
|
95
|
+
def discover_external_tasks():
|
|
96
|
+
"""Find external secator tasks."""
|
|
97
|
+
output = []
|
|
98
|
+
prev_state = sys.dont_write_bytecode
|
|
99
|
+
sys.dont_write_bytecode = True
|
|
100
|
+
for path in CONFIG.dirs.templates.glob('**/*.py'):
|
|
101
|
+
try:
|
|
102
|
+
task_name = path.stem
|
|
103
|
+
module_name = f'secator.tasks.{task_name}'
|
|
104
|
+
|
|
105
|
+
# console.print(f'Importing module {module_name} from {path}')
|
|
106
|
+
spec = importlib.util.spec_from_file_location(module_name, path)
|
|
107
|
+
module = importlib.util.module_from_spec(spec)
|
|
108
|
+
if not spec:
|
|
109
|
+
console.print(f'[bold red]Could not load external module {path.name}: invalid import spec.[/] ({path})')
|
|
110
|
+
continue
|
|
111
|
+
# console.print(f'Adding module "{module_name}" to sys path')
|
|
112
|
+
sys.modules[module_name] = module
|
|
113
|
+
|
|
114
|
+
# console.print(f'Executing module "{module}"')
|
|
115
|
+
spec.loader.exec_module(module)
|
|
116
|
+
|
|
117
|
+
# console.print(f'Checking that {module} contains task {task_name}')
|
|
118
|
+
if not hasattr(module, task_name):
|
|
119
|
+
console.print(f'[bold orange1]Could not load external task "{task_name}" from module {path.name}[/] ({path})')
|
|
120
|
+
continue
|
|
121
|
+
cls = getattr(module, task_name)
|
|
122
|
+
console.print(f'[bold green]Successfully loaded external task "{task_name}"[/] ({path})')
|
|
123
|
+
cls.__external__ = True
|
|
124
|
+
output.append(cls)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
console.print(f'[bold red]Could not load external module {path.name}. Reason: {str(e)}.[/] ({path})')
|
|
127
|
+
sys.dont_write_bytecode = prev_state
|
|
128
|
+
return output
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'Domain',
|
|
3
|
+
'Error',
|
|
4
|
+
'OutputType',
|
|
5
|
+
'Info',
|
|
6
|
+
'Ip',
|
|
7
|
+
'Port',
|
|
8
|
+
'Progress',
|
|
9
|
+
'Record',
|
|
10
|
+
'Stat',
|
|
11
|
+
'State',
|
|
12
|
+
'Subdomain',
|
|
13
|
+
'Url',
|
|
14
|
+
'UrlParam',
|
|
15
|
+
'UserAccount',
|
|
16
|
+
'Vulnerability',
|
|
17
|
+
'Warning',
|
|
18
|
+
]
|
|
19
|
+
from secator.output_types._base import OutputType
|
|
20
|
+
from secator.output_types.progress import Progress
|
|
21
|
+
from secator.output_types.ip import Ip
|
|
22
|
+
from secator.output_types.exploit import Exploit
|
|
23
|
+
from secator.output_types.port import Port
|
|
24
|
+
from secator.output_types.subdomain import Subdomain
|
|
25
|
+
from secator.output_types.tag import Tag
|
|
26
|
+
from secator.output_types.target import Target
|
|
27
|
+
from secator.output_types.url import Url
|
|
28
|
+
from secator.output_types.user_account import UserAccount
|
|
29
|
+
from secator.output_types.vulnerability import Vulnerability
|
|
30
|
+
from secator.output_types.record import Record
|
|
31
|
+
from secator.output_types.certificate import Certificate
|
|
32
|
+
from secator.output_types.info import Info
|
|
33
|
+
from secator.output_types.warning import Warning
|
|
34
|
+
from secator.output_types.error import Error
|
|
35
|
+
from secator.output_types.stat import Stat
|
|
36
|
+
from secator.output_types.state import State
|
|
37
|
+
from secator.output_types.domain import Domain
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
EXECUTION_TYPES = [
|
|
41
|
+
Target, Progress, Info, Warning, Error, State
|
|
42
|
+
]
|
|
43
|
+
STAT_TYPES = [
|
|
44
|
+
Stat
|
|
45
|
+
]
|
|
46
|
+
FINDING_TYPES = [
|
|
47
|
+
Subdomain, Ip, Port, Url, Tag, Exploit, UserAccount, Vulnerability, Certificate, Record, Domain
|
|
48
|
+
]
|
|
49
|
+
OUTPUT_TYPES = FINDING_TYPES + EXECUTION_TYPES + STAT_TYPES
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from dataclasses import _MISSING_TYPE, dataclass, fields
|
|
4
|
+
from secator.definitions import DEBUG
|
|
5
|
+
from secator.rich import console
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class OutputType:
|
|
12
|
+
_table_fields = []
|
|
13
|
+
_sort_by = ()
|
|
14
|
+
|
|
15
|
+
def __str__(self):
|
|
16
|
+
return self.__class__.__name__
|
|
17
|
+
|
|
18
|
+
def __gt__(self, other):
|
|
19
|
+
if not self.__eq__(other):
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
# Point-based system based on number of non-empty extra-data present.
|
|
23
|
+
# In this configuration, a > b if a == b AND a has more non-empty fields than b
|
|
24
|
+
# extra_fields = [f for f in fields(self) if not f.compare]
|
|
25
|
+
# points1 = 0
|
|
26
|
+
# points2 = 0
|
|
27
|
+
# for field in extra_fields:
|
|
28
|
+
# v1 = getattr(self, field.name)
|
|
29
|
+
# v2 = getattr(other, field.name)
|
|
30
|
+
# if v1 and not v2:
|
|
31
|
+
# points1 += 1
|
|
32
|
+
# elif v2 and not v1:
|
|
33
|
+
# points2 += 1
|
|
34
|
+
# if points1 > points2:
|
|
35
|
+
# return True
|
|
36
|
+
|
|
37
|
+
# Timestamp-based system: return newest object
|
|
38
|
+
return self._timestamp > other._timestamp
|
|
39
|
+
|
|
40
|
+
def __ge__(self, other):
|
|
41
|
+
return self == other
|
|
42
|
+
|
|
43
|
+
def __lt__(self, other):
|
|
44
|
+
return other > self
|
|
45
|
+
|
|
46
|
+
def __le__(self, other):
|
|
47
|
+
return self == other
|
|
48
|
+
|
|
49
|
+
def __post_init__(self):
|
|
50
|
+
"""Initialize default fields to their proper types."""
|
|
51
|
+
for field in fields(self):
|
|
52
|
+
default_factory = field.default_factory
|
|
53
|
+
default = field.default
|
|
54
|
+
if getattr(self, field.name) is None:
|
|
55
|
+
if not isinstance(default, _MISSING_TYPE):
|
|
56
|
+
setattr(self, field.name, field.default)
|
|
57
|
+
elif not isinstance(default_factory, _MISSING_TYPE):
|
|
58
|
+
setattr(self, field.name, default_factory())
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def load(cls, item, output_map={}):
|
|
62
|
+
new_item = {}
|
|
63
|
+
|
|
64
|
+
# Check for explicit _type keys
|
|
65
|
+
_type = item.get('_type')
|
|
66
|
+
if _type and _type != cls.get_name():
|
|
67
|
+
raise TypeError(f'Item has different _type set: {_type}')
|
|
68
|
+
|
|
69
|
+
for field in fields(cls):
|
|
70
|
+
key = field.name
|
|
71
|
+
if key in output_map:
|
|
72
|
+
mapped_key = output_map[key]
|
|
73
|
+
if callable(mapped_key):
|
|
74
|
+
try:
|
|
75
|
+
mapped_val = mapped_key(item)
|
|
76
|
+
except Exception as e:
|
|
77
|
+
mapped_val = None
|
|
78
|
+
if DEBUG > 1:
|
|
79
|
+
console.print_exception(show_locals=True)
|
|
80
|
+
raise TypeError(
|
|
81
|
+
f'Fail to transform value for "{key}" using output_map function. Exception: '
|
|
82
|
+
f'{type(e).__name__}: {str(e)}')
|
|
83
|
+
else:
|
|
84
|
+
mapped_val = item.get(mapped_key)
|
|
85
|
+
new_item[key] = mapped_val
|
|
86
|
+
elif key in item:
|
|
87
|
+
new_item[key] = item[key]
|
|
88
|
+
|
|
89
|
+
# All values None, raise an error
|
|
90
|
+
if all(val is None for val in new_item.values()):
|
|
91
|
+
raise TypeError(f'Item does not match {cls} schema')
|
|
92
|
+
|
|
93
|
+
new_item['_type'] = cls.get_name()
|
|
94
|
+
return cls(**new_item)
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def get_name(cls):
|
|
98
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '_', cls.__name__).lower()
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def keys(cls):
|
|
102
|
+
return [f.name for f in fields(cls)]
|
|
103
|
+
|
|
104
|
+
def toDict(self, exclude=[]):
|
|
105
|
+
data = self.__dict__.copy()
|
|
106
|
+
if exclude:
|
|
107
|
+
return {k: v for k, v in data.items() if k not in exclude}
|
|
108
|
+
return data
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from secator.output_types import OutputType
|
|
5
|
+
from secator.utils import rich_to_ansi
|
|
6
|
+
from secator.definitions import CERTIFICATE_STATUS_UNKNOWN
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
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, compare=False)
|
|
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
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
return self.subject_cn
|
|
42
|
+
|
|
43
|
+
def is_expired(self) -> bool:
|
|
44
|
+
if self.not_after:
|
|
45
|
+
return self.not_after < datetime.now()
|
|
46
|
+
return True
|
|
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
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def format_date(date):
|
|
55
|
+
if date:
|
|
56
|
+
return date.strftime("%m/%d/%Y")
|
|
57
|
+
return '?'
|
|
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)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
from secator.definitions import ALIVE, DOMAIN
|
|
6
|
+
from secator.output_types import OutputType
|
|
7
|
+
from secator.utils import rich_to_ansi, format_object
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Domain(OutputType):
|
|
12
|
+
domain: str
|
|
13
|
+
registrar: str = ''
|
|
14
|
+
alive: bool = False
|
|
15
|
+
creation_date: str = ''
|
|
16
|
+
expiration_date: str = ''
|
|
17
|
+
registrant: str = ''
|
|
18
|
+
extra_data: dict = field(default_factory=dict, compare=False)
|
|
19
|
+
_source: str = field(default='', repr=True)
|
|
20
|
+
_type: str = field(default='domain', repr=True)
|
|
21
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
22
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
23
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
24
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
25
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
26
|
+
_related: list = field(default_factory=list, compare=False)
|
|
27
|
+
|
|
28
|
+
_table_fields = [DOMAIN, ALIVE]
|
|
29
|
+
_sort_by = (DOMAIN,)
|
|
30
|
+
|
|
31
|
+
def __str__(self) -> str:
|
|
32
|
+
return self.domain
|
|
33
|
+
|
|
34
|
+
def __repr__(self) -> str:
|
|
35
|
+
s = f'🪪 [bold white]{self.domain}[/]'
|
|
36
|
+
if self.registrant:
|
|
37
|
+
s += rf' \[[bold magenta]{self.registrant}[/]]'
|
|
38
|
+
if self.registrar:
|
|
39
|
+
s += rf' \[[bold blue]{self.registrar}[/]]'
|
|
40
|
+
if self.expiration_date:
|
|
41
|
+
now = datetime.now()
|
|
42
|
+
expiration_date_strptime = datetime.strptime(self.expiration_date, "%Y-%m-%d %H:%M:%S")
|
|
43
|
+
if expiration_date_strptime < now:
|
|
44
|
+
s += rf' \[[bold red]{self.expiration_date}[/]]'
|
|
45
|
+
else:
|
|
46
|
+
s += rf' \[[bold green]{self.expiration_date}[/]]'
|
|
47
|
+
if self.extra_data:
|
|
48
|
+
s += format_object(self.extra_data, 'yellow')
|
|
49
|
+
|
|
50
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import time
|
|
3
|
+
from secator.output_types import OutputType
|
|
4
|
+
from secator.utils import rich_to_ansi, traceback_as_string, rich_escape as _s
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Error(OutputType):
|
|
9
|
+
message: str
|
|
10
|
+
traceback: str = field(default='', compare=False)
|
|
11
|
+
traceback_title: str = field(default='', compare=False)
|
|
12
|
+
_source: str = field(default='', repr=True)
|
|
13
|
+
_type: str = field(default='error', repr=True)
|
|
14
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
15
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
16
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
17
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
18
|
+
_related: list = field(default_factory=list, compare=False)
|
|
19
|
+
|
|
20
|
+
_table_fields = ['message', 'traceback']
|
|
21
|
+
_sort_by = ('_timestamp',)
|
|
22
|
+
|
|
23
|
+
def from_exception(e, **kwargs):
|
|
24
|
+
errtype = type(e).__name__
|
|
25
|
+
if str(e):
|
|
26
|
+
errtype += f': {str(e)}'
|
|
27
|
+
message = kwargs.pop('message', errtype)
|
|
28
|
+
traceback = traceback_as_string(e) if errtype not in ['KeyboardInterrupt', 'GreenletExit'] else ''
|
|
29
|
+
error = Error(message=_s(message), traceback=traceback, **kwargs)
|
|
30
|
+
return error
|
|
31
|
+
|
|
32
|
+
def __str__(self):
|
|
33
|
+
return self.message
|
|
34
|
+
|
|
35
|
+
def __repr__(self):
|
|
36
|
+
s = rf"\[[bold red]ERR[/]] {self.message}"
|
|
37
|
+
if self.traceback:
|
|
38
|
+
traceback_pretty = ' ' + _s(self.traceback).replace('\n', '\n ')
|
|
39
|
+
if self.traceback_title:
|
|
40
|
+
traceback_pretty = f' {self.traceback_title}:\n{traceback_pretty}'
|
|
41
|
+
s += f'\n[dim]{_s(traceback_pretty)}[/]'
|
|
42
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from secator.output_types import OutputType
|
|
4
|
+
from secator.utils import rich_to_ansi, rich_escape as _s, format_object
|
|
5
|
+
from secator.definitions import MATCHED_AT, NAME, ID, EXTRA_DATA, REFERENCE
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Exploit(OutputType):
|
|
10
|
+
name: str
|
|
11
|
+
provider: str
|
|
12
|
+
id: str
|
|
13
|
+
matched_at: str = ''
|
|
14
|
+
ip: str = ''
|
|
15
|
+
confidence: str = 'low'
|
|
16
|
+
reference: str = field(default='', repr=True, compare=False)
|
|
17
|
+
cves: list = field(default_factory=list, compare=False)
|
|
18
|
+
tags: list = field(default_factory=list, compare=False)
|
|
19
|
+
extra_data: dict = field(default_factory=dict, compare=False)
|
|
20
|
+
_source: str = field(default='', repr=True, compare=False)
|
|
21
|
+
_type: str = field(default='exploit', repr=True)
|
|
22
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
23
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
24
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
25
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
26
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
27
|
+
_related: list = field(default_factory=list, compare=False)
|
|
28
|
+
|
|
29
|
+
_table_fields = [
|
|
30
|
+
MATCHED_AT,
|
|
31
|
+
NAME,
|
|
32
|
+
ID,
|
|
33
|
+
EXTRA_DATA,
|
|
34
|
+
REFERENCE
|
|
35
|
+
]
|
|
36
|
+
_sort_by = ('matched_at', 'name')
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return self.name
|
|
40
|
+
|
|
41
|
+
def __repr__(self):
|
|
42
|
+
s = rf'[bold red]⍼[/] \[[bold red]{self.name}'
|
|
43
|
+
if self.reference:
|
|
44
|
+
s += f' [link={_s(self.reference)}]🡕[/link]'
|
|
45
|
+
s += '[/]]'
|
|
46
|
+
if self.matched_at:
|
|
47
|
+
s += f' {_s(self.matched_at)}'
|
|
48
|
+
if self.cves:
|
|
49
|
+
cves_str = ', '.join(self.cves)
|
|
50
|
+
s += rf' \[[red]{cves_str}[/]]'
|
|
51
|
+
if self.tags:
|
|
52
|
+
tags_str = ', '.join(self.tags)
|
|
53
|
+
s += rf' \[[cyan]{tags_str}[/]]'
|
|
54
|
+
if self.extra_data:
|
|
55
|
+
s += format_object(self.extra_data, 'yellow')
|
|
56
|
+
if self.confidence == 'low':
|
|
57
|
+
s = f'[dim]{s}[/]'
|
|
58
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
import time
|
|
3
|
+
from secator.output_types import OutputType
|
|
4
|
+
from secator.utils import rich_to_ansi
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Info(OutputType):
|
|
9
|
+
message: str
|
|
10
|
+
task_id: str = field(default='', compare=False)
|
|
11
|
+
_source: str = field(default='', repr=True)
|
|
12
|
+
_type: str = field(default='info', repr=True)
|
|
13
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
14
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
15
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
16
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
17
|
+
_related: list = field(default_factory=list, compare=False)
|
|
18
|
+
|
|
19
|
+
_table_fields = ['message', 'task_id']
|
|
20
|
+
_sort_by = ('_timestamp',)
|
|
21
|
+
|
|
22
|
+
def __repr__(self):
|
|
23
|
+
s = rf"\[[blue]INF[/]] {self.message}"
|
|
24
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
from secator.definitions import ALIVE, IP
|
|
6
|
+
from secator.output_types import OutputType
|
|
7
|
+
from secator.utils import rich_to_ansi, format_object
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IpProtocol(str, Enum):
|
|
11
|
+
IPv6 = 'IPv6'
|
|
12
|
+
IPv4 = 'IPv4'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Ip(OutputType):
|
|
17
|
+
ip: str
|
|
18
|
+
host: str = field(default='', repr=True, compare=False)
|
|
19
|
+
alive: bool = False
|
|
20
|
+
protocol: str = field(default=IpProtocol.IPv4)
|
|
21
|
+
extra_data: dict = field(default_factory=dict, compare=False)
|
|
22
|
+
_source: str = field(default='', repr=True, compare=False)
|
|
23
|
+
_type: str = field(default='ip', repr=True)
|
|
24
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
25
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
26
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
27
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
28
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
29
|
+
_related: list = field(default_factory=list, compare=False)
|
|
30
|
+
|
|
31
|
+
_table_fields = [IP, ALIVE]
|
|
32
|
+
_sort_by = (IP,)
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
return self.ip
|
|
36
|
+
|
|
37
|
+
def __repr__(self) -> str:
|
|
38
|
+
s = f'💻 [bold white]{self.ip}[/]'
|
|
39
|
+
if self.host and self.host != self.ip:
|
|
40
|
+
s += rf' \[[bold magenta]{self.host}[/]]'
|
|
41
|
+
if self.alive:
|
|
42
|
+
s += r' \[[bold green]alive[/]]'
|
|
43
|
+
if self.extra_data:
|
|
44
|
+
s += format_object(self.extra_data, 'yellow')
|
|
45
|
+
if not self.alive:
|
|
46
|
+
s = f'[dim]{s}[/]'
|
|
47
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from secator.definitions import CPES, EXTRA_DATA, HOST, IP, PORT
|
|
5
|
+
from secator.output_types import OutputType
|
|
6
|
+
from secator.utils import rich_to_ansi, format_object
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Port(OutputType):
|
|
11
|
+
port: int
|
|
12
|
+
ip: str
|
|
13
|
+
state: str = 'UNKNOWN'
|
|
14
|
+
service_name: str = field(default='', compare=False)
|
|
15
|
+
cpes: list = field(default_factory=list, compare=False)
|
|
16
|
+
host: str = field(default='', repr=True, compare=False)
|
|
17
|
+
protocol: str = field(default='tcp', repr=True, compare=False)
|
|
18
|
+
extra_data: dict = field(default_factory=dict, compare=False)
|
|
19
|
+
confidence: str = field(default='low', repr=False, compare=False)
|
|
20
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
21
|
+
_source: str = field(default='', repr=True, compare=False)
|
|
22
|
+
_type: str = field(default='port', repr=True)
|
|
23
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
24
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
25
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
26
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
27
|
+
_related: list = field(default_factory=list, compare=False)
|
|
28
|
+
|
|
29
|
+
_table_fields = [IP, PORT, HOST, CPES, EXTRA_DATA]
|
|
30
|
+
_sort_by = (PORT, IP)
|
|
31
|
+
|
|
32
|
+
def __gt__(self, other):
|
|
33
|
+
# favor nmap over other port detection tools
|
|
34
|
+
if self._source == 'nmap' and other._source != 'nmap':
|
|
35
|
+
return True
|
|
36
|
+
return super().__gt__(other)
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return f'{self.host}:{self.port}'
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
s = f'🔓 {self.ip}:[bold red]{self.port:<4}[/] [bold yellow]{self.state.upper()}[/]'
|
|
43
|
+
if self.protocol != 'TCP':
|
|
44
|
+
s += rf' \[[yellow3]{self.protocol}[/]]'
|
|
45
|
+
if self.service_name:
|
|
46
|
+
conf = ''
|
|
47
|
+
if self.confidence == 'low':
|
|
48
|
+
conf = '[bold orange3]?[/]'
|
|
49
|
+
s += rf' \[[bold purple]{self.service_name}{conf}[/]]'
|
|
50
|
+
if self.host and self.host != self.ip:
|
|
51
|
+
s += rf' \[[cyan]{self.host}[/]]'
|
|
52
|
+
if self.extra_data:
|
|
53
|
+
skip_keys = ['name', 'servicefp', 'method', 'service_name', 'product', 'version', 'conf']
|
|
54
|
+
s += format_object(self.extra_data, 'yellow', skip_keys=skip_keys)
|
|
55
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from secator.output_types import OutputType
|
|
5
|
+
from secator.utils import rich_to_ansi, format_object
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Progress(OutputType):
|
|
10
|
+
percent: int = 0
|
|
11
|
+
extra_data: dict = field(default_factory=dict)
|
|
12
|
+
_source: str = field(default='', repr=True, compare=False)
|
|
13
|
+
_type: str = field(default='progress', repr=True)
|
|
14
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
15
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
16
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
17
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
18
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
19
|
+
_related: list = field(default_factory=list, compare=False)
|
|
20
|
+
|
|
21
|
+
_table_fields = ['percent']
|
|
22
|
+
_sort_by = ('percent',)
|
|
23
|
+
|
|
24
|
+
def __post_init__(self):
|
|
25
|
+
super().__post_init__()
|
|
26
|
+
if not 0 <= self.percent <= 100:
|
|
27
|
+
self.percent = 0
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
return f'{self.percent}%'
|
|
31
|
+
|
|
32
|
+
def __repr__(self) -> str:
|
|
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}[/]'
|
|
36
|
+
return rich_to_ansi(s)
|