secator 0.10.1a11__py3-none-any.whl → 0.11.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 +10 -5
- secator/celery_signals.py +2 -11
- secator/cli.py +153 -46
- secator/configs/workflows/url_params_fuzz.yaml +23 -0
- secator/configs/workflows/wordpress.yaml +4 -1
- secator/decorators.py +10 -5
- secator/definitions.py +3 -0
- secator/installer.py +46 -32
- secator/output_types/__init__.py +2 -1
- secator/output_types/certificate.py +78 -0
- secator/output_types/user_account.py +1 -1
- secator/report.py +1 -1
- secator/rich.py +1 -1
- secator/runners/_base.py +14 -6
- secator/runners/_helpers.py +4 -3
- secator/runners/command.py +81 -21
- secator/runners/scan.py +5 -3
- secator/runners/workflow.py +22 -4
- secator/tasks/_categories.py +6 -1
- secator/tasks/arjun.py +82 -0
- secator/tasks/dalfox.py +1 -1
- secator/tasks/ffuf.py +2 -1
- secator/tasks/gitleaks.py +76 -0
- secator/tasks/mapcidr.py +1 -1
- secator/tasks/naabu.py +7 -1
- secator/tasks/nmap.py +29 -29
- secator/tasks/subfinder.py +1 -1
- secator/tasks/testssl.py +274 -0
- secator/tasks/trivy.py +95 -0
- secator/tasks/wafw00f.py +83 -0
- secator/tasks/wpprobe.py +94 -0
- secator/template.py +49 -67
- secator/utils.py +13 -5
- secator/utils_test.py +26 -8
- {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/METADATA +1 -1
- {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/RECORD +39 -31
- {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/WHEEL +0 -0
- {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/entry_points.txt +0 -0
- {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from secator.config import CONFIG
|
|
6
|
+
from secator.decorators import task
|
|
7
|
+
from secator.runners import Command
|
|
8
|
+
from secator.definitions import (OUTPUT_PATH)
|
|
9
|
+
from secator.utils import caml_to_snake
|
|
10
|
+
from secator.output_types import Tag, Info, Error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@task()
|
|
14
|
+
class gitleaks(Command):
|
|
15
|
+
"""Tool for detecting secrets like passwords, API keys, and tokens in git repos, files, and stdin."""
|
|
16
|
+
cmd = 'gitleaks'
|
|
17
|
+
input_flag = None
|
|
18
|
+
json_flag = '-f json'
|
|
19
|
+
opt_prefix = '--'
|
|
20
|
+
opts = {
|
|
21
|
+
'ignore_path': {'type': str, 'help': 'Path to .gitleaksignore file or folder containing one'},
|
|
22
|
+
'mode': {'type': click.Choice(['git', 'dir']), 'default': 'dir', 'help': 'Gitleaks mode', 'internal': True, 'display': True}, # noqa: E501
|
|
23
|
+
'config': {'type': str, 'short': 'config', 'help': 'Gitleaks config file path'}
|
|
24
|
+
}
|
|
25
|
+
opt_key_map = {
|
|
26
|
+
"ignore_path": "gitleaks-ignore-path"
|
|
27
|
+
}
|
|
28
|
+
input_type = "folder"
|
|
29
|
+
output_types = [Tag]
|
|
30
|
+
output_map = {
|
|
31
|
+
Tag: {
|
|
32
|
+
'name': 'RuleID',
|
|
33
|
+
'match': lambda x: f'{x["File"]}:{x["StartLine"]}:{x["StartColumn"]}',
|
|
34
|
+
'extra_data': lambda x: {caml_to_snake(k): v for k, v in x.items() if k not in ['RuleID', 'File']}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
install_pre = {'*': ['git', 'make']}
|
|
38
|
+
install_cmd = (
|
|
39
|
+
f'git clone https://github.com/gitleaks/gitleaks.git {CONFIG.dirs.share}/gitleaks || true &&'
|
|
40
|
+
f'cd {CONFIG.dirs.share}/gitleaks && make build &&'
|
|
41
|
+
f'mv {CONFIG.dirs.share}/gitleaks/gitleaks {CONFIG.dirs.bin}'
|
|
42
|
+
)
|
|
43
|
+
install_github_handle = 'gitleaks/gitleaks'
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def on_cmd(self):
|
|
47
|
+
# replace fake -mode opt by subcommand
|
|
48
|
+
mode = self.get_opt_value('mode')
|
|
49
|
+
self.cmd = self.cmd.replace(f'{gitleaks.cmd} ', f'{gitleaks.cmd} {mode} ')
|
|
50
|
+
|
|
51
|
+
# add output path
|
|
52
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
53
|
+
if not output_path:
|
|
54
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
55
|
+
self.output_path = output_path
|
|
56
|
+
self.cmd += f' -r {self.output_path}'
|
|
57
|
+
self.cmd += ' --exit-code 0'
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def on_cmd_done(self):
|
|
61
|
+
if not os.path.exists(self.output_path):
|
|
62
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
66
|
+
with open(self.output_path, 'r') as f:
|
|
67
|
+
results = yaml.safe_load(f.read())
|
|
68
|
+
for result in results:
|
|
69
|
+
yield Tag(
|
|
70
|
+
name=result['RuleID'],
|
|
71
|
+
match='{File}:{StartLine}:{StartColumn}'.format(**result),
|
|
72
|
+
extra_data={
|
|
73
|
+
caml_to_snake(k): v for k, v in result.items()
|
|
74
|
+
if k not in ['RuleID', 'File']
|
|
75
|
+
}
|
|
76
|
+
)
|
secator/tasks/mapcidr.py
CHANGED
|
@@ -10,7 +10,7 @@ from secator.tasks._categories import ReconIp
|
|
|
10
10
|
@task()
|
|
11
11
|
class mapcidr(ReconIp):
|
|
12
12
|
"""Utility program to perform multiple operations for a given subnet/cidr ranges."""
|
|
13
|
-
cmd = 'mapcidr
|
|
13
|
+
cmd = 'mapcidr'
|
|
14
14
|
input_flag = '-cidr'
|
|
15
15
|
file_flag = '-cl'
|
|
16
16
|
install_pre = {
|
secator/tasks/naabu.py
CHANGED
|
@@ -10,7 +10,7 @@ from secator.tasks._categories import ReconPort
|
|
|
10
10
|
@task()
|
|
11
11
|
class naabu(ReconPort):
|
|
12
12
|
"""Port scanning tool written in Go."""
|
|
13
|
-
cmd = 'naabu -Pn
|
|
13
|
+
cmd = 'naabu -Pn'
|
|
14
14
|
input_flag = '-host'
|
|
15
15
|
file_flag = '-list'
|
|
16
16
|
json_flag = '-json'
|
|
@@ -62,6 +62,12 @@ class naabu(ReconPort):
|
|
|
62
62
|
if input == 'localhost':
|
|
63
63
|
self.inputs[ix] = '127.0.0.1'
|
|
64
64
|
|
|
65
|
+
@staticmethod
|
|
66
|
+
def on_cmd(self):
|
|
67
|
+
scan_type = self.get_opt_value('scan_type')
|
|
68
|
+
if scan_type == 's':
|
|
69
|
+
self.requires_sudo = True
|
|
70
|
+
|
|
65
71
|
@staticmethod
|
|
66
72
|
def on_item(self, item):
|
|
67
73
|
if item.host == '127.0.0.1':
|
secator/tasks/nmap.py
CHANGED
|
@@ -36,7 +36,7 @@ class nmap(VulnMulti):
|
|
|
36
36
|
|
|
37
37
|
# Script scanning
|
|
38
38
|
SCRIPT: {'type': str, 'default': None, 'help': 'NSE scripts'},
|
|
39
|
-
'script_args': {'type': str, 'default': None, 'help': 'NSE script arguments (n1=v1,n2=v2,...)'},
|
|
39
|
+
'script_args': {'type': str, 'short': 'sargs', 'default': None, 'help': 'NSE script arguments (n1=v1,n2=v2,...)'},
|
|
40
40
|
|
|
41
41
|
# Host discovery
|
|
42
42
|
'skip_host_discovery': {'is_flag': True, 'short': 'Pn', 'default': False, 'help': 'Skip host discovery (no ping)'},
|
|
@@ -44,42 +44,45 @@ class nmap(VulnMulti):
|
|
|
44
44
|
# Service and version detection
|
|
45
45
|
'version_detection': {'is_flag': True, 'short': 'sV', 'default': False, 'help': 'Enable version detection (slow)'},
|
|
46
46
|
'detect_all': {'is_flag': True, 'short': 'A', 'default': False, 'help': 'Enable OS detection, version detection, script scanning, and traceroute on open ports'}, # noqa: E501
|
|
47
|
-
'detect_os': {'is_flag': True, 'short': 'O', 'default': False, 'help': 'Enable OS detection'},
|
|
47
|
+
'detect_os': {'is_flag': True, 'short': 'O', 'default': False, 'help': 'Enable OS detection', 'requires_sudo': True},
|
|
48
48
|
|
|
49
49
|
# Scan techniques
|
|
50
|
-
'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth'},
|
|
50
|
+
'tcp_syn_stealth': {'is_flag': True, 'short': 'sS', 'default': False, 'help': 'TCP SYN Stealth', 'requires_sudo': True}, # noqa: E501
|
|
51
51
|
'tcp_connect': {'is_flag': True, 'short': 'sT', 'default': False, 'help': 'TCP Connect scan'},
|
|
52
|
-
'udp_scan': {'is_flag': True, 'short': 'sU', 'default': False, 'help': 'UDP scan'},
|
|
53
|
-
'tcp_null_scan': {'is_flag': True, 'short': 'sN', 'default': False, 'help': 'TCP Null scan'},
|
|
54
|
-
'tcp_fin_scan': {'is_flag': True, 'short': 'sF', 'default': False, 'help': 'TCP FIN scan'},
|
|
55
|
-
'tcp_xmas_scan': {'is_flag': True, 'short': 'sX', 'default': False, 'help': 'TCP Xmas scan'},
|
|
56
|
-
'tcp_ack_scan': {'is_flag': True, 'short': 'sA', 'default': False, 'help': 'TCP ACK scan'},
|
|
57
|
-
'tcp_window_scan': {'is_flag': True, 'short': 'sW', 'default': False, 'help': 'TCP Window scan'},
|
|
58
|
-
'tcp_maimon_scan': {'is_flag': True, 'short': 'sM', 'default': False, 'help': 'TCP Maimon scan'},
|
|
59
|
-
'sctp_init_scan': {'is_flag': True, 'short': 'sY', 'default': False, 'help': 'SCTP Init scan'},
|
|
60
|
-
'sctp_cookie_echo_scan': {'is_flag': True, 'short': 'sZ', 'default': False, 'help': 'SCTP Cookie Echo scan'},
|
|
52
|
+
'udp_scan': {'is_flag': True, 'short': 'sU', 'default': False, 'help': 'UDP scan', 'requires_sudo': True},
|
|
53
|
+
'tcp_null_scan': {'is_flag': True, 'short': 'sN', 'default': False, 'help': 'TCP Null scan', 'requires_sudo': True},
|
|
54
|
+
'tcp_fin_scan': {'is_flag': True, 'short': 'sF', 'default': False, 'help': 'TCP FIN scan', 'requires_sudo': True},
|
|
55
|
+
'tcp_xmas_scan': {'is_flag': True, 'short': 'sX', 'default': False, 'help': 'TCP Xmas scan', 'requires_sudo': True},
|
|
56
|
+
'tcp_ack_scan': {'is_flag': True, 'short': 'sA', 'default': False, 'help': 'TCP ACK scan', 'requires_sudo': True},
|
|
57
|
+
'tcp_window_scan': {'is_flag': True, 'short': 'sW', 'default': False, 'help': 'TCP Window scan', 'requires_sudo': True}, # noqa: E501
|
|
58
|
+
'tcp_maimon_scan': {'is_flag': True, 'short': 'sM', 'default': False, 'help': 'TCP Maimon scan', 'requires_sudo': True}, # noqa: E501
|
|
59
|
+
'sctp_init_scan': {'is_flag': True, 'short': 'sY', 'default': False, 'help': 'SCTP Init scan', 'requires_sudo': True},
|
|
60
|
+
'sctp_cookie_echo_scan': {'is_flag': True, 'short': 'sZ', 'default': False, 'help': 'SCTP Cookie Echo scan', 'requires_sudo': True}, # noqa: E501
|
|
61
61
|
'ping_scan': {'is_flag': True, 'short': 'sn', 'default': False, 'help': 'Ping scan (disable port scan)'},
|
|
62
|
-
'ip_protocol_scan': {'type': str, 'short': 'sO', 'default': None, 'help': 'IP protocol scan'},
|
|
62
|
+
'ip_protocol_scan': {'type': str, 'short': 'sO', 'default': None, 'help': 'IP protocol scan', 'requires_sudo': True},
|
|
63
63
|
'script_scan': {'is_flag': True, 'short': 'sC', 'default': False, 'help': 'Enable default scanning'},
|
|
64
|
-
'zombie_host': {'type': str, 'short': 'sI', 'default': None, 'help': 'Use a zombie host for idle scan'},
|
|
64
|
+
'zombie_host': {'type': str, 'short': 'sI', 'default': None, 'help': 'Use a zombie host for idle scan', 'requires_sudo': True}, # noqa: E501
|
|
65
65
|
'ftp_relay_host': {'type': str, 'short': 'sB', 'default': None, 'help': 'FTP bounce scan relay host'},
|
|
66
66
|
|
|
67
67
|
# Firewall / IDS evasion and spoofing
|
|
68
68
|
'spoof_source_port': {'type': int, 'short': 'g', 'default': None, 'help': 'Send packets from a specific port'},
|
|
69
69
|
'spoof_source_ip': {'type': str, 'short': 'S', 'default': None, 'help': 'Spoof source IP address'},
|
|
70
70
|
'spoof_source_mac': {'type': str, 'short': 'spoofmac', 'default': None, 'help': 'Spoof MAC address'},
|
|
71
|
-
'fragment': {'is_flag': True, 'short': 'fragment', 'default': False, 'help': 'Fragment packets'},
|
|
72
|
-
'mtu': {'type': int, 'short': 'mtu', 'default': None, 'help': 'Fragment packets with given MTU'},
|
|
73
|
-
'ttl': {'type': int, 'short': 'ttl', 'default': None, 'help': 'Set TTL'},
|
|
74
|
-
'badsum': {'is_flag': True, 'short': 'badsum', 'default': False, 'help': 'Create a bad checksum in the TCP header'},
|
|
71
|
+
'fragment': {'is_flag': True, 'short': 'fragment', 'default': False, 'help': 'Fragment packets', 'requires_sudo': True}, # noqa: E501
|
|
72
|
+
'mtu': {'type': int, 'short': 'mtu', 'default': None, 'help': 'Fragment packets with given MTU', 'requires_sudo': True}, # noqa: E501
|
|
73
|
+
'ttl': {'type': int, 'short': 'ttl', 'default': None, 'help': 'Set TTL', 'requires_sudo': True},
|
|
74
|
+
'badsum': {'is_flag': True, 'short': 'badsum', 'default': False, 'help': 'Create a bad checksum in the TCP header', 'requires_sudo': True}, # noqa: E501
|
|
75
75
|
'ipv6': {'is_flag': True, 'short': 'ipv6', 'default': False, 'help': 'Enable IPv6 scanning'},
|
|
76
76
|
|
|
77
77
|
# Host discovery
|
|
78
|
-
'traceroute': {'is_flag': True, 'short': 'traceroute', 'default': False, 'help': 'Traceroute'},
|
|
79
|
-
'disable_arp_ping': {'is_flag': True, 'short': '
|
|
78
|
+
'traceroute': {'is_flag': True, 'short': 'traceroute', 'default': False, 'help': 'Traceroute', 'requires_sudo': True},
|
|
79
|
+
'disable_arp_ping': {'is_flag': True, 'short': 'dap', 'default': False, 'help': 'Disable ARP ping'},
|
|
80
80
|
|
|
81
81
|
# Misc
|
|
82
|
-
'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path'},
|
|
82
|
+
'output_path': {'type': str, 'short': 'oX', 'default': None, 'help': 'Output XML file path', 'internal': True, 'display': False}, # noqa: E501
|
|
83
|
+
'debug': {'is_flag': True, 'short': 'd', 'default': False, 'help': 'Enable debug mode'},
|
|
84
|
+
'verbose': {'is_flag': True, 'short': 'v', 'default': False, 'help': 'Enable verbose mode'},
|
|
85
|
+
'timing': {'type': int, 'short': 'T', 'default': None, 'help': 'Timing template (0: paranoid, 1: sneaky, 2: polite, 3: normal, 4: aggressive, 5: insane)'}, # noqa: E501
|
|
83
86
|
}
|
|
84
87
|
opt_key_map = {
|
|
85
88
|
HEADER: OPT_NOT_SUPPORTED,
|
|
@@ -117,7 +120,7 @@ class nmap(VulnMulti):
|
|
|
117
120
|
'spoof_source_port': '-g',
|
|
118
121
|
'spoof_source_ip': '-S',
|
|
119
122
|
'spoof_source_mac': '--spoof-mac',
|
|
120
|
-
'
|
|
123
|
+
'fragment': '-f',
|
|
121
124
|
'mtu': '--mtu',
|
|
122
125
|
'ttl': '--ttl',
|
|
123
126
|
'badsum': '--badsum',
|
|
@@ -144,20 +147,17 @@ class nmap(VulnMulti):
|
|
|
144
147
|
profile = 'io'
|
|
145
148
|
|
|
146
149
|
@staticmethod
|
|
147
|
-
def
|
|
150
|
+
def on_cmd(self):
|
|
148
151
|
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
149
152
|
if not output_path:
|
|
150
153
|
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.xml'
|
|
151
154
|
self.output_path = output_path
|
|
152
155
|
self.cmd += f' -oX {self.output_path}'
|
|
153
|
-
tcp_syn_stealth = self.
|
|
154
|
-
tcp_connect = self.
|
|
155
|
-
udp_scan = self.get_opt_value('udp_scan')
|
|
156
|
-
if tcp_syn_stealth or udp_scan:
|
|
157
|
-
self.cmd = f'sudo {self.cmd}'
|
|
156
|
+
tcp_syn_stealth = self.cmd_options.get('tcp_syn_stealth')
|
|
157
|
+
tcp_connect = self.cmd_options.get('tcp_connect')
|
|
158
158
|
if tcp_connect and tcp_syn_stealth:
|
|
159
159
|
self._print(
|
|
160
|
-
'Options -sT (SYN stealth scan) and -sS (CONNECT scan) are conflicting. Keeping only -
|
|
160
|
+
'Options -sT (SYN stealth scan) and -sS (CONNECT scan) are conflicting. Keeping only -sS.',
|
|
161
161
|
'bold gold3')
|
|
162
162
|
self.cmd = self.cmd.replace('-sT ', '')
|
|
163
163
|
|
secator/tasks/subfinder.py
CHANGED
secator/tasks/testssl.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from secator.config import CONFIG
|
|
6
|
+
from secator.decorators import task
|
|
7
|
+
from secator.output_types import Vulnerability, Certificate, Error, Info, Ip, Tag
|
|
8
|
+
from secator.definitions import (PROXY, HOST, USER_AGENT, HEADER, OUTPUT_PATH,
|
|
9
|
+
CERTIFICATE_STATUS_UNKNOWN, CERTIFICATE_STATUS_TRUSTED, CERTIFICATE_STATUS_REVOKED,
|
|
10
|
+
TIMEOUT)
|
|
11
|
+
from secator.tasks._categories import Command, OPTS
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@task()
|
|
15
|
+
class testssl(Command):
|
|
16
|
+
"""SSL/TLS security scanner, including ciphers, protocols and cryptographic flaws."""
|
|
17
|
+
cmd = 'testssl.sh'
|
|
18
|
+
input_type = HOST
|
|
19
|
+
input_flag = None
|
|
20
|
+
file_flag = '-iL'
|
|
21
|
+
file_eof_newline = True
|
|
22
|
+
version_flag = ''
|
|
23
|
+
opt_prefix = '--'
|
|
24
|
+
opts = {
|
|
25
|
+
'verbose': {'is_flag': True, 'default': False, 'internal': True, 'display': True, 'help': 'Record all SSL/TLS info, not only critical info'}, # noqa: E501
|
|
26
|
+
'parallel': {'is_flag': True, 'default': False, 'help': 'Test multiple hosts in parallel'},
|
|
27
|
+
'warnings': {'type': str, 'default': None, 'help': 'Set to "batch" to stop on errors, and "off" to skip errors and continue'}, # noqa: E501
|
|
28
|
+
'ids_friendly': {'is_flag': True, 'default': False, 'help': 'Avoid IDS blocking by skipping a few vulnerability checks'}, # noqa: E501
|
|
29
|
+
'hints': {'is_flag': True, 'default': False, 'help': 'Additional hints to findings'},
|
|
30
|
+
'server_defaults': {'is_flag': True, 'default': False, 'help': 'Displays the server default picks and certificate info'}, # noqa: E501
|
|
31
|
+
}
|
|
32
|
+
meta_opts = {
|
|
33
|
+
PROXY: OPTS[PROXY],
|
|
34
|
+
USER_AGENT: OPTS[USER_AGENT],
|
|
35
|
+
HEADER: OPTS[HEADER],
|
|
36
|
+
TIMEOUT: OPTS[TIMEOUT],
|
|
37
|
+
}
|
|
38
|
+
opt_key_map = {
|
|
39
|
+
PROXY: 'proxy',
|
|
40
|
+
USER_AGENT: 'user-agent',
|
|
41
|
+
HEADER: 'reqheader',
|
|
42
|
+
TIMEOUT: 'connect-timeout',
|
|
43
|
+
'ipv6': '-6',
|
|
44
|
+
}
|
|
45
|
+
output_types = [Certificate, Vulnerability, Ip, Tag]
|
|
46
|
+
proxy_http = True
|
|
47
|
+
proxychains = False
|
|
48
|
+
proxy_socks5 = False
|
|
49
|
+
profile = 'io'
|
|
50
|
+
install_pre = {
|
|
51
|
+
'apk': ['hexdump'],
|
|
52
|
+
'pacman': ['util-linux'],
|
|
53
|
+
'*': ['bsdmainutils']
|
|
54
|
+
}
|
|
55
|
+
install_cmd = (
|
|
56
|
+
f'git clone --depth 1 https://github.com/drwetter/testssl.sh.git {CONFIG.dirs.share}/testssl.sh || true && '
|
|
57
|
+
f'ln -sf {CONFIG.dirs.share}/testssl.sh/testssl.sh {CONFIG.dirs.bin}'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def on_cmd(self):
|
|
62
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
63
|
+
if not output_path:
|
|
64
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
65
|
+
self.output_path = output_path
|
|
66
|
+
self.cmd += f' --jsonfile {self.output_path}'
|
|
67
|
+
|
|
68
|
+
# Hack because target needs to be the last argument in testssl.sh
|
|
69
|
+
if len(self.inputs) == 1:
|
|
70
|
+
target = self.inputs[0]
|
|
71
|
+
self.cmd = self.cmd.replace(f' {target}', '')
|
|
72
|
+
self.cmd += f' {target}'
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def on_cmd_done(self):
|
|
76
|
+
if not os.path.exists(self.output_path):
|
|
77
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
78
|
+
return
|
|
79
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
80
|
+
|
|
81
|
+
verbose = self.get_opt_value('verbose')
|
|
82
|
+
with open(self.output_path, 'r') as f:
|
|
83
|
+
data = json.load(f)
|
|
84
|
+
bad_cyphers = {}
|
|
85
|
+
retrieved_certificates = {}
|
|
86
|
+
ignored_item_ids = ["scanTime", "overall_grade", "DNS_CAArecord"]
|
|
87
|
+
ip_addresses = []
|
|
88
|
+
host_to_ips = {}
|
|
89
|
+
|
|
90
|
+
for item in data:
|
|
91
|
+
host, ip = tuple(item['ip'].split('/'))
|
|
92
|
+
id = item['id']
|
|
93
|
+
# port = item['port']
|
|
94
|
+
finding = item['finding']
|
|
95
|
+
severity = item['severity'].lower()
|
|
96
|
+
cwe = item.get('cwe')
|
|
97
|
+
vuln_tags = ['ssl', 'tls']
|
|
98
|
+
if cwe:
|
|
99
|
+
vuln_tags.append(cwe)
|
|
100
|
+
|
|
101
|
+
# Skip ignored items
|
|
102
|
+
if id.startswith(tuple(ignored_item_ids)):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Add IP to address pool
|
|
106
|
+
host_to_ips.setdefault(host, []).append(ip)
|
|
107
|
+
if ip not in ip_addresses:
|
|
108
|
+
ip_addresses.append(ip)
|
|
109
|
+
yield Ip(
|
|
110
|
+
host=host,
|
|
111
|
+
ip=ip,
|
|
112
|
+
alive=True
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Process errors
|
|
116
|
+
if id.startswith("scanProblem"):
|
|
117
|
+
yield Error(message=finding)
|
|
118
|
+
|
|
119
|
+
# Process bad ciphers
|
|
120
|
+
elif id.startswith('cipher-'):
|
|
121
|
+
splited_item = item["finding"].split(" ")
|
|
122
|
+
concerned_protocol = splited_item[0]
|
|
123
|
+
bad_cypher = splited_item[-1]
|
|
124
|
+
bad_cyphers.setdefault(ip, {}).setdefault(concerned_protocol, []).append(bad_cypher) # noqa: E501
|
|
125
|
+
|
|
126
|
+
# Process certificates
|
|
127
|
+
elif id.startswith('cert_') or id.startswith('cert '):
|
|
128
|
+
retrieved_certificates.setdefault(ip, []).append(item)
|
|
129
|
+
|
|
130
|
+
# Process intermediate certificates
|
|
131
|
+
elif id.startswith('intermediate_cert_'):
|
|
132
|
+
# TODO: implement this
|
|
133
|
+
pass
|
|
134
|
+
|
|
135
|
+
# If info or ok, create a tag only if 'verbose' option is set
|
|
136
|
+
elif severity in ['info', 'ok']:
|
|
137
|
+
if not verbose:
|
|
138
|
+
continue
|
|
139
|
+
yield Tag(
|
|
140
|
+
name=f'SSL/TLS [{id}]',
|
|
141
|
+
match=host,
|
|
142
|
+
extra_data={
|
|
143
|
+
'type': id,
|
|
144
|
+
'finding': finding,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Create vulnerability
|
|
149
|
+
else:
|
|
150
|
+
if id in ['TLS1', 'TLS1_1']:
|
|
151
|
+
human_name = f'SSL/TLS deprecated protocol offered: {id}'
|
|
152
|
+
else:
|
|
153
|
+
human_name = f'SSL/TLS {id}: {finding}'
|
|
154
|
+
yield Vulnerability(
|
|
155
|
+
name=human_name,
|
|
156
|
+
matched_at=host,
|
|
157
|
+
ip=ip,
|
|
158
|
+
tags=vuln_tags,
|
|
159
|
+
severity=severity,
|
|
160
|
+
confidence='high',
|
|
161
|
+
extra_data={
|
|
162
|
+
'id': id,
|
|
163
|
+
'finding': finding
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Creating vulnerability for the deprecated ciphers
|
|
168
|
+
for ip, protocols in bad_cyphers.items():
|
|
169
|
+
for protocol, cyphers in protocols.items():
|
|
170
|
+
yield Vulnerability(
|
|
171
|
+
name=f'SSL/TLS vulnerability ciphers for {protocol} deprecated',
|
|
172
|
+
matched_at=ip,
|
|
173
|
+
ip=ip,
|
|
174
|
+
confidence='high',
|
|
175
|
+
severity='low',
|
|
176
|
+
extra_data={
|
|
177
|
+
'cyphers': cyphers
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Creating certificates for each founded target
|
|
182
|
+
host_to_ips = {k: set(v) for k, v in host_to_ips.items()}
|
|
183
|
+
for ip, certs in retrieved_certificates.items():
|
|
184
|
+
host = [k for k, v in host_to_ips.items() if ip in v][0]
|
|
185
|
+
cert_data = {
|
|
186
|
+
'host': host,
|
|
187
|
+
'ip': ip,
|
|
188
|
+
'fingerprint_sha256': None,
|
|
189
|
+
'subject_cn': None,
|
|
190
|
+
'subject_an': None,
|
|
191
|
+
'not_before': None,
|
|
192
|
+
'not_after': None,
|
|
193
|
+
'issuer_cn': None,
|
|
194
|
+
'self_signed': None,
|
|
195
|
+
'trusted': None,
|
|
196
|
+
'status': None,
|
|
197
|
+
'keysize': None,
|
|
198
|
+
'serial_number': None,
|
|
199
|
+
}
|
|
200
|
+
for cert in certs:
|
|
201
|
+
host = [k for k, v in host_to_ips.items() if ip in v][0]
|
|
202
|
+
id = cert['id']
|
|
203
|
+
finding = cert['finding']
|
|
204
|
+
|
|
205
|
+
if id.startswith('cert_crlDistributionPoints') and finding != '--':
|
|
206
|
+
# TODO not implemented, need to find a certificate that is revoked by CRL
|
|
207
|
+
cert_data['status'] = CERTIFICATE_STATUS_UNKNOWN
|
|
208
|
+
|
|
209
|
+
if id.startswith('cert_ocspRevoked'):
|
|
210
|
+
if finding.startswith('not revoked'):
|
|
211
|
+
cert_data['status'] = CERTIFICATE_STATUS_TRUSTED
|
|
212
|
+
else:
|
|
213
|
+
cert_data['status'] = CERTIFICATE_STATUS_REVOKED
|
|
214
|
+
|
|
215
|
+
if id.startswith('cert_fingerprintSHA256'):
|
|
216
|
+
cert_data['fingerprint_sha256'] = finding
|
|
217
|
+
|
|
218
|
+
if id.startswith('cert_commonName'):
|
|
219
|
+
cert_data['subject_cn'] = finding
|
|
220
|
+
|
|
221
|
+
if id.startswith('cert_subjectAltName'):
|
|
222
|
+
cert_data['subject_an'] = finding.split(" ")
|
|
223
|
+
|
|
224
|
+
if id.startswith('cert_notBefore'):
|
|
225
|
+
cert_data['not_before'] = datetime.strptime(finding, "%Y-%m-%d %H:%M")
|
|
226
|
+
|
|
227
|
+
if id.startswith('cert_notAfter'):
|
|
228
|
+
cert_data['not_after'] = datetime.strptime(finding, "%Y-%m-%d %H:%M")
|
|
229
|
+
|
|
230
|
+
if id.startswith('cert_caIssuers'):
|
|
231
|
+
cert_data['issuer_cn'] = finding
|
|
232
|
+
|
|
233
|
+
if id.startswith('cert_chain_of_trust'):
|
|
234
|
+
cert_data['self_signed'] = 'self signed' in finding
|
|
235
|
+
|
|
236
|
+
if id.startswith('cert_chain_of_trust'):
|
|
237
|
+
cert_data['trusted'] = finding.startswith('passed')
|
|
238
|
+
|
|
239
|
+
if id.startswith('cert_keySize'):
|
|
240
|
+
cert_data['keysize'] = int(finding.split(" ")[1])
|
|
241
|
+
|
|
242
|
+
if id.startswith('cert_serialNumber'):
|
|
243
|
+
cert_data['serial_number'] = finding
|
|
244
|
+
|
|
245
|
+
if id.startswith('cert ') and finding.startswith('-----BEGIN CERTIFICATE-----'):
|
|
246
|
+
cert_data['raw_value'] = finding
|
|
247
|
+
|
|
248
|
+
# For the following attributes commented, it's because at the time of writting it
|
|
249
|
+
# I did not found the value inside the result of testssl
|
|
250
|
+
cert = Certificate(
|
|
251
|
+
**cert_data
|
|
252
|
+
# issuer_dn='',
|
|
253
|
+
# issuer='',
|
|
254
|
+
# TODO: delete the ciphers attribute from certificate outputType
|
|
255
|
+
# ciphers=None,
|
|
256
|
+
# TODO: need to find a way to retrieve the parent certificate,
|
|
257
|
+
# parent_certificate=None,
|
|
258
|
+
)
|
|
259
|
+
yield cert
|
|
260
|
+
if cert.is_expired():
|
|
261
|
+
yield Vulnerability(
|
|
262
|
+
name='SSL certificate expired',
|
|
263
|
+
provider='testssl',
|
|
264
|
+
description='The SSL certificate is expired. This can easily lead to domain takeovers',
|
|
265
|
+
matched_at=host,
|
|
266
|
+
ip=ip,
|
|
267
|
+
tags=['ssl', 'tls'],
|
|
268
|
+
severity='medium',
|
|
269
|
+
confidence='high',
|
|
270
|
+
extra_data={
|
|
271
|
+
'id': id,
|
|
272
|
+
'expiration_date': Certificate.format_date(cert.not_after)
|
|
273
|
+
}
|
|
274
|
+
)
|
secator/tasks/trivy.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import os
|
|
3
|
+
import yaml
|
|
4
|
+
|
|
5
|
+
from secator.config import CONFIG
|
|
6
|
+
from secator.decorators import task
|
|
7
|
+
from secator.definitions import (THREADS, OUTPUT_PATH, OPT_NOT_SUPPORTED, HEADER, DELAY, FOLLOW_REDIRECT,
|
|
8
|
+
PROXY, RATE_LIMIT, RETRIES, TIMEOUT, USER_AGENT)
|
|
9
|
+
from secator.tasks._categories import Vuln
|
|
10
|
+
from secator.output_types import Vulnerability, Tag, Info, Error
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@task()
|
|
14
|
+
class trivy(Vuln):
|
|
15
|
+
"""Comprehensive and versatile security scanner."""
|
|
16
|
+
cmd = 'trivy'
|
|
17
|
+
input_flag = None
|
|
18
|
+
json_flag = '-f json'
|
|
19
|
+
opts = {
|
|
20
|
+
"mode": {"type": click.Choice(['image', 'fs', 'repo']), 'default': 'image', 'help': 'Trivy mode', 'required': True} # noqa: E501
|
|
21
|
+
}
|
|
22
|
+
opt_key_map = {
|
|
23
|
+
THREADS: OPT_NOT_SUPPORTED,
|
|
24
|
+
HEADER: OPT_NOT_SUPPORTED,
|
|
25
|
+
DELAY: OPT_NOT_SUPPORTED,
|
|
26
|
+
FOLLOW_REDIRECT: OPT_NOT_SUPPORTED,
|
|
27
|
+
PROXY: OPT_NOT_SUPPORTED,
|
|
28
|
+
RATE_LIMIT: OPT_NOT_SUPPORTED,
|
|
29
|
+
RETRIES: OPT_NOT_SUPPORTED,
|
|
30
|
+
TIMEOUT: OPT_NOT_SUPPORTED,
|
|
31
|
+
USER_AGENT: OPT_NOT_SUPPORTED
|
|
32
|
+
}
|
|
33
|
+
output_types = [Tag, Vulnerability]
|
|
34
|
+
install_cmd = (
|
|
35
|
+
'curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh |'
|
|
36
|
+
f'sudo sh -s -- -b {CONFIG.dirs.bin} v0.61.1'
|
|
37
|
+
)
|
|
38
|
+
install_github_handle = 'aquasecurity/trivy'
|
|
39
|
+
install_github_handle = 'aquasecurity/trivy'
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def on_cmd(self):
|
|
43
|
+
mode = self.get_opt_value('mode')
|
|
44
|
+
output_path = self.get_opt_value(OUTPUT_PATH)
|
|
45
|
+
if not output_path:
|
|
46
|
+
output_path = f'{self.reports_folder}/.outputs/{self.unique_name}.json'
|
|
47
|
+
self.output_path = output_path
|
|
48
|
+
self.cmd = self.cmd.replace(f' -mode {mode}', '').replace('trivy', f'trivy {mode}')
|
|
49
|
+
self.cmd += f' -o {self.output_path}'
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def on_cmd_done(self):
|
|
53
|
+
if not os.path.exists(self.output_path):
|
|
54
|
+
yield Error(message=f'Could not find JSON results in {self.output_path}')
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
yield Info(message=f'JSON results saved to {self.output_path}')
|
|
58
|
+
with open(self.output_path, 'r') as f:
|
|
59
|
+
results = yaml.safe_load(f.read()).get('Results', [])
|
|
60
|
+
for item in results:
|
|
61
|
+
for vuln in item.get('Vulnerabilities', []):
|
|
62
|
+
vuln_id = vuln['VulnerabilityID']
|
|
63
|
+
extra_data = {}
|
|
64
|
+
if 'PkgName' in vuln:
|
|
65
|
+
extra_data['product'] = vuln['PkgName']
|
|
66
|
+
if 'InstalledVersion' in vuln:
|
|
67
|
+
extra_data['version'] = vuln['InstalledVersion']
|
|
68
|
+
cvss = vuln.get('CVSS', {})
|
|
69
|
+
cvss_score = -1
|
|
70
|
+
for _, cvss_data in cvss.items():
|
|
71
|
+
cvss_score = cvss_data.get('V3Score', -1) or cvss_data.get('V2Score', -1)
|
|
72
|
+
data = {
|
|
73
|
+
'name': vuln_id,
|
|
74
|
+
'id': vuln_id,
|
|
75
|
+
'provider': vuln.get('DataSource', {}).get('ID', ''),
|
|
76
|
+
'description': vuln.get('Description'),
|
|
77
|
+
'matched_at': self.inputs[0],
|
|
78
|
+
'confidence': 'high',
|
|
79
|
+
'severity': vuln['Severity'].lower(),
|
|
80
|
+
'cvss_score': cvss_score,
|
|
81
|
+
'reference': vuln.get('PrimaryURL', ''),
|
|
82
|
+
'references': vuln.get('References', []),
|
|
83
|
+
'extra_data': extra_data
|
|
84
|
+
}
|
|
85
|
+
if vuln_id.startswith('CVE'):
|
|
86
|
+
remote_data = Vuln.lookup_cve(vuln_id)
|
|
87
|
+
if remote_data:
|
|
88
|
+
data.update(remote_data)
|
|
89
|
+
yield Vulnerability(**data)
|
|
90
|
+
for secret in item.get('Secrets', []):
|
|
91
|
+
yield Tag(
|
|
92
|
+
name=secret['RuleID'],
|
|
93
|
+
match=secret['Match'],
|
|
94
|
+
extra_data={k: v for k, v in secret.items() if k not in ['RuleID', 'Match']}
|
|
95
|
+
)
|