certinspect 0.1.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.
- certinspect/__init__.py +3 -0
- certinspect/cli.py +169 -0
- certinspect/fetch.py +24 -0
- certinspect/formatter.py +81 -0
- certinspect/parser.py +123 -0
- certinspect-0.1.0.dist-info/METADATA +117 -0
- certinspect-0.1.0.dist-info/RECORD +11 -0
- certinspect-0.1.0.dist-info/WHEEL +5 -0
- certinspect-0.1.0.dist-info/entry_points.txt +2 -0
- certinspect-0.1.0.dist-info/licenses/LICENSE +21 -0
- certinspect-0.1.0.dist-info/top_level.txt +1 -0
certinspect/__init__.py
ADDED
certinspect/cli.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cli.py — Command-line entry point.
|
|
3
|
+
|
|
4
|
+
Wire together fetch -> parser -> formatter, reading the CLI arguments.
|
|
5
|
+
|
|
6
|
+
It fetches a certificate from one or more hosts (or reads a local file),
|
|
7
|
+
analyzes it, prints the result as human-readable text or JSON, and exits
|
|
8
|
+
with a status code reflecting the worst certificate state found.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
import ssl
|
|
15
|
+
from certinspect.fetch import get_server_cert_der
|
|
16
|
+
from certinspect.parser import (
|
|
17
|
+
load_certificate,
|
|
18
|
+
analyze,
|
|
19
|
+
certificate_status,
|
|
20
|
+
hostname_matches,
|
|
21
|
+
to_pem,
|
|
22
|
+
)
|
|
23
|
+
from certinspect.formatter import format_human
|
|
24
|
+
|
|
25
|
+
# Exit codes reflecting the certificate status, kept distinct from
|
|
26
|
+
# argparse's usage error (2) and the generic runtime error (1).
|
|
27
|
+
EXIT_BY_STATUS = {
|
|
28
|
+
"VALID": 0,
|
|
29
|
+
"EXPIRING": 3,
|
|
30
|
+
"EXPIRED": 4,
|
|
31
|
+
"INVALID DATES": 4,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
36
|
+
"""Build and return the ArgumentParser."""
|
|
37
|
+
|
|
38
|
+
parser = argparse.ArgumentParser(
|
|
39
|
+
prog="certinspect",
|
|
40
|
+
description="Inspect a TLS certificate from a host or a local file.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"target",
|
|
44
|
+
nargs="*",
|
|
45
|
+
help=(
|
|
46
|
+
"One or more domain names to inspect (e.g. example.com). "
|
|
47
|
+
"Omit when using --file."
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--file",
|
|
52
|
+
help=(
|
|
53
|
+
"Path to a local certificate file (PEM or DER) to inspect "
|
|
54
|
+
"instead of a host."
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--port",
|
|
59
|
+
type=int,
|
|
60
|
+
default=443,
|
|
61
|
+
help="TCP port to connect to (default: 443).",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--json",
|
|
65
|
+
action="store_true",
|
|
66
|
+
help="Output the result as JSON instead of human-readable text.",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--days",
|
|
70
|
+
type=int,
|
|
71
|
+
default=30,
|
|
72
|
+
help=("Warn if the certificate expires within this many days (default: 30)."),
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--export",
|
|
76
|
+
metavar="PATH",
|
|
77
|
+
help="Save the inspected certificate as a PEM file at PATH.",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return parser
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _fetch_bytes(target: str | None, port: int, file: str | None) -> bytes:
|
|
84
|
+
"""Return the raw certificate bytes for a single source (file or host)."""
|
|
85
|
+
if file:
|
|
86
|
+
with open(file, "rb") as f:
|
|
87
|
+
return f.read()
|
|
88
|
+
return get_server_cert_der(target, port)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _inspect(
|
|
92
|
+
target: str | None,
|
|
93
|
+
*,
|
|
94
|
+
port: int,
|
|
95
|
+
file: str | None,
|
|
96
|
+
days: int,
|
|
97
|
+
export: str | None,
|
|
98
|
+
) -> tuple[dict, int]:
|
|
99
|
+
"""Inspect one source and return its (info, exit_code).
|
|
100
|
+
|
|
101
|
+
The hostname match (and its exit code 5) only applies to host targets;
|
|
102
|
+
with --file it is left as None.
|
|
103
|
+
"""
|
|
104
|
+
cert = load_certificate(_fetch_bytes(target, port, file))
|
|
105
|
+
info = analyze(cert)
|
|
106
|
+
if export:
|
|
107
|
+
with open(export, "wb") as f:
|
|
108
|
+
f.write(to_pem(cert))
|
|
109
|
+
info["hostname_match"] = hostname_matches(info, target) if target else None
|
|
110
|
+
|
|
111
|
+
code = EXIT_BY_STATUS[certificate_status(info, days)]
|
|
112
|
+
if info["hostname_match"] is False:
|
|
113
|
+
code = 5
|
|
114
|
+
return info, code
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _render(
|
|
118
|
+
results: list[tuple[str | None, dict]], *, as_json: bool, days: int
|
|
119
|
+
) -> None:
|
|
120
|
+
"""Print the collected results as JSON (always a list) or human text."""
|
|
121
|
+
if as_json:
|
|
122
|
+
print(json.dumps([info for _, info in results], indent=2, default=str))
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
blocks = []
|
|
126
|
+
for target, info in results:
|
|
127
|
+
text = format_human(info, warn_days=days)
|
|
128
|
+
blocks.append(f"=== {target} ===\n{text}" if target else text)
|
|
129
|
+
if blocks:
|
|
130
|
+
print("\n\n".join(blocks))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main() -> None:
|
|
134
|
+
"""CLI entry point."""
|
|
135
|
+
|
|
136
|
+
parser = build_parser()
|
|
137
|
+
args = parser.parse_args()
|
|
138
|
+
|
|
139
|
+
if not args.target and not args.file:
|
|
140
|
+
parser.error("There is no target or no file to inspect.")
|
|
141
|
+
|
|
142
|
+
# A single --file source has no host; otherwise iterate over the targets.
|
|
143
|
+
targets: list[str | None] = [None] if args.file else list(args.target)
|
|
144
|
+
|
|
145
|
+
results: list[tuple[str | None, dict]] = []
|
|
146
|
+
codes: list[int] = []
|
|
147
|
+
for target in targets:
|
|
148
|
+
try:
|
|
149
|
+
info, code = _inspect(
|
|
150
|
+
target,
|
|
151
|
+
port=args.port,
|
|
152
|
+
file=args.file,
|
|
153
|
+
days=args.days,
|
|
154
|
+
export=args.export,
|
|
155
|
+
)
|
|
156
|
+
except (OSError, ssl.SSLError, ValueError) as err:
|
|
157
|
+
label = f"{target}: " if target else ""
|
|
158
|
+
print(f"error: {label}{err}", file=sys.stderr)
|
|
159
|
+
codes.append(1)
|
|
160
|
+
continue
|
|
161
|
+
results.append((target, info))
|
|
162
|
+
codes.append(code)
|
|
163
|
+
|
|
164
|
+
_render(results, as_json=args.json, days=args.days)
|
|
165
|
+
sys.exit(max(codes, default=0))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
certinspect/fetch.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fetch.py — Retrieving the certificate from the TLS server.
|
|
3
|
+
|
|
4
|
+
Connect to a host:port over TLS and obtain the server certificate in DER
|
|
5
|
+
format (bytes), ready to be parsed in parser.py.
|
|
6
|
+
|
|
7
|
+
Hostname checking and verification are disabled on purpose: this tool must
|
|
8
|
+
be able to inspect expired or self-signed certificates without the
|
|
9
|
+
connection failing. Validity is computed later in parser.py.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import socket
|
|
13
|
+
import ssl
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_server_cert_der(host: str, port: int = 443, timeout: float = 5.0) -> bytes:
|
|
17
|
+
"""Return the server certificate in DER format (bytes)."""
|
|
18
|
+
|
|
19
|
+
context = ssl.create_default_context()
|
|
20
|
+
context.check_hostname = False
|
|
21
|
+
context.verify_mode = ssl.CERT_NONE
|
|
22
|
+
with socket.create_connection((host, port), timeout=timeout) as sock:
|
|
23
|
+
with context.wrap_socket(sock, server_hostname=host) as ssock:
|
|
24
|
+
return ssock.getpeercert(binary_form=True)
|
certinspect/formatter.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
formatter.py — Presentation of the results to the user.
|
|
3
|
+
|
|
4
|
+
MODULE GOAL
|
|
5
|
+
-----------
|
|
6
|
+
Take the dict produced by parser.analyze() and produce human-readable output
|
|
7
|
+
(text) or machine-readable output (JSON).
|
|
8
|
+
|
|
9
|
+
GUIDED STEPS (write them yourself):
|
|
10
|
+
1. format_human(info: dict) -> str
|
|
11
|
+
- Print the fields in a tidy, readable way.
|
|
12
|
+
- Highlight the status: VALID / EXPIRED / NOT YET VALID.
|
|
13
|
+
- Show a WARNING if the days to expiry are < 30.
|
|
14
|
+
2. format_json(info: dict) -> str
|
|
15
|
+
- Serialize with json.dumps(info, default=str, indent=2).
|
|
16
|
+
- Note: dates are not serializable by default → use default=str
|
|
17
|
+
or convert them to ISO 8601 in parser.analyze().
|
|
18
|
+
|
|
19
|
+
Hint: keep color/emoji logic optional and simple.
|
|
20
|
+
Do not put analysis logic here: this module only PRESENTS the data.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
|
|
25
|
+
from certinspect.parser import certificate_status
|
|
26
|
+
|
|
27
|
+
LABEL_WIDTH = 16
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_human(info: dict, warn_days: int = 30) -> str:
|
|
31
|
+
"""Return a human-readable text representation."""
|
|
32
|
+
days = info["days_to_expire"]
|
|
33
|
+
status = certificate_status(info, warn_days)
|
|
34
|
+
|
|
35
|
+
def row(label: str, value: object) -> str:
|
|
36
|
+
return f"{label + ':':<{LABEL_WIDTH}}{value}"
|
|
37
|
+
|
|
38
|
+
lines = [
|
|
39
|
+
row("Subject", info["subject"]),
|
|
40
|
+
row("Status", status),
|
|
41
|
+
"",
|
|
42
|
+
row("Issuer", info["issuer"]),
|
|
43
|
+
row("Valid from", info["not_valid_before"]),
|
|
44
|
+
row("Valid until", info["not_valid_after"]),
|
|
45
|
+
row("Days to expiry", days),
|
|
46
|
+
"",
|
|
47
|
+
row("Serial number", info["serial_number"]),
|
|
48
|
+
row("Signature", info["signature_algorithm"]),
|
|
49
|
+
row("Key size", f"{info['key_size']} bit"),
|
|
50
|
+
row("Fingerprint", info["fingerprint_sha256"]),
|
|
51
|
+
row("CA", info["is_ca"]),
|
|
52
|
+
row("Self-Signed", info["self_signed"]),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
if info["weak"]:
|
|
56
|
+
lines.append("")
|
|
57
|
+
for reason in info["weak"]:
|
|
58
|
+
lines.append(f"WARNING: {reason}")
|
|
59
|
+
|
|
60
|
+
if info.get("hostname_match") is not None:
|
|
61
|
+
lines.append(row("Hostname match", info["hostname_match"]))
|
|
62
|
+
|
|
63
|
+
if 0 <= days < warn_days:
|
|
64
|
+
lines.append("")
|
|
65
|
+
lines.append(f"WARNING: certificate expires in {days} days")
|
|
66
|
+
|
|
67
|
+
san = info["san"]
|
|
68
|
+
lines.append("")
|
|
69
|
+
if san:
|
|
70
|
+
lines.append("SAN:")
|
|
71
|
+
lines.extend(f" - {name}" for name in san)
|
|
72
|
+
else:
|
|
73
|
+
lines.append(row("SAN", "(none)"))
|
|
74
|
+
|
|
75
|
+
return "\n".join(lines)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def format_json(info: dict) -> str:
|
|
79
|
+
"""Return a JSON representation."""
|
|
80
|
+
|
|
81
|
+
return json.dumps(info, indent=2, default=str)
|
certinspect/parser.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""X.509 certificate parsing and analysis.
|
|
2
|
+
|
|
3
|
+
Turns certificate bytes (DER or PEM) into a dictionary of fields ready to be
|
|
4
|
+
formatted for the user.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from cryptography import x509
|
|
9
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
10
|
+
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CertificateLoadError(ValueError): ...
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_pem(cert: x509.Certificate) -> bytes:
|
|
17
|
+
return cert.public_bytes(serialization.Encoding.PEM)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _name_matches(pattern: str, hostname: str) -> bool:
|
|
21
|
+
pattern = pattern.lower()
|
|
22
|
+
hostname = hostname.lower()
|
|
23
|
+
if pattern.startswith("*."):
|
|
24
|
+
head, _, tail = hostname.partition(".")
|
|
25
|
+
return bool(head) and tail == pattern[2:]
|
|
26
|
+
|
|
27
|
+
return pattern == hostname
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def hostname_matches(info: dict, hostname: str) -> bool:
|
|
31
|
+
"""Return True if hostname is covered by the certificate's SAN names."""
|
|
32
|
+
|
|
33
|
+
return any(_name_matches(name, hostname) for name in info["san"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def format_fingerprint(cert: x509.Certificate) -> str:
|
|
37
|
+
return ":".join(f"{b:02X}" for b in cert.fingerprint(hashes.SHA256()))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _weak_key(public_key) -> str | None:
|
|
41
|
+
"""Return a warning if the key is below safe size for its type.
|
|
42
|
+
|
|
43
|
+
RSA/DSA need >= 2048 bit; EC needs >= 256 bit. Other key types
|
|
44
|
+
(e.g. Ed25519) are always considered strong.
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(public_key, ec.EllipticCurvePublicKey):
|
|
47
|
+
if public_key.key_size < 256:
|
|
48
|
+
return f"Weak EC key ({public_key.key_size} bit)"
|
|
49
|
+
elif isinstance(public_key, (rsa.RSAPublicKey, dsa.DSAPublicKey)):
|
|
50
|
+
if public_key.key_size < 2048:
|
|
51
|
+
return f"Weak key ({public_key.key_size} bit)"
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def load_certificate(data: bytes) -> x509.Certificate:
|
|
56
|
+
"""Load a certificate from DER or PEM bytes."""
|
|
57
|
+
|
|
58
|
+
if not data:
|
|
59
|
+
raise CertificateLoadError("There is no certificate to load.")
|
|
60
|
+
try:
|
|
61
|
+
return x509.load_der_x509_certificate(data)
|
|
62
|
+
except ValueError:
|
|
63
|
+
return x509.load_pem_x509_certificate(data)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def analyze(cert: x509.Certificate) -> dict:
|
|
67
|
+
"""Extract the relevant information from the certificate as a dict."""
|
|
68
|
+
|
|
69
|
+
now = datetime.now(timezone.utc)
|
|
70
|
+
days_to_expire = (cert.not_valid_after_utc - now).days
|
|
71
|
+
try:
|
|
72
|
+
ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
|
|
73
|
+
san = ext.value.get_values_for_type(x509.DNSName)
|
|
74
|
+
except x509.ExtensionNotFound:
|
|
75
|
+
san = []
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
ext = cert.extensions.get_extension_for_class(x509.BasicConstraints)
|
|
79
|
+
is_ca = ext.value.ca
|
|
80
|
+
except x509.ExtensionNotFound:
|
|
81
|
+
is_ca = False
|
|
82
|
+
|
|
83
|
+
weak = []
|
|
84
|
+
reason = _weak_key(cert.public_key())
|
|
85
|
+
if reason:
|
|
86
|
+
weak.append(reason)
|
|
87
|
+
sig = cert.signature_algorithm_oid._name.lower()
|
|
88
|
+
if "sha1" in sig or "md5" in sig:
|
|
89
|
+
weak.append(f"Weak signature ({cert.signature_algorithm_oid._name})")
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
"subject": cert.subject.rfc4514_string(),
|
|
93
|
+
"issuer": cert.issuer.rfc4514_string(),
|
|
94
|
+
"not_valid_before": cert.not_valid_before_utc,
|
|
95
|
+
"not_valid_after": cert.not_valid_after_utc,
|
|
96
|
+
"serial_number": cert.serial_number,
|
|
97
|
+
"signature_algorithm": cert.signature_algorithm_oid._name,
|
|
98
|
+
"days_to_expire": days_to_expire,
|
|
99
|
+
"key_size": cert.public_key().key_size,
|
|
100
|
+
"san": san,
|
|
101
|
+
"fingerprint_sha256": format_fingerprint(cert),
|
|
102
|
+
"is_ca": is_ca,
|
|
103
|
+
"self_signed": cert.subject == cert.issuer,
|
|
104
|
+
"weak": weak,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def certificate_status(info: dict, warn_days: int = 30) -> str:
|
|
109
|
+
"""Return the validity status derived from the analyzed data.
|
|
110
|
+
|
|
111
|
+
One of: 'INVALID DATES', 'EXPIRED', 'EXPIRING', 'VALID'.
|
|
112
|
+
'EXPIRING' means the certificate is still valid but expires within
|
|
113
|
+
``warn_days`` days.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
if info["not_valid_before"] > info["not_valid_after"]:
|
|
117
|
+
return "INVALID DATES"
|
|
118
|
+
days = info["days_to_expire"]
|
|
119
|
+
if days < 0:
|
|
120
|
+
return "EXPIRED"
|
|
121
|
+
if days < warn_days:
|
|
122
|
+
return "EXPIRING"
|
|
123
|
+
return "VALID"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: certinspect
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Ispettore di certificati TLS da riga di comando
|
|
5
|
+
Author: Michele Angrisano
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/mangrisano/certinspect
|
|
8
|
+
Project-URL: Repository, https://github.com/mangrisano/certinspect
|
|
9
|
+
Project-URL: Issues, https://github.com/mangrisano/certinspect/issues
|
|
10
|
+
Keywords: tls,ssl,certificate,x509,cli
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: Security :: Cryptography
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: cryptography>=42.0.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.4.0; extra == "dev"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# certinspect
|
|
24
|
+
|
|
25
|
+
Ispettore di certificati TLS da riga di comando.
|
|
26
|
+
|
|
27
|
+
Dato uno o più domini (o un file `.pem`/`.der`), mostra validità, giorni alla
|
|
28
|
+
scadenza, soggetto, issuer, SAN, algoritmo di firma, dimensione della chiave,
|
|
29
|
+
fingerprint SHA-256, flag CA, self-signed, eventuali debolezze crittografiche e
|
|
30
|
+
la corrispondenza dell'hostname.
|
|
31
|
+
|
|
32
|
+
## Requisiti
|
|
33
|
+
|
|
34
|
+
- Python >= 3.14
|
|
35
|
+
|
|
36
|
+
## Installazione (sviluppo)
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python3.14 -m venv .venv
|
|
40
|
+
source .venv/bin/activate
|
|
41
|
+
pip install -r requirements.txt
|
|
42
|
+
pip install -e ".[dev]"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Uso
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Ispeziona un host
|
|
49
|
+
certinspect example.com
|
|
50
|
+
|
|
51
|
+
# Più host in una volta (modalità batch)
|
|
52
|
+
certinspect example.com github.com api.example.com
|
|
53
|
+
|
|
54
|
+
# Porta personalizzata
|
|
55
|
+
certinspect example.com --port 8443
|
|
56
|
+
|
|
57
|
+
# Output JSON (sempre una lista di oggetti)
|
|
58
|
+
certinspect example.com --json
|
|
59
|
+
|
|
60
|
+
# Ispeziona un certificato locale
|
|
61
|
+
certinspect --file ./certificato.pem
|
|
62
|
+
|
|
63
|
+
# Soglia di avviso scadenza personalizzata (default: 30 giorni)
|
|
64
|
+
certinspect example.com --days 14
|
|
65
|
+
|
|
66
|
+
# Salva il certificato scaricato in formato PEM
|
|
67
|
+
certinspect example.com --export ./scaricato.pem
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Opzioni
|
|
71
|
+
|
|
72
|
+
| Opzione | Descrizione |
|
|
73
|
+
| --------------- | -------------------------------------------------------------- |
|
|
74
|
+
| `target...` | Uno o più domini da ispezionare. Ometti quando usi `--file`. |
|
|
75
|
+
| `--file PATH` | Ispeziona un certificato locale (PEM o DER) invece di un host. |
|
|
76
|
+
| `--port N` | Porta TCP a cui connettersi (default: 443). |
|
|
77
|
+
| `--json` | Stampa il risultato come JSON invece del testo leggibile. |
|
|
78
|
+
| `--days N` | Avvisa se il certificato scade entro N giorni (default: 30). |
|
|
79
|
+
| `--export PATH` | Salva il certificato ispezionato come file PEM in PATH. |
|
|
80
|
+
|
|
81
|
+
## Exit code
|
|
82
|
+
|
|
83
|
+
Pensati per l'automazione (cron, CI, script di monitoraggio). In modalità batch
|
|
84
|
+
viene restituito il caso peggiore tra tutti i target.
|
|
85
|
+
|
|
86
|
+
| Code | Significato |
|
|
87
|
+
| ---- | -------------------------------------------- |
|
|
88
|
+
| 0 | Certificato valido |
|
|
89
|
+
| 1 | Errore di runtime (rete, file, parsing) |
|
|
90
|
+
| 2 | Errore negli argomenti della riga di comando |
|
|
91
|
+
| 3 | In scadenza entro la soglia `--days` |
|
|
92
|
+
| 4 | Scaduto o con date non valide |
|
|
93
|
+
| 5 | L'hostname non corrisponde al certificato |
|
|
94
|
+
|
|
95
|
+
Esempio in uno script:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
certinspect tuosito.it --days 21
|
|
99
|
+
case $? in
|
|
100
|
+
0) ;; # tutto ok
|
|
101
|
+
3) echo "In scadenza" | mail -s "Avviso" tu@mail.it ;;
|
|
102
|
+
4) echo "Scaduto" | mail -s "Urgente" tu@mail.it ;;
|
|
103
|
+
5) echo "Host errato" | mail -s "Urgente" tu@mail.it ;;
|
|
104
|
+
*) echo "Check fallito" ;;
|
|
105
|
+
esac
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Sviluppo
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Test
|
|
112
|
+
pytest
|
|
113
|
+
|
|
114
|
+
# Lint e formattazione (Ruff)
|
|
115
|
+
ruff check src tests
|
|
116
|
+
ruff format src tests
|
|
117
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
certinspect/__init__.py,sha256=P2kUejoL_94FrH4bxiJ5VO-TCOGIG_t2I4dke_u1Vt8,94
|
|
2
|
+
certinspect/cli.py,sha256=DlrYz9N7ibC1jdOZhFLfq629Gl6_AVtAxj6hxZQLS50,4712
|
|
3
|
+
certinspect/fetch.py,sha256=U6ayjyAdDLC0JuDdmxGFQo8gFw_DHFVTFQdx5uAghiU,890
|
|
4
|
+
certinspect/formatter.py,sha256=wuJDbkUmdWlsun7HcMkqPDdXCokosp20R6cAe9yvSw8,2478
|
|
5
|
+
certinspect/parser.py,sha256=H3rd0YWKVDbcbQeh7baM4FQtajQ8fwQyaLVNE-3CKwM,4036
|
|
6
|
+
certinspect-0.1.0.dist-info/licenses/LICENSE,sha256=sPNtfuKYItXUOAQoOMJDaYVd-xqfnNMN80sMYubj04E,1074
|
|
7
|
+
certinspect-0.1.0.dist-info/METADATA,sha256=xh0_PS1c2ymDuvRUG9SYfBogs6kA6t9CMITfDjbHa5c,3622
|
|
8
|
+
certinspect-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
9
|
+
certinspect-0.1.0.dist-info/entry_points.txt,sha256=bhS-GAMCRbi-RaZ-oppKA1Ib862in5eu0dOR5Gcid6s,53
|
|
10
|
+
certinspect-0.1.0.dist-info/top_level.txt,sha256=WAwsULyGErsoAXIgN_q9OlutSDBea50QBp_ZwERtjIA,12
|
|
11
|
+
certinspect-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michele Angrisano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
certinspect
|