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.

Files changed (39) hide show
  1. secator/celery.py +10 -5
  2. secator/celery_signals.py +2 -11
  3. secator/cli.py +153 -46
  4. secator/configs/workflows/url_params_fuzz.yaml +23 -0
  5. secator/configs/workflows/wordpress.yaml +4 -1
  6. secator/decorators.py +10 -5
  7. secator/definitions.py +3 -0
  8. secator/installer.py +46 -32
  9. secator/output_types/__init__.py +2 -1
  10. secator/output_types/certificate.py +78 -0
  11. secator/output_types/user_account.py +1 -1
  12. secator/report.py +1 -1
  13. secator/rich.py +1 -1
  14. secator/runners/_base.py +14 -6
  15. secator/runners/_helpers.py +4 -3
  16. secator/runners/command.py +81 -21
  17. secator/runners/scan.py +5 -3
  18. secator/runners/workflow.py +22 -4
  19. secator/tasks/_categories.py +6 -1
  20. secator/tasks/arjun.py +82 -0
  21. secator/tasks/dalfox.py +1 -1
  22. secator/tasks/ffuf.py +2 -1
  23. secator/tasks/gitleaks.py +76 -0
  24. secator/tasks/mapcidr.py +1 -1
  25. secator/tasks/naabu.py +7 -1
  26. secator/tasks/nmap.py +29 -29
  27. secator/tasks/subfinder.py +1 -1
  28. secator/tasks/testssl.py +274 -0
  29. secator/tasks/trivy.py +95 -0
  30. secator/tasks/wafw00f.py +83 -0
  31. secator/tasks/wpprobe.py +94 -0
  32. secator/template.py +49 -67
  33. secator/utils.py +13 -5
  34. secator/utils_test.py +26 -8
  35. {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/METADATA +1 -1
  36. {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/RECORD +39 -31
  37. {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/WHEEL +0 -0
  38. {secator-0.10.1a11.dist-info → secator-0.11.0.dist-info}/entry_points.txt +0 -0
  39. {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 -silent'
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 -silent'
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': 'disable-arp-ping', 'default': False, 'help': 'Disable ARP ping'},
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
- 'fragmentation': '-f',
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 on_init(self):
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.get_opt_value('tcp_syn_stealth')
154
- tcp_connect = self.get_opt_value('tcp_connect')
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 -sT.',
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
 
@@ -9,7 +9,7 @@ from secator.tasks._categories import ReconDns
9
9
  @task()
10
10
  class subfinder(ReconDns):
11
11
  """Fast passive subdomain enumeration tool."""
12
- cmd = 'subfinder -silent -cs'
12
+ cmd = 'subfinder -cs'
13
13
  file_flag = '-dL'
14
14
  input_flag = '-d'
15
15
  json_flag = '-json'
@@ -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
+ )