certinspect 2.3.0__tar.gz → 2.3.2__tar.gz

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.
Files changed (31) hide show
  1. {certinspect-2.3.0/src/certinspect.egg-info → certinspect-2.3.2}/PKG-INFO +1 -1
  2. {certinspect-2.3.0 → certinspect-2.3.2}/pyproject.toml +1 -1
  3. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect/__init__.py +1 -1
  4. certinspect-2.3.2/src/certinspect/args.py +540 -0
  5. certinspect-2.3.2/src/certinspect/cli.py +747 -0
  6. certinspect-2.3.2/src/certinspect/completion.py +58 -0
  7. certinspect-2.3.2/src/certinspect/config.py +64 -0
  8. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect/discover.py +1 -1
  9. certinspect-2.3.2/src/certinspect/exit_codes.py +53 -0
  10. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect/fetch.py +2 -318
  11. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect/formatter.py +21 -18
  12. certinspect-2.3.2/src/certinspect/httpfetch.py +84 -0
  13. certinspect-2.3.2/src/certinspect/models.py +51 -0
  14. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect/parser.py +15 -13
  15. certinspect-2.3.2/src/certinspect/render.py +140 -0
  16. certinspect-2.3.2/src/certinspect/revocation.py +257 -0
  17. {certinspect-2.3.0 → certinspect-2.3.2/src/certinspect.egg-info}/PKG-INFO +1 -1
  18. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect.egg-info/SOURCES.txt +8 -0
  19. {certinspect-2.3.0 → certinspect-2.3.2}/tests/test_fetch.py +47 -47
  20. certinspect-2.3.0/src/certinspect/cli.py +0 -1456
  21. {certinspect-2.3.0 → certinspect-2.3.2}/LICENSE +0 -0
  22. {certinspect-2.3.0 → certinspect-2.3.2}/README.md +0 -0
  23. {certinspect-2.3.0 → certinspect-2.3.2}/setup.cfg +0 -0
  24. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect.egg-info/dependency_links.txt +0 -0
  25. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect.egg-info/entry_points.txt +0 -0
  26. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect.egg-info/requires.txt +0 -0
  27. {certinspect-2.3.0 → certinspect-2.3.2}/src/certinspect.egg-info/top_level.txt +0 -0
  28. {certinspect-2.3.0 → certinspect-2.3.2}/tests/test_cli.py +0 -0
  29. {certinspect-2.3.0 → certinspect-2.3.2}/tests/test_discover.py +0 -0
  30. {certinspect-2.3.0 → certinspect-2.3.2}/tests/test_formatter.py +0 -0
  31. {certinspect-2.3.0 → certinspect-2.3.2}/tests/test_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: certinspect
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Command-line TLS certificate inspector
5
5
  Author-email: Michele Angrisano <michele.angrisano@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "certinspect"
3
- version = "2.3.0"
3
+ version = "2.3.2"
4
4
  description = "Command-line TLS certificate inspector"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -1,3 +1,3 @@
1
1
  """certinspect — command-line TLS certificate inspector."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.3.2"
@@ -0,0 +1,540 @@
1
+ """Command-line argument parser definition.
2
+
3
+ Builds the full argparse parser for certinspect. Kept separate from the
4
+ orchestration in ``cli`` so the wall of flag declarations does not crowd the
5
+ run logic; the completion and --config machinery introspect this parser.
6
+ """
7
+
8
+ import argparse
9
+
10
+ from certinspect import __version__
11
+ from certinspect.config import _DEFAULT_CONFIG_PATH
12
+ from certinspect.fetch import STARTTLS_PORTS
13
+ from certinspect.parser import POLICY_PROFILES
14
+
15
+
16
+ def build_parser() -> argparse.ArgumentParser:
17
+ """Build and return the ArgumentParser."""
18
+ parser = argparse.ArgumentParser(
19
+ prog="certinspect",
20
+ description="Inspect a TLS certificate from a host or a local file.",
21
+ )
22
+ parser.add_argument(
23
+ "--version",
24
+ action="version",
25
+ version=f"%(prog)s {__version__}",
26
+ )
27
+ parser.add_argument(
28
+ "--config",
29
+ metavar="PATH",
30
+ help=(
31
+ f"TOML file of default option values (keys use the argparse "
32
+ f"destination name, e.g. 'days = 14' or 'verify = false'); an "
33
+ f"explicit command-line flag always overrides it. Without "
34
+ f"--config, {_DEFAULT_CONFIG_PATH} is loaded automatically if it "
35
+ "exists."
36
+ ),
37
+ )
38
+ parser.add_argument(
39
+ "--print-completion",
40
+ choices=("bash", "zsh"),
41
+ dest="print_completion",
42
+ help=(
43
+ "Print a shell completion script for bash or zsh to stdout, then "
44
+ "exit; e.g. 'certinspect --print-completion bash > "
45
+ "/etc/bash_completion.d/certinspect'. Generated from this parser, "
46
+ "so it always matches the installed version's flags."
47
+ ),
48
+ )
49
+ parser.add_argument(
50
+ "target",
51
+ nargs="*",
52
+ help=(
53
+ "One or more domain names to inspect (e.g. example.com). "
54
+ "Omit when using --file."
55
+ ),
56
+ )
57
+ parser.add_argument(
58
+ "--file",
59
+ action="append",
60
+ help=(
61
+ "Path to a local certificate file (PEM or DER) to inspect "
62
+ "instead of a host; use '-' to read the certificate from standard "
63
+ "input. Repeat the flag to inspect several files in one run (each "
64
+ "is reported separately); at most one may be '-'. Cannot be "
65
+ "combined with host targets."
66
+ ),
67
+ )
68
+ parser.add_argument(
69
+ "--port",
70
+ type=int,
71
+ default=443,
72
+ help="TCP port to connect to (default: 443).",
73
+ )
74
+ parser.add_argument(
75
+ "--timeout",
76
+ type=float,
77
+ default=5.0,
78
+ help="Connection timeout in seconds (default: 5).",
79
+ )
80
+ parser.add_argument(
81
+ "--connect-timeout",
82
+ type=float,
83
+ default=None,
84
+ dest="connect_timeout",
85
+ help="TCP connect timeout in seconds (default: --timeout).",
86
+ )
87
+ parser.add_argument(
88
+ "--read-timeout",
89
+ type=float,
90
+ default=None,
91
+ dest="read_timeout",
92
+ help="Handshake/read timeout in seconds (default: --timeout).",
93
+ )
94
+ parser.add_argument(
95
+ "--retries",
96
+ type=int,
97
+ default=0,
98
+ help="Retry transient connection failures this many times (default: 0).",
99
+ )
100
+ # --json, --csv and --exporter select the output format and are mutually
101
+ # exclusive; argparse rejects any combination for us (exit code 2).
102
+ output_group = parser.add_mutually_exclusive_group()
103
+ output_group.add_argument(
104
+ "--json",
105
+ action="store_true",
106
+ help="Output the result as JSON instead of human-readable text.",
107
+ )
108
+ output_group.add_argument(
109
+ "--field",
110
+ action="append",
111
+ metavar="NAME",
112
+ dest="field",
113
+ help=(
114
+ "Print only the given field(s), one tab-separated line per target "
115
+ "(e.g. --field days_to_expire). Repeat for several fields; use "
116
+ "'target' for the inspected host. Handy for scripting without "
117
+ "piping --json through a JSON tool."
118
+ ),
119
+ )
120
+ output_group.add_argument(
121
+ "--csv",
122
+ action="store_true",
123
+ help=(
124
+ "Output the results as CSV (one row per target, with a header), "
125
+ "convenient for spreadsheets."
126
+ ),
127
+ )
128
+ parser.add_argument(
129
+ "--csv-delimiter",
130
+ default=",",
131
+ metavar="SEP",
132
+ help=(
133
+ "Field separator for --csv (default: ','). Use ';' for Numbers or "
134
+ "Excel in locales that expect it (e.g. Italian)."
135
+ ),
136
+ )
137
+ parser.add_argument(
138
+ "--quiet",
139
+ action="store_true",
140
+ help="Only print certificates that have a problem.",
141
+ )
142
+ parser.add_argument(
143
+ "--schema",
144
+ type=int,
145
+ choices=[1, 2],
146
+ default=2,
147
+ help=(
148
+ "JSON schema version for --json (default: 2). Version 2 nests fields "
149
+ "by domain, uses ISO 8601 dates, stringifies the serial number and "
150
+ "wraps results in a versioned object; version 1 is the legacy flat "
151
+ "array."
152
+ ),
153
+ )
154
+ verify_group = parser.add_mutually_exclusive_group()
155
+ verify_group.add_argument(
156
+ "--verify",
157
+ dest="verify",
158
+ action="store_true",
159
+ default=True,
160
+ help=(
161
+ "Verify the certificate chain against the system trust store "
162
+ "(the default): a verified handshake plus OCSP/CRL revocation for a "
163
+ "host, or the bundle validated offline for --file. Kept for "
164
+ "explicitness; verification is on unless --no-verify is given."
165
+ ),
166
+ )
167
+ verify_group.add_argument(
168
+ "--no-verify",
169
+ dest="verify",
170
+ action="store_false",
171
+ help=(
172
+ "Skip chain verification (and revocation for hosts); only inspect "
173
+ "the certificate itself."
174
+ ),
175
+ )
176
+ parser.add_argument(
177
+ "--cafile",
178
+ metavar="PATH",
179
+ help=(
180
+ "Verify the chain against this CA bundle (PEM) instead of the "
181
+ "system trust store; useful behind an internal/private PKI. "
182
+ "Incompatible with --no-verify."
183
+ ),
184
+ )
185
+ parser.add_argument(
186
+ "--capath",
187
+ metavar="DIR",
188
+ help=(
189
+ "Verify the chain against the hashed CA certificates in this "
190
+ "directory (OpenSSL c_rehash layout) instead of the system trust "
191
+ "store. Incompatible with --no-verify; may be combined with --cafile."
192
+ ),
193
+ )
194
+ parser.add_argument(
195
+ "--chain",
196
+ action="store_true",
197
+ help=(
198
+ "Show the certificate chain: the one presented by the server for a "
199
+ "host, or every certificate in the bundle for --file."
200
+ ),
201
+ )
202
+ parser.add_argument(
203
+ "--client-cert",
204
+ metavar="PATH",
205
+ dest="client_cert",
206
+ help=(
207
+ "Present a client certificate (PEM) for mutual-TLS endpoints (host "
208
+ "targets only). If the file does not also contain the private key, "
209
+ "pass it with --client-key."
210
+ ),
211
+ )
212
+ parser.add_argument(
213
+ "--client-key",
214
+ metavar="PATH",
215
+ dest="client_key",
216
+ help=(
217
+ "Private key (PEM) for --client-cert, when it is stored separately "
218
+ "from the certificate."
219
+ ),
220
+ )
221
+ parser.add_argument(
222
+ "--proxy",
223
+ metavar="URL",
224
+ help=(
225
+ "Tunnel the connection through an HTTP CONNECT proxy, e.g. "
226
+ "http://proxy:8080 or http://user:pass@proxy:8080 (host targets "
227
+ "only). With no --proxy the environment proxy (HTTPS_PROXY, honouring "
228
+ "NO_PROXY) is used automatically, like curl."
229
+ ),
230
+ )
231
+ parser.add_argument(
232
+ "--no-proxy",
233
+ action="store_true",
234
+ dest="no_proxy",
235
+ help=(
236
+ "Force a direct connection, ignoring any proxy set in the "
237
+ "environment. Mutually exclusive with --proxy."
238
+ ),
239
+ )
240
+ parser.add_argument(
241
+ "--pin",
242
+ metavar="SHA256",
243
+ help=(
244
+ "Expected SHA-256 fingerprint; exit with code 7 if it does not "
245
+ "match (colons and case are ignored)."
246
+ ),
247
+ )
248
+ parser.add_argument(
249
+ "--servername",
250
+ metavar="NAME",
251
+ help=(
252
+ "Override the SNI hostname sent in the TLS handshake (host targets "
253
+ "only). Lets you reach a specific backend by IP or DNS name while "
254
+ "presenting the virtual host a load balancer routes on; the "
255
+ "hostname match is checked against this name instead of the target."
256
+ ),
257
+ )
258
+ parser.add_argument(
259
+ "--expect-san",
260
+ metavar="NAME",
261
+ action="append",
262
+ dest="expect_san",
263
+ help=(
264
+ "Assert that the certificate's SAN covers NAME (wildcards honored); "
265
+ "exit with code 8 if any expected name is missing. Repeat the flag "
266
+ "to require several names. Works for host and --file targets."
267
+ ),
268
+ )
269
+ parser.add_argument(
270
+ "--input",
271
+ metavar="PATH",
272
+ help=(
273
+ "Read additional targets from a file (one per line, '#' comments "
274
+ "allowed); use '-' to read from standard input."
275
+ ),
276
+ )
277
+ parser.add_argument(
278
+ "--discover",
279
+ metavar="DOMAIN",
280
+ action="append",
281
+ help=(
282
+ "Discover hostnames from Certificate Transparency logs (crt.sh) for "
283
+ "DOMAIN and inspect each one, surfacing forgotten or shadow "
284
+ "certificates. Repeat the flag for several domains. Host targets "
285
+ "only; the crt.sh query uses --discover-timeout."
286
+ ),
287
+ )
288
+ parser.add_argument(
289
+ "--discover-only",
290
+ action="store_true",
291
+ dest="discover_only",
292
+ help=(
293
+ "With --discover, list the certificates Certificate Transparency "
294
+ "knows for the domain(s) — expiry, issuer and hostnames, "
295
+ "tab-separated, soonest expiry first — without connecting to any "
296
+ "host, then exit. Wildcard certificates are included. Handy for a "
297
+ "fast CT inventory or spotting a certificate from an unexpected CA."
298
+ ),
299
+ )
300
+ parser.add_argument(
301
+ "--expect-issuer",
302
+ metavar="SUBSTRING",
303
+ action="append",
304
+ dest="expect_issuer",
305
+ help=(
306
+ "With --discover-only, flag any certificate whose issuer contains "
307
+ "none of these substrings (case-insensitive) and exit with code 9, "
308
+ "turning the CT inventory into a mis-issuance check. Repeat to "
309
+ 'allow several issuers, e.g. --expect-issuer "Let\'s Encrypt".'
310
+ ),
311
+ )
312
+ parser.add_argument(
313
+ "--discover-timeout",
314
+ type=float,
315
+ default=30.0,
316
+ metavar="N",
317
+ dest="discover_timeout",
318
+ help=(
319
+ "Timeout in seconds for the Certificate Transparency (crt.sh) query "
320
+ "used by --discover (default: 30). Separate from --timeout, since "
321
+ "the log search can be slower than a TLS handshake."
322
+ ),
323
+ )
324
+ parser.add_argument(
325
+ "--days",
326
+ type=int,
327
+ default=30,
328
+ help=("Warn if the certificate expires within this many days (default: 30)."),
329
+ )
330
+ parser.add_argument(
331
+ "--critical-days",
332
+ type=int,
333
+ default=None,
334
+ metavar="N",
335
+ help=(
336
+ "Escalate to CRITICAL (exit code 4) when the certificate expires "
337
+ "within this many days. Must be <= --days; lets monitoring "
338
+ "distinguish a warning window from a critical one."
339
+ ),
340
+ )
341
+ parser.add_argument(
342
+ "--profile",
343
+ choices=tuple(POLICY_PROFILES),
344
+ default=None,
345
+ help=(
346
+ "Apply a named bundle of the opt-in policy checks (exit code 9) in "
347
+ "one go. Intensity ladder, not an official standard: 'lenient' = "
348
+ "TLS >= 1.2 and fail on weak crypto; 'standard' adds a 2048-bit "
349
+ "minimum key; 'strict' = TLS >= 1.3, 2048-bit key, weak-crypto "
350
+ "failure, required Certificate Transparency SCTs and the CA/Browser "
351
+ "Forum validity cap. Any explicit flag overrides the profile; the "
352
+ "TLS-version part applies to host targets only. Passing a profile "
353
+ "is not a compliance attestation."
354
+ ),
355
+ )
356
+ # --not-after-max and --cab-forum both cap the total validity; the latter
357
+ # resolves to the current CA/Browser Forum maximum, so they are mutually
358
+ # exclusive (argparse rejects any combination with exit code 2).
359
+ validity_group = parser.add_mutually_exclusive_group()
360
+ validity_group.add_argument(
361
+ "--not-after-max",
362
+ type=int,
363
+ default=None,
364
+ metavar="N",
365
+ dest="not_after_max",
366
+ help=(
367
+ "Fail (exit code 9) when the certificate's total validity exceeds N "
368
+ "days. Use 398 to enforce the current CA/Browser Forum maximum. "
369
+ "Opt-in policy check; works for host and --file targets."
370
+ ),
371
+ )
372
+ validity_group.add_argument(
373
+ "--cab-forum",
374
+ action="store_true",
375
+ dest="cab_forum",
376
+ help=(
377
+ "Fail (exit code 9) when the total validity exceeds the CA/Browser "
378
+ "Forum maximum in effect today (398 days now, then 200, 100 and 47 "
379
+ "on 2026/2027/2029-03-15). Date-aware shorthand for --not-after-max."
380
+ ),
381
+ )
382
+ parser.add_argument(
383
+ "--min-key-size",
384
+ type=int,
385
+ default=None,
386
+ metavar="N",
387
+ dest="min_key_size",
388
+ help=(
389
+ "Fail (exit code 9) when the public key is smaller than N bits "
390
+ "(e.g. 2048 for RSA). Opt-in policy check."
391
+ ),
392
+ )
393
+ parser.add_argument(
394
+ "--fail-weak",
395
+ action="store_true",
396
+ dest="fail_weak",
397
+ help=(
398
+ "Turn the weak-crypto warnings (small key, SHA-1/MD5 signature) "
399
+ "into a hard failure (exit code 9) instead of a mere warning."
400
+ ),
401
+ )
402
+ parser.add_argument(
403
+ "--require-sct",
404
+ action="store_true",
405
+ dest="require_sct",
406
+ help=(
407
+ "Fail (exit code 9) when the certificate embeds no Signed "
408
+ "Certificate Timestamps (Certificate Transparency). Only the SCTs "
409
+ "embedded in the certificate are checked, not those delivered over "
410
+ "the TLS handshake or OCSP. Opt-in policy check."
411
+ ),
412
+ )
413
+ parser.add_argument(
414
+ "--require-must-staple",
415
+ action="store_true",
416
+ dest="require_must_staple",
417
+ help=(
418
+ "Fail (exit code 9) when the certificate lacks the OCSP Must-Staple "
419
+ "extension (RFC 7633 TLS Feature status_request). Opt-in policy "
420
+ "check."
421
+ ),
422
+ )
423
+ parser.add_argument(
424
+ "--require-revocation-check",
425
+ action="store_true",
426
+ dest="require_revocation_check",
427
+ help=(
428
+ "Fail (exit code 9) unless OCSP or CRL returns a definitive GOOD "
429
+ "revocation verdict. Host targets only; opt-in policy check."
430
+ ),
431
+ )
432
+ parser.add_argument(
433
+ "--min-tls-version",
434
+ choices=("TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"),
435
+ default=None,
436
+ dest="min_tls_version",
437
+ help=(
438
+ "Fail (exit code 9) when the connection negotiates a TLS version "
439
+ "older than this (e.g. TLSv1.2). Opt-in policy check; host targets "
440
+ "only, as it needs a live handshake."
441
+ ),
442
+ )
443
+ parser.add_argument(
444
+ "--export",
445
+ metavar="PATH",
446
+ help="Save the inspected certificate as a PEM file at PATH.",
447
+ )
448
+ parser.add_argument(
449
+ "--starttls",
450
+ choices=tuple(STARTTLS_PORTS),
451
+ help=(
452
+ "Upgrade a plaintext connection to TLS before inspecting (host "
453
+ "targets only). When --port is left at its default, the protocol's "
454
+ "standard port is used (smtp=587, imap=143, pop3=110, ftp=21)."
455
+ ),
456
+ )
457
+ output_group.add_argument(
458
+ "--exporter",
459
+ choices=("nagios", "prometheus"),
460
+ help=(
461
+ "Emit machine-readable monitoring output instead of the normal "
462
+ "report: a Nagios/Icinga plugin line per target (exit code follows "
463
+ "the plugin convention) or Prometheus textfile metrics. Ignores "
464
+ "--quiet so every target is always reported."
465
+ ),
466
+ )
467
+ parser.add_argument(
468
+ "--concurrency",
469
+ type=int,
470
+ default=1,
471
+ metavar="N",
472
+ help=(
473
+ "Number of hosts to inspect in parallel in batch mode "
474
+ "(default: 1). Output order is preserved regardless of N."
475
+ ),
476
+ )
477
+ parser.add_argument(
478
+ "--max-days",
479
+ type=int,
480
+ default=None,
481
+ metavar="N",
482
+ help=(
483
+ "Only show certificates that expire within N days (already-expired "
484
+ "ones are always shown). Filters the output only; the exit code "
485
+ "still reflects every inspected target."
486
+ ),
487
+ )
488
+ parser.add_argument(
489
+ "--sort",
490
+ choices=("host", "expiry"),
491
+ default=None,
492
+ help=(
493
+ "Sort the output: 'host' alphabetically by target, 'expiry' by "
494
+ "days left (soonest first). Affects the display only, not the "
495
+ "exit code."
496
+ ),
497
+ )
498
+ parser.add_argument(
499
+ "--summary",
500
+ action="store_true",
501
+ help=(
502
+ "Print a one-line tally (valid/expiring/expired/errors) to stderr "
503
+ "after the report. Counts every inspected target, ignoring "
504
+ "--quiet/--max-days filtering."
505
+ ),
506
+ )
507
+ parser.add_argument(
508
+ "--exit-zero",
509
+ action="store_true",
510
+ dest="exit_zero",
511
+ help=(
512
+ "Always exit with code 0, even on problems or fetch errors. "
513
+ "Report-only mode for dashboards/CI that read the output rather "
514
+ "than the exit code."
515
+ ),
516
+ )
517
+ parser.add_argument(
518
+ "--state-file",
519
+ metavar="PATH",
520
+ dest="state_file",
521
+ help=(
522
+ "Persist each host target's status across runs at PATH (JSON) and "
523
+ "compare against the previous run. Combine with --only-changed to "
524
+ "show only the targets whose status changed; without it, the file "
525
+ "is simply kept up to date for the next run. Host targets only; a "
526
+ "target seen for the first time counts as changed."
527
+ ),
528
+ )
529
+ parser.add_argument(
530
+ "--only-changed",
531
+ action="store_true",
532
+ dest="only_changed",
533
+ help=(
534
+ "Show only targets whose status differs from the previous "
535
+ "--state-file run. Requires --state-file; affects the display "
536
+ "only, not the exit code."
537
+ ),
538
+ )
539
+
540
+ return parser