secator 0.1.0__py2.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/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +421 -0
- secator/cli.py +927 -0
- secator/config.py +137 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +7 -0
- secator/configs/profiles/default.yaml +9 -0
- secator/configs/profiles/stealth.yaml +7 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +18 -0
- secator/configs/scans/host.yaml +14 -0
- secator/configs/scans/network.yaml +17 -0
- secator/configs/scans/subdomain.yaml +8 -0
- secator/configs/scans/url.yaml +12 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +28 -0
- secator/configs/workflows/code_scan.yaml +11 -0
- secator/configs/workflows/host_recon.yaml +41 -0
- secator/configs/workflows/port_scan.yaml +34 -0
- secator/configs/workflows/subdomain_recon.yaml +33 -0
- secator/configs/workflows/url_crawl.yaml +29 -0
- secator/configs/workflows/url_dirsearch.yaml +29 -0
- secator/configs/workflows/url_fuzz.yaml +35 -0
- secator/configs/workflows/url_nuclei.yaml +11 -0
- secator/configs/workflows/url_vuln.yaml +55 -0
- secator/configs/workflows/user_hunt.yaml +10 -0
- secator/configs/workflows/wordpress.yaml +14 -0
- secator/decorators.py +346 -0
- secator/definitions.py +183 -0
- secator/exporters/__init__.py +12 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/csv.py +29 -0
- secator/exporters/gdrive.py +118 -0
- secator/exporters/json.py +14 -0
- secator/exporters/table.py +7 -0
- secator/exporters/txt.py +24 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/mongodb.py +212 -0
- secator/output_types/__init__.py +24 -0
- secator/output_types/_base.py +95 -0
- secator/output_types/exploit.py +50 -0
- secator/output_types/ip.py +33 -0
- secator/output_types/port.py +45 -0
- secator/output_types/progress.py +35 -0
- secator/output_types/record.py +34 -0
- secator/output_types/subdomain.py +42 -0
- secator/output_types/tag.py +46 -0
- secator/output_types/target.py +30 -0
- secator/output_types/url.py +76 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +97 -0
- secator/report.py +95 -0
- secator/rich.py +123 -0
- secator/runners/__init__.py +12 -0
- secator/runners/_base.py +873 -0
- secator/runners/_helpers.py +154 -0
- secator/runners/command.py +674 -0
- secator/runners/scan.py +67 -0
- secator/runners/task.py +107 -0
- secator/runners/workflow.py +137 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +33 -0
- secator/serializers/json.py +15 -0
- secator/serializers/regex.py +17 -0
- secator/tasks/__init__.py +10 -0
- secator/tasks/_categories.py +304 -0
- secator/tasks/cariddi.py +102 -0
- secator/tasks/dalfox.py +66 -0
- secator/tasks/dirsearch.py +88 -0
- secator/tasks/dnsx.py +56 -0
- secator/tasks/dnsxbrute.py +34 -0
- secator/tasks/feroxbuster.py +89 -0
- secator/tasks/ffuf.py +85 -0
- secator/tasks/fping.py +44 -0
- secator/tasks/gau.py +43 -0
- secator/tasks/gf.py +34 -0
- secator/tasks/gospider.py +71 -0
- secator/tasks/grype.py +78 -0
- secator/tasks/h8mail.py +80 -0
- secator/tasks/httpx.py +104 -0
- secator/tasks/katana.py +128 -0
- secator/tasks/maigret.py +78 -0
- secator/tasks/mapcidr.py +32 -0
- secator/tasks/msfconsole.py +176 -0
- secator/tasks/naabu.py +52 -0
- secator/tasks/nmap.py +341 -0
- secator/tasks/nuclei.py +97 -0
- secator/tasks/searchsploit.py +53 -0
- secator/tasks/subfinder.py +40 -0
- secator/tasks/wpscan.py +177 -0
- secator/utils.py +404 -0
- secator/utils_test.py +183 -0
- secator-0.1.0.dist-info/METADATA +379 -0
- secator-0.1.0.dist-info/RECORD +99 -0
- secator-0.1.0.dist-info/WHEEL +5 -0
- secator-0.1.0.dist-info/entry_points.txt +2 -0
- secator-0.1.0.dist-info/licenses/LICENSE +60 -0
|
@@ -0,0 +1,30 @@
|
|
|
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
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Target(OutputType):
|
|
10
|
+
name: str
|
|
11
|
+
_source: str = field(default='', repr=True)
|
|
12
|
+
_type: str = field(default='target', 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
|
+
_tagged: bool = field(default=False, 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 = [
|
|
21
|
+
'name',
|
|
22
|
+
]
|
|
23
|
+
_sort_by = ('name',)
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return self.name
|
|
27
|
+
|
|
28
|
+
def __repr__(self):
|
|
29
|
+
s = f'🎯 {self.name}'
|
|
30
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from secator.definitions import (CONTENT_LENGTH, CONTENT_TYPE, STATUS_CODE,
|
|
5
|
+
TECH, TIME, TITLE, URL, WEBSERVER)
|
|
6
|
+
from secator.output_types import OutputType
|
|
7
|
+
from secator.utils import rich_to_ansi
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Url(OutputType):
|
|
12
|
+
url: str
|
|
13
|
+
host: str = field(default='', compare=False)
|
|
14
|
+
status_code: int = field(default=0, compare=False)
|
|
15
|
+
title: str = field(default='', compare=False)
|
|
16
|
+
webserver: str = field(default='', compare=False)
|
|
17
|
+
tech: list = field(default_factory=list, compare=False)
|
|
18
|
+
content_type: str = field(default='', compare=False)
|
|
19
|
+
content_length: int = field(default=0, compare=False)
|
|
20
|
+
time: str = field(default='', compare=False)
|
|
21
|
+
method: str = field(default='', compare=False)
|
|
22
|
+
words: int = field(default=0, compare=False)
|
|
23
|
+
lines: int = field(default=0, compare=False)
|
|
24
|
+
screenshot_path: str = field(default='', compare=False)
|
|
25
|
+
stored_response_path: str = field(default='', compare=False)
|
|
26
|
+
_source: str = field(default='', repr=True, compare=False)
|
|
27
|
+
_type: str = field(default='url', repr=True)
|
|
28
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
29
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
30
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
31
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
32
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
33
|
+
_related: list = field(default_factory=list, compare=False)
|
|
34
|
+
|
|
35
|
+
_table_fields = [
|
|
36
|
+
URL,
|
|
37
|
+
STATUS_CODE,
|
|
38
|
+
TITLE,
|
|
39
|
+
WEBSERVER,
|
|
40
|
+
TECH,
|
|
41
|
+
CONTENT_TYPE,
|
|
42
|
+
CONTENT_LENGTH,
|
|
43
|
+
TIME
|
|
44
|
+
]
|
|
45
|
+
_sort_by = (URL,)
|
|
46
|
+
|
|
47
|
+
def __gt__(self, other):
|
|
48
|
+
# favor httpx over other url info tools
|
|
49
|
+
if self._source == 'httpx' and other._source != 'httpx':
|
|
50
|
+
return True
|
|
51
|
+
return super().__gt__(other)
|
|
52
|
+
|
|
53
|
+
def __str__(self):
|
|
54
|
+
return self.url
|
|
55
|
+
|
|
56
|
+
def __repr__(self):
|
|
57
|
+
s = f'🔗 [white]{self.url}'
|
|
58
|
+
if self.status_code and self.status_code != 0:
|
|
59
|
+
if self.status_code < 400:
|
|
60
|
+
s += f' \[[green]{self.status_code}[/]]'
|
|
61
|
+
else:
|
|
62
|
+
s += f' \[[red]{self.status_code}[/]]'
|
|
63
|
+
if self.title:
|
|
64
|
+
s += f' \[[green]{self.title}[/]]'
|
|
65
|
+
if self.webserver:
|
|
66
|
+
s += f' \[[magenta]{self.webserver}[/]]'
|
|
67
|
+
if self.tech:
|
|
68
|
+
techs_str = ', '.join([f'[magenta]{tech}[/]' for tech in self.tech])
|
|
69
|
+
s += f' [{techs_str}]'
|
|
70
|
+
if self.content_type:
|
|
71
|
+
s += f' \[[magenta]{self.content_type}[/]]'
|
|
72
|
+
if self.content_length:
|
|
73
|
+
s += f' \[[magenta]{self.content_length}[/]]'
|
|
74
|
+
if self.screenshot_path:
|
|
75
|
+
s += f' \[[magenta]{self.screenshot_path}[/]]'
|
|
76
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
|
|
4
|
+
from secator.definitions import SITE_NAME, URL, USERNAME
|
|
5
|
+
from secator.output_types import OutputType
|
|
6
|
+
from secator.utils import rich_to_ansi
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class UserAccount(OutputType):
|
|
11
|
+
username: str
|
|
12
|
+
url: str = ''
|
|
13
|
+
email: str = ''
|
|
14
|
+
site_name: str = ''
|
|
15
|
+
extra_data: dict = field(default_factory=dict, compare=False)
|
|
16
|
+
_source: str = field(default='', repr=True)
|
|
17
|
+
_type: str = field(default='user_account', repr=True)
|
|
18
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
19
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
20
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
21
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
22
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
23
|
+
_related: list = field(default_factory=list, compare=False)
|
|
24
|
+
|
|
25
|
+
_table_fields = [SITE_NAME, USERNAME, URL]
|
|
26
|
+
_sort_by = (URL, USERNAME)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
return self.url
|
|
30
|
+
|
|
31
|
+
def __repr__(self) -> str:
|
|
32
|
+
s = f'👤 [green]{self.username}[/]'
|
|
33
|
+
if self.email:
|
|
34
|
+
s += f' \[[bold yellow]{self.email}[/]]'
|
|
35
|
+
if self.site_name:
|
|
36
|
+
s += f' \[[bold blue]{self.site_name}[/]]'
|
|
37
|
+
if self.url:
|
|
38
|
+
s += f' \[[white]{self.url}[/]]'
|
|
39
|
+
if self.extra_data:
|
|
40
|
+
s += ' \[[bold yellow]' + ', '.join(f'{k}:{v}' for k, v in self.extra_data.items()) + '[/]]'
|
|
41
|
+
return rich_to_ansi(s)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
from secator.definitions import (CONFIDENCE, CVSS_SCORE, EXTRA_DATA, ID,
|
|
6
|
+
MATCHED_AT, NAME, REFERENCE, SEVERITY, TAGS)
|
|
7
|
+
from secator.output_types import OutputType
|
|
8
|
+
from secator.utils import rich_to_ansi
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Vulnerability(OutputType):
|
|
13
|
+
name: str
|
|
14
|
+
provider: str = ''
|
|
15
|
+
id: str = ''
|
|
16
|
+
matched_at: str = ''
|
|
17
|
+
ip: str = field(default='', compare=False)
|
|
18
|
+
confidence: int = 'low'
|
|
19
|
+
severity: str = 'unknown'
|
|
20
|
+
cvss_score: float = 0
|
|
21
|
+
tags: List[str] = field(default_factory=list)
|
|
22
|
+
extra_data: dict = field(default_factory=dict, compare=False)
|
|
23
|
+
description: str = field(default='', compare=False)
|
|
24
|
+
references: List[str] = field(default_factory=list, compare=False)
|
|
25
|
+
reference: str = field(default='', compare=False)
|
|
26
|
+
confidence_nb: int = 0
|
|
27
|
+
severity_nb: int = 0
|
|
28
|
+
_source: str = field(default='', repr=True)
|
|
29
|
+
_type: str = field(default='vulnerability', repr=True)
|
|
30
|
+
_timestamp: int = field(default_factory=lambda: time.time(), compare=False)
|
|
31
|
+
_uuid: str = field(default='', repr=True, compare=False)
|
|
32
|
+
_context: dict = field(default_factory=dict, repr=True, compare=False)
|
|
33
|
+
_tagged: bool = field(default=False, repr=True, compare=False)
|
|
34
|
+
_duplicate: bool = field(default=False, repr=True, compare=False)
|
|
35
|
+
_related: list = field(default_factory=list, compare=False)
|
|
36
|
+
|
|
37
|
+
_table_fields = [
|
|
38
|
+
MATCHED_AT,
|
|
39
|
+
SEVERITY,
|
|
40
|
+
CONFIDENCE,
|
|
41
|
+
NAME,
|
|
42
|
+
ID,
|
|
43
|
+
CVSS_SCORE,
|
|
44
|
+
TAGS,
|
|
45
|
+
EXTRA_DATA,
|
|
46
|
+
REFERENCE
|
|
47
|
+
]
|
|
48
|
+
_sort_by = ('confidence_nb', 'severity_nb', 'matched_at', 'cvss_score')
|
|
49
|
+
|
|
50
|
+
def __post_init__(self):
|
|
51
|
+
super().__post_init__()
|
|
52
|
+
severity_map = {
|
|
53
|
+
'critical': 0,
|
|
54
|
+
'high': 1,
|
|
55
|
+
'medium': 2,
|
|
56
|
+
'low': 3,
|
|
57
|
+
'info': 4,
|
|
58
|
+
'unknown': 5,
|
|
59
|
+
None: 6
|
|
60
|
+
}
|
|
61
|
+
self.severity_nb = severity_map[self.severity]
|
|
62
|
+
self.confidence_nb = severity_map[self.confidence]
|
|
63
|
+
if len(self.references) > 0:
|
|
64
|
+
self.reference = self.references[0]
|
|
65
|
+
|
|
66
|
+
def __repr__(self):
|
|
67
|
+
data = self.extra_data
|
|
68
|
+
if 'data' in data and isinstance(data['data'], list):
|
|
69
|
+
data = ','.join(data['data'])
|
|
70
|
+
elif isinstance(data, dict):
|
|
71
|
+
data = ', '.join([f'{k}:{v}' for k, v in data.items()])
|
|
72
|
+
tags = self.tags
|
|
73
|
+
colors = {
|
|
74
|
+
'critical': 'bold red',
|
|
75
|
+
'high': 'red',
|
|
76
|
+
'medium': 'yellow',
|
|
77
|
+
'low': 'green',
|
|
78
|
+
'info': 'magenta',
|
|
79
|
+
'unknown': 'dim magenta'
|
|
80
|
+
}
|
|
81
|
+
c = colors[self.severity]
|
|
82
|
+
s = f'🚨 \[[green]{self.name} [link={self.reference}]🡕[/link][/]] \[[{c}]{self.severity}[/]] {self.matched_at}'
|
|
83
|
+
if tags:
|
|
84
|
+
tags_str = ','.join(tags)
|
|
85
|
+
s += f' \[[cyan]{tags_str}[/]]'
|
|
86
|
+
if data:
|
|
87
|
+
s += f' \[[yellow]{str(data)}[/]]'
|
|
88
|
+
return rich_to_ansi(s)
|
|
89
|
+
|
|
90
|
+
# def __gt__(self, other):
|
|
91
|
+
# # favor httpx over other url info tools
|
|
92
|
+
# if self._source == 'httpx' and other._source != 'httpx':
|
|
93
|
+
# return True
|
|
94
|
+
# return super().__gt__(other)
|
|
95
|
+
|
|
96
|
+
def __str__(self):
|
|
97
|
+
return self.matched_at + ' -> ' + self.name
|
secator/report.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
|
|
3
|
+
from secator.output_types import OUTPUT_TYPES, OutputType
|
|
4
|
+
from secator.utils import merge_opts, get_file_timestamp, print_results_table
|
|
5
|
+
from secator.rich import console
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# TODO: initialize from data, not from runner
|
|
9
|
+
class Report:
|
|
10
|
+
"""Report class.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
runner (secator.runners.Runner): Runner instance.
|
|
14
|
+
title (str): Report title.
|
|
15
|
+
exporters (list): List of exporter classes.
|
|
16
|
+
"""
|
|
17
|
+
def __init__(self, runner, title=None, exporters=[]):
|
|
18
|
+
self.title = title or f'{runner.__class__.__name__.lower()}_{runner.config.name}'
|
|
19
|
+
self.runner = runner
|
|
20
|
+
self.timestamp = get_file_timestamp()
|
|
21
|
+
self.exporters = exporters
|
|
22
|
+
self.workspace_name = runner.workspace_name
|
|
23
|
+
self.output_folder = runner.reports_folder
|
|
24
|
+
|
|
25
|
+
def as_table(self):
|
|
26
|
+
print_results_table(self.results, self.title)
|
|
27
|
+
|
|
28
|
+
def send(self):
|
|
29
|
+
for report_cls in self.exporters:
|
|
30
|
+
try:
|
|
31
|
+
report_cls(self).send()
|
|
32
|
+
except Exception as e:
|
|
33
|
+
console.print(
|
|
34
|
+
f'Could not create exporter {report_cls.__name__} for {self.__class__.__name__}: {str(e)}',
|
|
35
|
+
style='bold red')
|
|
36
|
+
|
|
37
|
+
def build(self):
|
|
38
|
+
# Trim options
|
|
39
|
+
from secator.decorators import DEFAULT_CLI_OPTIONS
|
|
40
|
+
opts = merge_opts(self.runner.config.options, self.runner.run_opts)
|
|
41
|
+
opts = {
|
|
42
|
+
k: v for k, v in opts.items()
|
|
43
|
+
if k not in DEFAULT_CLI_OPTIONS
|
|
44
|
+
and not k.startswith('print_')
|
|
45
|
+
and v is not None
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Prepare report structure
|
|
49
|
+
data = {
|
|
50
|
+
'info': {
|
|
51
|
+
'title': self.title,
|
|
52
|
+
'runner': self.runner.__class__.__name__,
|
|
53
|
+
'name': self.runner.config.name,
|
|
54
|
+
'targets': self.runner.targets,
|
|
55
|
+
'total_time': str(self.runner.elapsed),
|
|
56
|
+
'total_human': self.runner.elapsed_human,
|
|
57
|
+
'opts': opts,
|
|
58
|
+
},
|
|
59
|
+
'results': {},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Fill report
|
|
63
|
+
for output_type in OUTPUT_TYPES:
|
|
64
|
+
if output_type.__name__ == 'Progress':
|
|
65
|
+
continue
|
|
66
|
+
output_name = output_type.get_name()
|
|
67
|
+
sort_by, _ = get_table_fields(output_type)
|
|
68
|
+
items = [
|
|
69
|
+
item for item in self.runner.results
|
|
70
|
+
if isinstance(item, OutputType) and item._type == output_name and not item._duplicate
|
|
71
|
+
]
|
|
72
|
+
if items:
|
|
73
|
+
if sort_by and all(sort_by):
|
|
74
|
+
items = sorted(items, key=operator.attrgetter(*sort_by))
|
|
75
|
+
data['results'][output_name] = items
|
|
76
|
+
|
|
77
|
+
# Save data
|
|
78
|
+
self.data = data
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_table_fields(output_type):
|
|
82
|
+
"""Get output fields and sort fields based on output type.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
output_type (str): Output type.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
tuple: Tuple of sort_by (tuple), output_fields (list).
|
|
89
|
+
"""
|
|
90
|
+
sort_by = ()
|
|
91
|
+
output_fields = []
|
|
92
|
+
if output_type in OUTPUT_TYPES:
|
|
93
|
+
sort_by = output_type._sort_by
|
|
94
|
+
output_fields = output_type._table_fields
|
|
95
|
+
return sort_by, output_fields
|
secator/rich.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import operator
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import rich_click
|
|
5
|
+
import yaml
|
|
6
|
+
from rich import box
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.traceback import install
|
|
10
|
+
|
|
11
|
+
from secator.definitions import DEBUG, RECORD
|
|
12
|
+
|
|
13
|
+
console = Console(stderr=True, record=RECORD, color_system='truecolor')
|
|
14
|
+
console_stdout = Console(record=True)
|
|
15
|
+
# handler = RichHandler(rich_tracebacks=True) # TODO: add logging handler
|
|
16
|
+
install(show_locals=DEBUG > 2, suppress=[click, rich_click])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def criticity_to_color(value):
|
|
20
|
+
if value == 'critical':
|
|
21
|
+
value = f'[bold red3]{value.upper()}[/]'
|
|
22
|
+
elif value == 'high':
|
|
23
|
+
value = f'[bold orange_red1]{value.upper()}[/]'
|
|
24
|
+
elif value == 'medium':
|
|
25
|
+
value = f'[bold dark_orange]{value.upper()}[/]'
|
|
26
|
+
elif value == 'low':
|
|
27
|
+
value = f'[bold yellow1]{value.upper()}[/]'
|
|
28
|
+
elif value == 'info':
|
|
29
|
+
value = f'[bold green]{value.upper()}[/]'
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def status_to_color(value):
|
|
34
|
+
value = int(value) if value else None
|
|
35
|
+
if value is None:
|
|
36
|
+
return value
|
|
37
|
+
if value < 400:
|
|
38
|
+
value = f'[bold green]{value}[/]'
|
|
39
|
+
elif value in [400, 499]:
|
|
40
|
+
value = f'[bold dark_orange]{value}[/]'
|
|
41
|
+
elif value >= 500:
|
|
42
|
+
value = f'[bold red3]{value}[/]'
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
FORMATTERS = {
|
|
47
|
+
'confidence': criticity_to_color,
|
|
48
|
+
'severity': criticity_to_color,
|
|
49
|
+
'cvss_score': lambda score: '' if score == -1 else f'[bold cyan]{score}[/]',
|
|
50
|
+
'port': lambda port: f'[bold cyan]{port}[/]',
|
|
51
|
+
'url': lambda host: f'[bold underline blue]{host}[/]',
|
|
52
|
+
'ip': lambda ip: f'[bold yellow]{ip}[/]',
|
|
53
|
+
'status_code': status_to_color,
|
|
54
|
+
'reference': lambda reference: f'[link={reference}]🡕[/]',
|
|
55
|
+
'_source': lambda source: f'[bold gold3]{source}[/]'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def build_table(items, output_fields=[], exclude_fields=[], sort_by=None):
|
|
60
|
+
"""Build rich table.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
items (list): List of items.
|
|
64
|
+
output_fields (list, Optional): List of fields to add.
|
|
65
|
+
exclude_fields (list, Optional): List of fields to exclude.
|
|
66
|
+
sort_by (tuple, Optional): Tuple of sort_by keys.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
rich.table.Table: rich table.
|
|
70
|
+
"""
|
|
71
|
+
# Sort items by one or multiple fields
|
|
72
|
+
if sort_by and all(sort_by):
|
|
73
|
+
items = sorted(items, key=operator.attrgetter(*sort_by))
|
|
74
|
+
|
|
75
|
+
# Create rich table
|
|
76
|
+
box_style = box.DOUBLE if RECORD else box.ROUNDED
|
|
77
|
+
table = Table(show_lines=True, box=box_style)
|
|
78
|
+
|
|
79
|
+
# Get table schema if any, default to first item keys
|
|
80
|
+
keys = output_fields
|
|
81
|
+
|
|
82
|
+
# List of fields to exclude
|
|
83
|
+
keys = [k for k in keys if k not in exclude_fields]
|
|
84
|
+
|
|
85
|
+
# Remove meta fields not needed in output
|
|
86
|
+
if '_cls' in keys:
|
|
87
|
+
keys.remove('_cls')
|
|
88
|
+
if '_type' in keys:
|
|
89
|
+
keys.remove('_type')
|
|
90
|
+
if '_uuid' in keys:
|
|
91
|
+
keys.remove('_uuid')
|
|
92
|
+
|
|
93
|
+
# Add _source field
|
|
94
|
+
if '_source' not in keys:
|
|
95
|
+
keys.append('_source')
|
|
96
|
+
|
|
97
|
+
# Create table columns
|
|
98
|
+
for key in keys:
|
|
99
|
+
key_str = key
|
|
100
|
+
if not key.startswith('_'):
|
|
101
|
+
key_str = ' '.join(key.split('_')).upper()
|
|
102
|
+
no_wrap = key in ['url', 'reference', 'references', 'matched_at']
|
|
103
|
+
overflow = None if no_wrap else 'fold'
|
|
104
|
+
table.add_column(
|
|
105
|
+
key_str,
|
|
106
|
+
overflow=overflow,
|
|
107
|
+
min_width=10,
|
|
108
|
+
no_wrap=no_wrap,
|
|
109
|
+
header_style='bold blue')
|
|
110
|
+
|
|
111
|
+
# Create table rows
|
|
112
|
+
for item in items:
|
|
113
|
+
values = []
|
|
114
|
+
for key in keys:
|
|
115
|
+
value = getattr(item, key)
|
|
116
|
+
value = FORMATTERS.get(key, lambda x: x)(value)
|
|
117
|
+
if isinstance(value, dict) or isinstance(value, list):
|
|
118
|
+
value = yaml.dump(value)
|
|
119
|
+
elif isinstance(value, int) or isinstance(value, float):
|
|
120
|
+
value = str(value)
|
|
121
|
+
values.append(value)
|
|
122
|
+
table.add_row(*values)
|
|
123
|
+
return table
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
__all__ = [
|
|
2
|
+
'Runner',
|
|
3
|
+
'Command',
|
|
4
|
+
'Task',
|
|
5
|
+
'Workflow',
|
|
6
|
+
'Scan',
|
|
7
|
+
]
|
|
8
|
+
from secator.runners._base import Runner
|
|
9
|
+
from secator.runners.command import Command
|
|
10
|
+
from secator.runners.task import Task
|
|
11
|
+
from secator.runners.scan import Scan
|
|
12
|
+
from secator.runners.workflow import Workflow
|