@raghulm/aegis-mcp 1.0.4 → 1.0.7
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.
- package/README.md +286 -290
- package/audit/audit_logger.py +62 -62
- package/package.json +5 -6
- package/policies/roles.yaml +34 -34
- package/policies/scope_rules.yaml +16 -16
- package/requirements.txt +8 -7
- package/run_stdio.py +22 -22
- package/server/auth.py +69 -69
- package/server/config.py +82 -82
- package/server/health.py +19 -19
- package/server/logging.py +33 -33
- package/server/main.py +212 -144
- package/server/stdio.py +7 -7
- package/tools/aws/ec2.py +26 -26
- package/tools/aws/s3.py +54 -54
- package/tools/cicd/jenkins.py +256 -0
- package/tools/cicd/pipeline.py +33 -33
- package/tools/git/repo.py +22 -22
- package/tools/kubernetes/audit.py +108 -108
- package/tools/kubernetes/pods.py +27 -27
- package/tools/network/headers.py +99 -99
- package/tools/network/port_scanner.py +66 -66
- package/tools/network/ssl_checker.py +65 -65
- package/tools/security/deps.py +103 -103
- package/tools/security/secrets.py +91 -91
- package/tools/security/semgrep.py +261 -261
- package/tools/security/trivy.py +19 -19
package/tools/network/headers.py
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
|
|
7
|
-
_SECURITY_HEADERS = {
|
|
8
|
-
"Strict-Transport-Security": {
|
|
9
|
-
"description": "Enforces HTTPS connections",
|
|
10
|
-
"recommended": True,
|
|
11
|
-
},
|
|
12
|
-
"Content-Security-Policy": {
|
|
13
|
-
"description": "Controls resources the browser is allowed to load",
|
|
14
|
-
"recommended": True,
|
|
15
|
-
},
|
|
16
|
-
"X-Content-Type-Options": {
|
|
17
|
-
"description": "Prevents MIME-type sniffing",
|
|
18
|
-
"recommended": True,
|
|
19
|
-
"expected_value": "nosniff",
|
|
20
|
-
},
|
|
21
|
-
"X-Frame-Options": {
|
|
22
|
-
"description": "Controls whether the page can be embedded in iframes",
|
|
23
|
-
"recommended": True,
|
|
24
|
-
},
|
|
25
|
-
"Referrer-Policy": {
|
|
26
|
-
"description": "Controls how much referrer information is sent",
|
|
27
|
-
"recommended": True,
|
|
28
|
-
},
|
|
29
|
-
"Permissions-Policy": {
|
|
30
|
-
"description": "Controls browser features available to the page",
|
|
31
|
-
"recommended": True,
|
|
32
|
-
},
|
|
33
|
-
"X-XSS-Protection": {
|
|
34
|
-
"description": "Legacy XSS filter (mostly deprecated but still checked)",
|
|
35
|
-
"recommended": False,
|
|
36
|
-
},
|
|
37
|
-
"Cache-Control": {
|
|
38
|
-
"description": "Controls caching behavior for sensitive data",
|
|
39
|
-
"recommended": True,
|
|
40
|
-
},
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def check_http_headers(url: str) -> dict[str, Any]:
|
|
45
|
-
"""Audit HTTP security headers for a given URL.
|
|
46
|
-
|
|
47
|
-
Args:
|
|
48
|
-
url: The target URL to check (must include scheme, e.g. https://example.com).
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Dict with header analysis, score, and recommendations.
|
|
52
|
-
"""
|
|
53
|
-
if not url.startswith(("http://", "https://")):
|
|
54
|
-
url = "https://" + url
|
|
55
|
-
|
|
56
|
-
try:
|
|
57
|
-
resp = requests.head(url, timeout=10, allow_redirects=True)
|
|
58
|
-
except requests.ConnectionError as exc:
|
|
59
|
-
raise RuntimeError(f"Cannot connect to '{url}': {exc}") from exc
|
|
60
|
-
except requests.Timeout as exc:
|
|
61
|
-
raise RuntimeError(f"Request to '{url}' timed out") from exc
|
|
62
|
-
|
|
63
|
-
headers_lower = {k.lower(): v for k, v in resp.headers.items()}
|
|
64
|
-
total_recommended = sum(1 for h in _SECURITY_HEADERS.values() if h["recommended"])
|
|
65
|
-
present_recommended = 0
|
|
66
|
-
|
|
67
|
-
results: list[dict[str, Any]] = []
|
|
68
|
-
for header_name, info in _SECURITY_HEADERS.items():
|
|
69
|
-
value = headers_lower.get(header_name.lower())
|
|
70
|
-
is_present = value is not None
|
|
71
|
-
if is_present and info["recommended"]:
|
|
72
|
-
present_recommended += 1
|
|
73
|
-
|
|
74
|
-
entry: dict[str, Any] = {
|
|
75
|
-
"header": header_name,
|
|
76
|
-
"present": is_present,
|
|
77
|
-
"value": value or "",
|
|
78
|
-
"description": info["description"],
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
expected = info.get("expected_value")
|
|
82
|
-
if expected and is_present and value != expected:
|
|
83
|
-
entry["warning"] = f"Expected '{expected}', got '{value}'"
|
|
84
|
-
|
|
85
|
-
results.append(entry)
|
|
86
|
-
|
|
87
|
-
score = round((present_recommended / total_recommended) * 100) if total_recommended else 0
|
|
88
|
-
grade = "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 50 else "D" if score >= 30 else "F"
|
|
89
|
-
|
|
90
|
-
missing = [r["header"] for r in results if not r["present"] and _SECURITY_HEADERS[r["header"]]["recommended"]]
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
"url": url,
|
|
94
|
-
"status_code": resp.status_code,
|
|
95
|
-
"score": score,
|
|
96
|
-
"grade": grade,
|
|
97
|
-
"headers": results,
|
|
98
|
-
"missing_recommended": missing,
|
|
99
|
-
}
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
_SECURITY_HEADERS = {
|
|
8
|
+
"Strict-Transport-Security": {
|
|
9
|
+
"description": "Enforces HTTPS connections",
|
|
10
|
+
"recommended": True,
|
|
11
|
+
},
|
|
12
|
+
"Content-Security-Policy": {
|
|
13
|
+
"description": "Controls resources the browser is allowed to load",
|
|
14
|
+
"recommended": True,
|
|
15
|
+
},
|
|
16
|
+
"X-Content-Type-Options": {
|
|
17
|
+
"description": "Prevents MIME-type sniffing",
|
|
18
|
+
"recommended": True,
|
|
19
|
+
"expected_value": "nosniff",
|
|
20
|
+
},
|
|
21
|
+
"X-Frame-Options": {
|
|
22
|
+
"description": "Controls whether the page can be embedded in iframes",
|
|
23
|
+
"recommended": True,
|
|
24
|
+
},
|
|
25
|
+
"Referrer-Policy": {
|
|
26
|
+
"description": "Controls how much referrer information is sent",
|
|
27
|
+
"recommended": True,
|
|
28
|
+
},
|
|
29
|
+
"Permissions-Policy": {
|
|
30
|
+
"description": "Controls browser features available to the page",
|
|
31
|
+
"recommended": True,
|
|
32
|
+
},
|
|
33
|
+
"X-XSS-Protection": {
|
|
34
|
+
"description": "Legacy XSS filter (mostly deprecated but still checked)",
|
|
35
|
+
"recommended": False,
|
|
36
|
+
},
|
|
37
|
+
"Cache-Control": {
|
|
38
|
+
"description": "Controls caching behavior for sensitive data",
|
|
39
|
+
"recommended": True,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_http_headers(url: str) -> dict[str, Any]:
|
|
45
|
+
"""Audit HTTP security headers for a given URL.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
url: The target URL to check (must include scheme, e.g. https://example.com).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict with header analysis, score, and recommendations.
|
|
52
|
+
"""
|
|
53
|
+
if not url.startswith(("http://", "https://")):
|
|
54
|
+
url = "https://" + url
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
resp = requests.head(url, timeout=10, allow_redirects=True)
|
|
58
|
+
except requests.ConnectionError as exc:
|
|
59
|
+
raise RuntimeError(f"Cannot connect to '{url}': {exc}") from exc
|
|
60
|
+
except requests.Timeout as exc:
|
|
61
|
+
raise RuntimeError(f"Request to '{url}' timed out") from exc
|
|
62
|
+
|
|
63
|
+
headers_lower = {k.lower(): v for k, v in resp.headers.items()}
|
|
64
|
+
total_recommended = sum(1 for h in _SECURITY_HEADERS.values() if h["recommended"])
|
|
65
|
+
present_recommended = 0
|
|
66
|
+
|
|
67
|
+
results: list[dict[str, Any]] = []
|
|
68
|
+
for header_name, info in _SECURITY_HEADERS.items():
|
|
69
|
+
value = headers_lower.get(header_name.lower())
|
|
70
|
+
is_present = value is not None
|
|
71
|
+
if is_present and info["recommended"]:
|
|
72
|
+
present_recommended += 1
|
|
73
|
+
|
|
74
|
+
entry: dict[str, Any] = {
|
|
75
|
+
"header": header_name,
|
|
76
|
+
"present": is_present,
|
|
77
|
+
"value": value or "",
|
|
78
|
+
"description": info["description"],
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
expected = info.get("expected_value")
|
|
82
|
+
if expected and is_present and value != expected:
|
|
83
|
+
entry["warning"] = f"Expected '{expected}', got '{value}'"
|
|
84
|
+
|
|
85
|
+
results.append(entry)
|
|
86
|
+
|
|
87
|
+
score = round((present_recommended / total_recommended) * 100) if total_recommended else 0
|
|
88
|
+
grade = "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 50 else "D" if score >= 30 else "F"
|
|
89
|
+
|
|
90
|
+
missing = [r["header"] for r in results if not r["present"] and _SECURITY_HEADERS[r["header"]]["recommended"]]
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"url": url,
|
|
94
|
+
"status_code": resp.status_code,
|
|
95
|
+
"score": score,
|
|
96
|
+
"grade": grade,
|
|
97
|
+
"headers": results,
|
|
98
|
+
"missing_recommended": missing,
|
|
99
|
+
}
|
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import socket
|
|
4
|
-
from typing import Any
|
|
5
|
-
|
|
6
|
-
_COMMON_PORTS: dict[int, str] = {
|
|
7
|
-
21: "FTP",
|
|
8
|
-
22: "SSH",
|
|
9
|
-
23: "Telnet",
|
|
10
|
-
25: "SMTP",
|
|
11
|
-
53: "DNS",
|
|
12
|
-
80: "HTTP",
|
|
13
|
-
110: "POP3",
|
|
14
|
-
143: "IMAP",
|
|
15
|
-
443: "HTTPS",
|
|
16
|
-
445: "SMB",
|
|
17
|
-
993: "IMAPS",
|
|
18
|
-
995: "POP3S",
|
|
19
|
-
3306: "MySQL",
|
|
20
|
-
3389: "RDP",
|
|
21
|
-
5432: "PostgreSQL",
|
|
22
|
-
6379: "Redis",
|
|
23
|
-
8080: "HTTP-Alt",
|
|
24
|
-
8443: "HTTPS-Alt",
|
|
25
|
-
27017: "MongoDB",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def port_scan(host: str, ports: str = "") -> list[dict[str, Any]]:
|
|
30
|
-
"""Perform a basic TCP connect scan on a host.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
host: Target hostname or IP address.
|
|
34
|
-
ports: Comma-separated port numbers. If empty, scans common service ports.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
List of port results with port number, status, and service name.
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
resolved_ip = socket.gethostbyname(host)
|
|
41
|
-
except socket.gaierror as exc:
|
|
42
|
-
raise RuntimeError(f"Cannot resolve hostname '{host}': {exc}") from exc
|
|
43
|
-
|
|
44
|
-
if ports:
|
|
45
|
-
try:
|
|
46
|
-
port_list = [int(p.strip()) for p in ports.split(",") if p.strip()]
|
|
47
|
-
except ValueError as exc:
|
|
48
|
-
raise RuntimeError(f"Invalid port specification: {exc}") from exc
|
|
49
|
-
else:
|
|
50
|
-
port_list = list(_COMMON_PORTS.keys())
|
|
51
|
-
|
|
52
|
-
results: list[dict[str, Any]] = []
|
|
53
|
-
for port in sorted(port_list):
|
|
54
|
-
try:
|
|
55
|
-
with socket.create_connection((host, port), timeout=2):
|
|
56
|
-
status = "open"
|
|
57
|
-
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
58
|
-
status = "closed"
|
|
59
|
-
|
|
60
|
-
results.append({
|
|
61
|
-
"port": port,
|
|
62
|
-
"status": status,
|
|
63
|
-
"service": _COMMON_PORTS.get(port, "unknown"),
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
return results
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
_COMMON_PORTS: dict[int, str] = {
|
|
7
|
+
21: "FTP",
|
|
8
|
+
22: "SSH",
|
|
9
|
+
23: "Telnet",
|
|
10
|
+
25: "SMTP",
|
|
11
|
+
53: "DNS",
|
|
12
|
+
80: "HTTP",
|
|
13
|
+
110: "POP3",
|
|
14
|
+
143: "IMAP",
|
|
15
|
+
443: "HTTPS",
|
|
16
|
+
445: "SMB",
|
|
17
|
+
993: "IMAPS",
|
|
18
|
+
995: "POP3S",
|
|
19
|
+
3306: "MySQL",
|
|
20
|
+
3389: "RDP",
|
|
21
|
+
5432: "PostgreSQL",
|
|
22
|
+
6379: "Redis",
|
|
23
|
+
8080: "HTTP-Alt",
|
|
24
|
+
8443: "HTTPS-Alt",
|
|
25
|
+
27017: "MongoDB",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def port_scan(host: str, ports: str = "") -> list[dict[str, Any]]:
|
|
30
|
+
"""Perform a basic TCP connect scan on a host.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
host: Target hostname or IP address.
|
|
34
|
+
ports: Comma-separated port numbers. If empty, scans common service ports.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of port results with port number, status, and service name.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
resolved_ip = socket.gethostbyname(host)
|
|
41
|
+
except socket.gaierror as exc:
|
|
42
|
+
raise RuntimeError(f"Cannot resolve hostname '{host}': {exc}") from exc
|
|
43
|
+
|
|
44
|
+
if ports:
|
|
45
|
+
try:
|
|
46
|
+
port_list = [int(p.strip()) for p in ports.split(",") if p.strip()]
|
|
47
|
+
except ValueError as exc:
|
|
48
|
+
raise RuntimeError(f"Invalid port specification: {exc}") from exc
|
|
49
|
+
else:
|
|
50
|
+
port_list = list(_COMMON_PORTS.keys())
|
|
51
|
+
|
|
52
|
+
results: list[dict[str, Any]] = []
|
|
53
|
+
for port in sorted(port_list):
|
|
54
|
+
try:
|
|
55
|
+
with socket.create_connection((host, port), timeout=2):
|
|
56
|
+
status = "open"
|
|
57
|
+
except (socket.timeout, ConnectionRefusedError, OSError):
|
|
58
|
+
status = "closed"
|
|
59
|
+
|
|
60
|
+
results.append({
|
|
61
|
+
"port": port,
|
|
62
|
+
"status": status,
|
|
63
|
+
"service": _COMMON_PORTS.get(port, "unknown"),
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return results
|
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import socket
|
|
4
|
-
import ssl
|
|
5
|
-
from datetime import datetime, timezone
|
|
6
|
-
from typing import Any
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def check_ssl_certificate(hostname: str, port: int = 443) -> dict[str, Any]:
|
|
10
|
-
"""Check the SSL/TLS certificate for a given hostname.
|
|
11
|
-
|
|
12
|
-
Args:
|
|
13
|
-
hostname: Domain name to check.
|
|
14
|
-
port: TCP port (default 443).
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Certificate details including subject, issuer, validity, and expiry info.
|
|
18
|
-
"""
|
|
19
|
-
context = ssl.create_default_context()
|
|
20
|
-
try:
|
|
21
|
-
with socket.create_connection((hostname, port), timeout=10) as sock:
|
|
22
|
-
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
23
|
-
cert = ssock.getpeercert()
|
|
24
|
-
except socket.gaierror as exc:
|
|
25
|
-
raise RuntimeError(f"DNS resolution failed for '{hostname}': {exc}") from exc
|
|
26
|
-
except socket.timeout as exc:
|
|
27
|
-
raise RuntimeError(f"Connection to '{hostname}:{port}' timed out") from exc
|
|
28
|
-
except ssl.SSLError as exc:
|
|
29
|
-
raise RuntimeError(f"SSL error for '{hostname}:{port}': {exc}") from exc
|
|
30
|
-
except OSError as exc:
|
|
31
|
-
raise RuntimeError(f"Connection failed to '{hostname}:{port}': {exc}") from exc
|
|
32
|
-
|
|
33
|
-
if not cert:
|
|
34
|
-
raise RuntimeError(f"No certificate returned by '{hostname}:{port}'")
|
|
35
|
-
|
|
36
|
-
def _format_name(name_tuples: tuple) -> str:
|
|
37
|
-
parts = []
|
|
38
|
-
for attr_group in name_tuples:
|
|
39
|
-
for key, value in attr_group:
|
|
40
|
-
parts.append(f"{key}={value}")
|
|
41
|
-
return ", ".join(parts)
|
|
42
|
-
|
|
43
|
-
not_before = datetime.strptime(cert["notBefore"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
|
44
|
-
not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
|
45
|
-
now = datetime.now(tz=timezone.utc)
|
|
46
|
-
days_until_expiry = (not_after - now).days
|
|
47
|
-
|
|
48
|
-
san_entries = []
|
|
49
|
-
for san_type, san_value in cert.get("subjectAltName", ()):
|
|
50
|
-
san_entries.append(f"{san_type}:{san_value}")
|
|
51
|
-
|
|
52
|
-
return {
|
|
53
|
-
"hostname": hostname,
|
|
54
|
-
"port": port,
|
|
55
|
-
"subject": _format_name(cert.get("subject", ())),
|
|
56
|
-
"issuer": _format_name(cert.get("issuer", ())),
|
|
57
|
-
"serial_number": cert.get("serialNumber", ""),
|
|
58
|
-
"version": cert.get("version", ""),
|
|
59
|
-
"not_before": not_before.isoformat(),
|
|
60
|
-
"not_after": not_after.isoformat(),
|
|
61
|
-
"days_until_expiry": days_until_expiry,
|
|
62
|
-
"expired": days_until_expiry < 0,
|
|
63
|
-
"subject_alt_names": san_entries,
|
|
64
|
-
"protocol": "TLSv1.2+",
|
|
65
|
-
}
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import socket
|
|
4
|
+
import ssl
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_ssl_certificate(hostname: str, port: int = 443) -> dict[str, Any]:
|
|
10
|
+
"""Check the SSL/TLS certificate for a given hostname.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
hostname: Domain name to check.
|
|
14
|
+
port: TCP port (default 443).
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Certificate details including subject, issuer, validity, and expiry info.
|
|
18
|
+
"""
|
|
19
|
+
context = ssl.create_default_context()
|
|
20
|
+
try:
|
|
21
|
+
with socket.create_connection((hostname, port), timeout=10) as sock:
|
|
22
|
+
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
|
|
23
|
+
cert = ssock.getpeercert()
|
|
24
|
+
except socket.gaierror as exc:
|
|
25
|
+
raise RuntimeError(f"DNS resolution failed for '{hostname}': {exc}") from exc
|
|
26
|
+
except socket.timeout as exc:
|
|
27
|
+
raise RuntimeError(f"Connection to '{hostname}:{port}' timed out") from exc
|
|
28
|
+
except ssl.SSLError as exc:
|
|
29
|
+
raise RuntimeError(f"SSL error for '{hostname}:{port}': {exc}") from exc
|
|
30
|
+
except OSError as exc:
|
|
31
|
+
raise RuntimeError(f"Connection failed to '{hostname}:{port}': {exc}") from exc
|
|
32
|
+
|
|
33
|
+
if not cert:
|
|
34
|
+
raise RuntimeError(f"No certificate returned by '{hostname}:{port}'")
|
|
35
|
+
|
|
36
|
+
def _format_name(name_tuples: tuple) -> str:
|
|
37
|
+
parts = []
|
|
38
|
+
for attr_group in name_tuples:
|
|
39
|
+
for key, value in attr_group:
|
|
40
|
+
parts.append(f"{key}={value}")
|
|
41
|
+
return ", ".join(parts)
|
|
42
|
+
|
|
43
|
+
not_before = datetime.strptime(cert["notBefore"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
|
44
|
+
not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
|
45
|
+
now = datetime.now(tz=timezone.utc)
|
|
46
|
+
days_until_expiry = (not_after - now).days
|
|
47
|
+
|
|
48
|
+
san_entries = []
|
|
49
|
+
for san_type, san_value in cert.get("subjectAltName", ()):
|
|
50
|
+
san_entries.append(f"{san_type}:{san_value}")
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
"hostname": hostname,
|
|
54
|
+
"port": port,
|
|
55
|
+
"subject": _format_name(cert.get("subject", ())),
|
|
56
|
+
"issuer": _format_name(cert.get("issuer", ())),
|
|
57
|
+
"serial_number": cert.get("serialNumber", ""),
|
|
58
|
+
"version": cert.get("version", ""),
|
|
59
|
+
"not_before": not_before.isoformat(),
|
|
60
|
+
"not_after": not_after.isoformat(),
|
|
61
|
+
"days_until_expiry": days_until_expiry,
|
|
62
|
+
"expired": days_until_expiry < 0,
|
|
63
|
+
"subject_alt_names": san_entries,
|
|
64
|
+
"protocol": "TLSv1.2+",
|
|
65
|
+
}
|