pcf-toolkit 0.2.5__py3-none-any.whl → 0.2.6__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.
pcf_toolkit/proxy/cli.py CHANGED
@@ -43,7 +43,7 @@ from pcf_toolkit.proxy.config import (
43
43
  render_dist_path,
44
44
  write_default_config,
45
45
  )
46
- from pcf_toolkit.proxy.doctor import CheckResult, run_doctor
46
+ from pcf_toolkit.proxy.doctor import CheckResult, check_mitmproxy_certificate, run_doctor
47
47
  from pcf_toolkit.proxy.mitm import ensure_mitmproxy, find_mitmproxy, spawn_mitmproxy
48
48
  from pcf_toolkit.proxy.server import spawn_http_server
49
49
  from pcf_toolkit.rich_help import RichTyperCommand, RichTyperGroup
@@ -365,6 +365,9 @@ def start(
365
365
  creationflags=creationflags,
366
366
  )
367
367
 
368
+ if not detach:
369
+ _maybe_prompt_for_cert_trust()
370
+
368
371
  if log_handle is not None:
369
372
  log_handle.close()
370
373
 
@@ -546,9 +549,84 @@ def _apply_fixes(results: list[CheckResult], config: ProxyConfig) -> None:
546
549
  except Exception as exc: # noqa: BLE001
547
550
  typer.secho(f"Failed to install mitmproxy: {exc}", fg=typer.colors.RED)
548
551
  if result.name == "mitmproxy_cert" and result.status == "warn":
552
+ if _is_interactive() and _supports_cert_install():
553
+ if typer.confirm(
554
+ "Install mitmproxy CA into the system trust store? This requires sudo.",
555
+ default=False,
556
+ ):
557
+ _install_mitmproxy_cert()
558
+ continue
549
559
  typer.secho(result.fix or "", fg=typer.colors.YELLOW)
550
560
 
551
561
 
562
+ def _supports_cert_install() -> bool:
563
+ return sys.platform == "darwin" or sys.platform.startswith("linux")
564
+
565
+
566
+ def _maybe_prompt_for_cert_trust() -> None:
567
+ cert_result = check_mitmproxy_certificate()
568
+ if cert_result.status == "ok":
569
+ return
570
+
571
+ typer.secho(f"[{cert_result.status.upper()}] {cert_result.name}: {cert_result.message}", fg=typer.colors.YELLOW)
572
+ if _is_interactive() and _supports_cert_install():
573
+ if typer.confirm(
574
+ "Install mitmproxy CA into the system trust store? This requires sudo.",
575
+ default=False,
576
+ ):
577
+ _install_mitmproxy_cert()
578
+ return
579
+ if cert_result.fix:
580
+ typer.secho(cert_result.fix, fg=typer.colors.YELLOW)
581
+
582
+
583
+ def _install_mitmproxy_cert() -> None:
584
+ cert_path = Path.home() / ".mitmproxy" / "mitmproxy-ca-cert.pem"
585
+ if not cert_path.exists():
586
+ typer.secho("mitmproxy CA cert not found. Run the proxy once to generate it.", fg=typer.colors.RED)
587
+ return
588
+
589
+ if sys.platform == "darwin":
590
+ _run_privileged(
591
+ [
592
+ "sudo",
593
+ "security",
594
+ "add-trusted-cert",
595
+ "-d",
596
+ "-r",
597
+ "trustRoot",
598
+ "-k",
599
+ "/Library/Keychains/System.keychain",
600
+ str(cert_path),
601
+ ],
602
+ "Trusting mitmproxy CA in System Keychain...",
603
+ )
604
+ return
605
+
606
+ if sys.platform.startswith("linux"):
607
+ if shutil.which("update-ca-certificates"):
608
+ target = Path("/usr/local/share/ca-certificates/mitmproxy-ca-cert.crt")
609
+ _run_privileged(["sudo", "cp", str(cert_path), str(target)], "Copying cert into CA store...")
610
+ _run_privileged(["sudo", "update-ca-certificates"], "Updating CA certificates...")
611
+ return
612
+ if shutil.which("update-ca-trust"):
613
+ target = Path("/etc/pki/ca-trust/source/anchors/mitmproxy-ca-cert.pem")
614
+ _run_privileged(["sudo", "cp", str(cert_path), str(target)], "Copying cert into CA trust anchors...")
615
+ _run_privileged(["sudo", "update-ca-trust", "extract"], "Updating CA trust...")
616
+ return
617
+
618
+ typer.secho("Automatic install not supported on this platform.", fg=typer.colors.YELLOW)
619
+
620
+
621
+ def _run_privileged(command: list[str], message: str) -> None:
622
+ typer.secho(message, fg=typer.colors.CYAN)
623
+ try:
624
+ subprocess.run(command, check=True)
625
+ typer.secho("Done.", fg=typer.colors.GREEN)
626
+ except subprocess.CalledProcessError as exc:
627
+ typer.secho(f"Command failed: {exc}", fg=typer.colors.RED)
628
+
629
+
552
630
  def _print_results(results: list[CheckResult]) -> None:
553
631
  """Prints doctor check results with color coding.
554
632
 
@@ -3,7 +3,9 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import shutil
6
7
  import socket
8
+ import subprocess
7
9
  import sys
8
10
  from dataclasses import dataclass
9
11
  from pathlib import Path
@@ -97,6 +99,11 @@ def run_doctor(
97
99
  return results
98
100
 
99
101
 
102
+ def check_mitmproxy_certificate() -> CheckResult:
103
+ """Returns a single check result for mitmproxy certificate trust."""
104
+ return _check_certificates()[0]
105
+
106
+
100
107
  def _check_ports(config: ProxyConfig) -> list[CheckResult]:
101
108
  """Checks if proxy and HTTP server ports are available.
102
109
 
@@ -168,11 +175,30 @@ def _check_certificates() -> list[CheckResult]:
168
175
  cert_dir = Path.home() / ".mitmproxy"
169
176
  cert_file = cert_dir / "mitmproxy-ca-cert.pem"
170
177
  if cert_file.exists():
178
+ trusted = _is_cert_trusted(cert_file)
179
+ if trusted is True:
180
+ return [
181
+ CheckResult(
182
+ name="mitmproxy_cert",
183
+ status="ok",
184
+ message="mitmproxy CA cert is trusted.",
185
+ )
186
+ ]
187
+ if trusted is False:
188
+ return [
189
+ CheckResult(
190
+ name="mitmproxy_cert",
191
+ status="warn",
192
+ message="mitmproxy CA cert exists but is not trusted.",
193
+ fix=_cert_fix_instructions(cert_dir),
194
+ )
195
+ ]
171
196
  return [
172
197
  CheckResult(
173
198
  name="mitmproxy_cert",
174
- status="ok",
175
- message=f"mitmproxy CA cert found at {cert_file}",
199
+ status="warn",
200
+ message="mitmproxy CA cert found but trust status is unknown.",
201
+ fix=_cert_fix_instructions(cert_dir),
176
202
  )
177
203
  ]
178
204
  return [
@@ -275,5 +301,68 @@ def _cert_fix_instructions(cert_dir: Path) -> str:
275
301
  if os.name == "nt":
276
302
  return f"Run: certutil -addstore -f Root {cert_path} (elevated)"
277
303
  if sys.platform == "darwin":
278
- return f"Run: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain {cert_path}"
279
- return f"Run: sudo cp {cert_path} /usr/local/share/ca-certificates/ && sudo update-ca-certificates"
304
+ return (
305
+ "Run: sudo security add-trusted-cert -d -r trustRoot "
306
+ f"-k /Library/Keychains/System.keychain {cert_path}"
307
+ )
308
+ if shutil.which("update-ca-certificates"):
309
+ return (
310
+ f"Run: sudo cp {cert_path} /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt "
311
+ "&& sudo update-ca-certificates"
312
+ )
313
+ if shutil.which("update-ca-trust"):
314
+ return (
315
+ f"Run: sudo cp {cert_path} /etc/pki/ca-trust/source/anchors/mitmproxy-ca-cert.pem "
316
+ "&& sudo update-ca-trust extract"
317
+ )
318
+ return f"Trust the cert manually: {cert_path}"
319
+
320
+
321
+ def _is_cert_trusted(cert_path: Path) -> bool | None:
322
+ """Best-effort check for whether the mitmproxy CA is trusted."""
323
+ if sys.platform == "darwin":
324
+ return _is_cert_trusted_macos(cert_path)
325
+ if sys.platform.startswith("linux"):
326
+ return _is_cert_trusted_linux(cert_path)
327
+ if os.name == "nt":
328
+ return _is_cert_trusted_windows(cert_path)
329
+ return None
330
+
331
+
332
+ def _is_cert_trusted_macos(cert_path: Path) -> bool | None:
333
+ try:
334
+ result = subprocess.run(
335
+ [
336
+ "security",
337
+ "find-certificate",
338
+ "-c",
339
+ "mitmproxy",
340
+ "-a",
341
+ "/Library/Keychains/System.keychain",
342
+ ],
343
+ capture_output=True,
344
+ text=True,
345
+ check=False,
346
+ )
347
+ except FileNotFoundError:
348
+ return None
349
+ if result.returncode != 0:
350
+ return False
351
+ return bool(result.stdout.strip())
352
+
353
+
354
+ def _is_cert_trusted_linux(cert_path: Path) -> bool:
355
+ candidates = [
356
+ Path("/usr/local/share/ca-certificates/mitmproxy-ca-cert.crt"),
357
+ Path("/etc/ssl/certs/mitmproxy-ca-cert.pem"),
358
+ Path("/etc/ssl/certs/mitmproxy-ca-cert.crt"),
359
+ Path("/etc/pki/ca-trust/source/anchors/mitmproxy-ca-cert.pem"),
360
+ Path("/etc/pki/ca-trust/source/anchors/mitmproxy-ca-cert.crt"),
361
+ ]
362
+ return any(path.exists() for path in candidates)
363
+
364
+
365
+ def _is_cert_trusted_windows(cert_path: Path) -> bool | None:
366
+ # Implementing a reliable trust-store check on Windows is non-trivial here.
367
+ # Fall back to unknown and let the CLI provide instructions.
368
+ return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pcf-toolkit
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: PCF toolkit CLI for manifest authoring and local proxy development.
5
5
  Author-email: Vectorfy Co - Andrew M <contact@vectorfy.co>
6
6
  License: <div align="center">
@@ -16,16 +16,16 @@ pcf_toolkit/data/schema_snapshot.json,sha256=87tr3NDBF9IshguUpxkNl-rNK3jpdHA9-2l
16
16
  pcf_toolkit/data/spec_raw.json,sha256=U6WtINyuJei3LLEfTOqyee_-tUfr_3UE-hkKE4S5oDQ,112224
17
17
  pcf_toolkit/proxy/__init__.py,sha256=ivhtZia35IJinxfZOoL2bcp7PyL0U7KxavqSp4nyDL0,37
18
18
  pcf_toolkit/proxy/browser.py,sha256=9a-sPvnM_W0Vg2nA_uYgSRmfaej6acgtl4OekDzI9wk,4561
19
- pcf_toolkit/proxy/cli.py,sha256=xIwbezSM0jgn-NZyfQNtq4MjVQz_t3FTZMAxD8eKTEI,55172
19
+ pcf_toolkit/proxy/cli.py,sha256=8U15hzZb9bLenJgoI9POaN4J8Ev2AxcdN7WahQFxBks,58181
20
20
  pcf_toolkit/proxy/config.py,sha256=-uKI96Fhc8CWKJ2Cws5yOpPzLVDd7ZeRhdvgC_hNDks,9753
21
- pcf_toolkit/proxy/doctor.py,sha256=XQTvPi7z9T3i0agcYTpjrlqe8zwlsM9mC-sc9f1y36s,7977
21
+ pcf_toolkit/proxy/doctor.py,sha256=VApvsScXbMmlj2dZY5xRQYyzS0eXtS4iOt41RVaMc44,10891
22
22
  pcf_toolkit/proxy/mitm.py,sha256=bKawnEqIsM8wASHfJGr1GQTeipSdHRwRrLNceXm8TTw,5624
23
23
  pcf_toolkit/proxy/server.py,sha256=Xt6dgw5MjBDP_GiHxD5iUQTAdULkyTP7e5erVWkNbfk,1152
24
24
  pcf_toolkit/proxy/addons/__init__.py,sha256=NI3FmO7tBhNGvy5O6r2cTgqxpmts8NVCQ4Sk2jNmlCM,32
25
25
  pcf_toolkit/proxy/addons/redirect_bundle.py,sha256=2neGzVjbUGjw7X94Si1MGcxi2rhdkcpVTNtA9Pua4g8,2072
26
- pcf_toolkit-0.2.5.dist-info/licenses/LICENSE.md,sha256=KVBxRZbs2pIY2yrA50_E8As1atJXuRw9tRAJ_vreyF4,8639
27
- pcf_toolkit-0.2.5.dist-info/METADATA,sha256=d17ad-HS-2gjsCzYuIPpkygPedcdHlEcD6LeKfvBcjQ,25773
28
- pcf_toolkit-0.2.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
- pcf_toolkit-0.2.5.dist-info/entry_points.txt,sha256=ieMDIFZ1D0i8lWqoUYQAtsbU3dzIt6vxx21jtDMncNo,52
30
- pcf_toolkit-0.2.5.dist-info/top_level.txt,sha256=0TURCNKgchkb-VxvC0o7jWJWJSp64d2ZVzZnRbj9GG8,12
31
- pcf_toolkit-0.2.5.dist-info/RECORD,,
26
+ pcf_toolkit-0.2.6.dist-info/licenses/LICENSE.md,sha256=KVBxRZbs2pIY2yrA50_E8As1atJXuRw9tRAJ_vreyF4,8639
27
+ pcf_toolkit-0.2.6.dist-info/METADATA,sha256=rVktwGe9uLifJEzM-1-z2NlnDrwiWiJj1VPYxzB63vE,25773
28
+ pcf_toolkit-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ pcf_toolkit-0.2.6.dist-info/entry_points.txt,sha256=ieMDIFZ1D0i8lWqoUYQAtsbU3dzIt6vxx21jtDMncNo,52
30
+ pcf_toolkit-0.2.6.dist-info/top_level.txt,sha256=0TURCNKgchkb-VxvC0o7jWJWJSp64d2ZVzZnRbj9GG8,12
31
+ pcf_toolkit-0.2.6.dist-info/RECORD,,