secpipw 1.0.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.
secpipw/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from secpipw.severity import Severity, parse_severity
2
+
3
+ __all__ = ["__version__", "Severity", "parse_severity"]
4
+
5
+ __version__ = "8.0"
secpipw/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from secpipw.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable
5
+
6
+ from secpipw.pypi_api import OfficialPyPIClient
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class CacheRefreshResult:
11
+ key: str
12
+ description: str
13
+ count: int
14
+ location: str
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class CacheRefreshTask:
19
+ key: str
20
+ description: str
21
+ run: Callable[[OfficialPyPIClient], CacheRefreshResult]
22
+
23
+
24
+ def _refresh_project_name_cache(client: OfficialPyPIClient) -> CacheRefreshResult:
25
+ count = client.refresh_project_name_cache()
26
+ return CacheRefreshResult(
27
+ key="pypi-project-names",
28
+ description="PyPI project name cache",
29
+ count=count,
30
+ location=str(client.cache_path),
31
+ )
32
+
33
+
34
+ REFRESH_TASKS: tuple[CacheRefreshTask, ...] = (
35
+ CacheRefreshTask(
36
+ key="pypi-project-names",
37
+ description="PyPI project name cache",
38
+ run=_refresh_project_name_cache,
39
+ ),
40
+ )
41
+
42
+
43
+ def refresh_all_caches(
44
+ client: OfficialPyPIClient | None = None,
45
+ ) -> list[CacheRefreshResult]:
46
+ client = client or OfficialPyPIClient()
47
+ return [task.run(client) for task in REFRESH_TASKS]
secpipw/cli.py ADDED
@@ -0,0 +1,470 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ from secpipw import __version__
6
+ from secpipw.severity import Severity, parse_severity
7
+
8
+ DEDICATED_TOOL_ENTRYPOINTS = {
9
+ "pipx": "spipx",
10
+ "poetry": "spoetry",
11
+ "uv": "suv",
12
+ }
13
+
14
+
15
+ def run_pip(*args, **kwargs):
16
+ from secpipw.pip_bridge import run_pip as impl
17
+
18
+ return impl(*args, **kwargs)
19
+
20
+
21
+ def run_tool(*args, **kwargs):
22
+ from secpipw.tool_bridge import run_tool as impl
23
+
24
+ return impl(*args, **kwargs)
25
+
26
+
27
+ def preflight_pip_args_for_tool(*args, **kwargs):
28
+ from secpipw.tool_bridge import preflight_pip_args_for_tool as impl
29
+
30
+ return impl(*args, **kwargs)
31
+
32
+
33
+ def tool_command_requires_preflight(*args, **kwargs):
34
+ from secpipw.tool_bridge import tool_command_requires_preflight as impl
35
+
36
+ return impl(*args, **kwargs)
37
+
38
+
39
+ def resolve_install_plan(*args, **kwargs):
40
+ from secpipw.install_plan import resolve_install_plan as impl
41
+
42
+ return impl(*args, **kwargs)
43
+
44
+
45
+ def inspect_install_plan_artifacts(*args, **kwargs):
46
+ from secpipw.tool_bridge import inspect_install_plan_artifacts as impl
47
+
48
+ return impl(*args, **kwargs)
49
+
50
+
51
+ def refresh_all_caches(*args, **kwargs):
52
+ from secpipw.cache_refresh import refresh_all_caches as impl
53
+
54
+ return impl(*args, **kwargs)
55
+
56
+
57
+ def OfficialPyPIClient(*args, **kwargs):
58
+ from secpipw.pypi_api import OfficialPyPIClient as client_class
59
+
60
+ return client_class(*args, **kwargs)
61
+
62
+
63
+ def _allow_install_decision():
64
+ from secpipw.warning_gate import GateDecision
65
+
66
+ return GateDecision(allow_install=True, exit_code=0)
67
+
68
+
69
+ def run_install_checks(*args, **kwargs):
70
+ from secpipw.install_checks import run_install_checks as impl
71
+
72
+ return impl(*args, **kwargs)
73
+
74
+
75
+ def run_guarded_pip_install(*args, **kwargs):
76
+ from secpipw.pip_guard import run_guarded_pip_install as impl
77
+
78
+ return impl(*args, **kwargs)
79
+
80
+
81
+ class PthMonitor:
82
+ @classmethod
83
+ def from_install_args(cls, *args, **kwargs):
84
+ from secpipw.pth_monitor import PthMonitor as monitor_class
85
+
86
+ return monitor_class.from_install_args(*args, **kwargs)
87
+
88
+
89
+ def gate_suspicious_pth_alerts(*args, **kwargs):
90
+ from secpipw.pth_monitor import gate_suspicious_pth_alerts as impl
91
+
92
+ return impl(*args, **kwargs)
93
+
94
+
95
+ def handle_suspicious_pth_alerts(*args, **kwargs):
96
+ from secpipw.pth_monitor import handle_suspicious_pth_alerts as impl
97
+
98
+ return impl(*args, **kwargs)
99
+
100
+
101
+ def inspect_install_artifacts(*args, **kwargs):
102
+ from secpipw.pth_monitor import inspect_install_artifacts as impl
103
+
104
+ return impl(*args, **kwargs)
105
+
106
+
107
+ def inspect_package_artifact_history(*args, **kwargs):
108
+ from secpipw.pth_monitor import inspect_package_artifact_history as impl
109
+
110
+ return impl(*args, **kwargs)
111
+
112
+
113
+ def handle_package_artifact_history_alerts(*args, **kwargs):
114
+ from secpipw.pth_monitor import handle_package_artifact_history_alerts as impl
115
+
116
+ return impl(*args, **kwargs)
117
+
118
+
119
+ def main(argv: list[str] | None = None) -> int:
120
+ args = list(sys.argv[1:] if argv is None else argv)
121
+ if args[:1] == ["refresh-cache"]:
122
+ return _refresh_caches()
123
+ if args[:1] and args[0] in DEDICATED_TOOL_ENTRYPOINTS:
124
+ shortcut = DEDICATED_TOOL_ENTRYPOINTS[args[0]]
125
+ sys.stderr.write(
126
+ f"ERROR: spip {args[0]} is no longer supported; use {shortcut} instead.\n"
127
+ )
128
+ return 2
129
+ if args[:1] == ["install"]:
130
+ try:
131
+ (
132
+ pip_args,
133
+ ignore_warning,
134
+ debug,
135
+ spip_status,
136
+ sensitivity,
137
+ ignore_severity,
138
+ ) = _split_wrapper_args(args[1:])
139
+ except ValueError as exc:
140
+ sys.stderr.write(f"ERROR: {exc}\n")
141
+ return 2
142
+ if spip_status:
143
+ sys.stderr.write(f"spip {__version__} guard enabled.\n")
144
+ return _install_with_guard(
145
+ pip_args,
146
+ ignore_warning=ignore_warning,
147
+ ignore_severity=ignore_severity,
148
+ debug=debug,
149
+ sensitivity=sensitivity,
150
+ )
151
+ return run_pip(args)
152
+
153
+
154
+ def pipx_main(argv: list[str] | None = None) -> int:
155
+ return _tool_with_guard("pipx", list(sys.argv[1:] if argv is None else argv))
156
+
157
+
158
+ def poetry_main(argv: list[str] | None = None) -> int:
159
+ return _tool_with_guard("poetry", list(sys.argv[1:] if argv is None else argv))
160
+
161
+
162
+ def uv_main(argv: list[str] | None = None) -> int:
163
+ return _tool_with_guard("uv", list(sys.argv[1:] if argv is None else argv))
164
+
165
+
166
+ def _split_wrapper_args(
167
+ args: list[str],
168
+ ) -> tuple[list[str], bool, bool, bool, Severity, Severity | None]:
169
+ return _split_guarded_args(args, stop_at_first_non_wrapper=False)
170
+
171
+
172
+ def _split_tool_wrapper_args(
173
+ args: list[str],
174
+ ) -> tuple[list[str], bool, bool, bool, Severity, Severity | None]:
175
+ return _split_guarded_args(args, stop_at_first_non_wrapper=True)
176
+
177
+
178
+ def _split_guarded_args(
179
+ args: list[str],
180
+ *,
181
+ stop_at_first_non_wrapper: bool,
182
+ ) -> tuple[list[str], bool, bool, bool, Severity, Severity | None]:
183
+ forwarded_args: list[str] = []
184
+ ignore_warning = False
185
+ ignore_severity: Severity | None = None
186
+ debug = False
187
+ spip_status = False
188
+ sensitivity = Severity.LOW
189
+
190
+ i = 0
191
+ while i < len(args):
192
+ arg = args[i]
193
+ if stop_at_first_non_wrapper and arg == "--":
194
+ forwarded_args.extend(args[i + 1 :])
195
+ break
196
+ if arg == "--spip-ignore-warning":
197
+ ignore_warning = True
198
+ i += 1
199
+ continue
200
+ if arg == "--spip-ignore":
201
+ if i + 1 >= len(args):
202
+ raise ValueError("--spip-ignore requires low, medium, or high")
203
+ ignore_severity = _parse_ignore_severity(args[i + 1])
204
+ i += 2
205
+ continue
206
+ if arg.startswith("--spip-ignore="):
207
+ ignore_severity = _parse_ignore_severity(arg.split("=", 1)[1])
208
+ i += 1
209
+ continue
210
+ if arg == "--spip-debug":
211
+ debug = True
212
+ i += 1
213
+ continue
214
+ if arg == "--spip-status":
215
+ spip_status = True
216
+ i += 1
217
+ continue
218
+ if arg == "--sensitivity":
219
+ if i + 1 >= len(args):
220
+ raise ValueError("--sensitivity requires low, medium, or high")
221
+ sensitivity = _parse_sensitivity(args[i + 1])
222
+ i += 2
223
+ continue
224
+ if arg.startswith("--sensitivity="):
225
+ sensitivity = _parse_sensitivity(arg.split("=", 1)[1])
226
+ i += 1
227
+ continue
228
+
229
+ if stop_at_first_non_wrapper:
230
+ forwarded_args.extend(args[i:])
231
+ break
232
+ forwarded_args.append(arg)
233
+ i += 1
234
+
235
+ return forwarded_args, ignore_warning, debug, spip_status, sensitivity, ignore_severity
236
+
237
+
238
+ def _parse_sensitivity(value: str) -> Severity:
239
+ try:
240
+ sensitivity = parse_severity(value)
241
+ except ValueError as exc:
242
+ raise ValueError("--sensitivity must be low, medium, or high") from exc
243
+ if sensitivity not in {Severity.LOW, Severity.MEDIUM, Severity.HIGH}:
244
+ raise ValueError("--sensitivity must be low, medium, or high")
245
+ return sensitivity
246
+
247
+
248
+ def _parse_ignore_severity(value: str) -> Severity:
249
+ try:
250
+ severity = parse_severity(value)
251
+ except ValueError as exc:
252
+ raise ValueError("--spip-ignore must be low, medium, or high") from exc
253
+ if severity not in {Severity.LOW, Severity.MEDIUM, Severity.HIGH}:
254
+ raise ValueError("--spip-ignore must be low, medium, or high")
255
+ return severity
256
+
257
+
258
+ def _create_pth_monitor(pip_args: list[str], *, debug: bool) -> PthMonitor | None:
259
+ try:
260
+ return PthMonitor.from_install_args(pip_args)
261
+ except Exception as exc:
262
+ if debug:
263
+ sys.stderr.write(f"[INFO] pth-monitor unavailable: {exc}\n")
264
+ return None
265
+
266
+
267
+ def _refresh_caches() -> int:
268
+ client = OfficialPyPIClient()
269
+ try:
270
+ results = refresh_all_caches(client)
271
+ except Exception as exc:
272
+ sys.stderr.write(f"failed to refresh caches: {exc}\n")
273
+ return 1
274
+ for result in results:
275
+ sys.stdout.write(
276
+ f"refreshed {result.description} with {result.count} entries at {result.location}\n"
277
+ )
278
+ return 0
279
+
280
+
281
+ def _install_with_guard(
282
+ pip_args: list[str],
283
+ *,
284
+ ignore_warning: bool,
285
+ ignore_severity: Severity | None = None,
286
+ debug: bool,
287
+ sensitivity: Severity,
288
+ ) -> int:
289
+ monitor_required = not _severity_ignored(ignore_severity, Severity.MEDIUM)
290
+ monitor = _create_pth_monitor(pip_args, debug=debug) if monitor_required else None
291
+ if monitor_required and monitor is None:
292
+ sys.stderr.write(
293
+ "ERROR: spip could not initialize .pth monitoring for this install path.\n"
294
+ )
295
+ sys.stderr.write(
296
+ "Refusing to continue because post-install .pth protection would be disabled.\n"
297
+ )
298
+ return 2
299
+ resolved_plan = None
300
+
301
+ def plan_hook(plan):
302
+ nonlocal resolved_plan
303
+ resolved_plan = plan
304
+ return run_install_checks(
305
+ plan,
306
+ pip_args,
307
+ ignore_warning=ignore_warning,
308
+ ignore_severity=ignore_severity,
309
+ sensitivity=sensitivity,
310
+ debug=debug,
311
+ )
312
+
313
+ def artifact_hook(requirements):
314
+ if _severity_ignored(ignore_severity, Severity.MEDIUM):
315
+ return _allow_install_decision()
316
+ return gate_suspicious_pth_alerts(
317
+ inspect_install_artifacts(requirements),
318
+ ignore_warning=ignore_warning,
319
+ ignore_severity=ignore_severity,
320
+ sensitivity=sensitivity,
321
+ )
322
+
323
+ try:
324
+ rc = run_guarded_pip_install(pip_args, plan_hook, artifact_hook)
325
+ except Exception as exc:
326
+ sys.stderr.write(f"ERROR: spip failed to run guarded pip install: {exc}\n")
327
+ return 1
328
+ if rc != 0:
329
+ return rc
330
+ if monitor is None:
331
+ decision = _allow_install_decision()
332
+ else:
333
+ decision = handle_suspicious_pth_alerts(
334
+ monitor.inspect(),
335
+ ignore_warning=ignore_warning,
336
+ ignore_severity=ignore_severity,
337
+ )
338
+ if not decision.allow_install:
339
+ return decision.exit_code
340
+
341
+ if resolved_plan is None or _severity_ignored(ignore_severity, Severity.MEDIUM):
342
+ return decision.exit_code
343
+ history_alerts = inspect_package_artifact_history(
344
+ resolved_plan.packages,
345
+ getattr(monitor, "directories", ()),
346
+ pip_args=pip_args,
347
+ )
348
+ history_decision = handle_package_artifact_history_alerts(
349
+ history_alerts,
350
+ ignore_warning=ignore_warning,
351
+ ignore_severity=ignore_severity,
352
+ sensitivity=sensitivity,
353
+ )
354
+ return history_decision.exit_code
355
+
356
+
357
+ def _tool_with_guard(tool: str, args: list[str]) -> int:
358
+ try:
359
+ (
360
+ tool_args,
361
+ ignore_warning,
362
+ debug,
363
+ spip_status,
364
+ sensitivity,
365
+ ignore_severity,
366
+ ) = _split_tool_wrapper_args(args)
367
+ except ValueError as exc:
368
+ sys.stderr.write(f"ERROR: {exc}\n")
369
+ return 2
370
+
371
+ if spip_status:
372
+ sys.stderr.write(f"spip {__version__} guard enabled for {tool}.\n")
373
+
374
+ pip_args = preflight_pip_args_for_tool(tool, tool_args)
375
+ if pip_args is None:
376
+ if tool_command_requires_preflight(tool, tool_args):
377
+ sys.stderr.write(
378
+ f"ERROR: spip could not derive a pip install plan for this {tool} "
379
+ "command.\n"
380
+ )
381
+ sys.stderr.write(
382
+ "Refusing to continue because supply-chain checks would be disabled.\n"
383
+ )
384
+ return 2
385
+ return run_tool(tool, tool_args)
386
+
387
+ decision = _preflight_external_install(
388
+ tool,
389
+ pip_args,
390
+ tool_args,
391
+ ignore_warning=ignore_warning,
392
+ ignore_severity=ignore_severity,
393
+ debug=debug,
394
+ sensitivity=sensitivity,
395
+ )
396
+ if not decision.allow_install:
397
+ return decision.exit_code
398
+ return run_tool(tool, tool_args)
399
+
400
+
401
+ def _preflight_external_install(
402
+ tool: str,
403
+ pip_args: list[str],
404
+ tool_args: list[str],
405
+ *,
406
+ ignore_warning: bool,
407
+ ignore_severity: Severity | None,
408
+ debug: bool,
409
+ sensitivity: Severity,
410
+ ):
411
+ from secpipw.warning_gate import GateDecision
412
+
413
+ try:
414
+ plan = resolve_install_plan(pip_args)
415
+ except Exception as exc:
416
+ if _install_plan_error_has_returncode(exc):
417
+ stderr = getattr(exc, "stderr", "")
418
+ stdout = getattr(exc, "stdout", "")
419
+ if stderr:
420
+ sys.stderr.write(stderr)
421
+ if stdout:
422
+ sys.stdout.write(stdout)
423
+ return GateDecision(allow_install=False, exit_code=exc.returncode)
424
+ sys.stderr.write(
425
+ f"ERROR: spip failed to resolve guarded install plan for {tool}: {exc}\n"
426
+ )
427
+ return GateDecision(allow_install=False, exit_code=1)
428
+
429
+ decision = run_install_checks(
430
+ plan,
431
+ pip_args,
432
+ ignore_warning=ignore_warning,
433
+ ignore_severity=ignore_severity,
434
+ sensitivity=sensitivity,
435
+ debug=debug,
436
+ )
437
+ if not decision.allow_install:
438
+ return decision
439
+
440
+ if _severity_ignored(ignore_severity, Severity.MEDIUM):
441
+ return decision
442
+
443
+ try:
444
+ artifact_alerts = inspect_install_plan_artifacts(plan)
445
+ except Exception as exc:
446
+ sys.stderr.write(
447
+ f"ERROR: spip failed to inspect resolved {tool} artifacts: {exc}\n"
448
+ )
449
+ return GateDecision(allow_install=False, exit_code=1)
450
+
451
+ artifact_decision = gate_suspicious_pth_alerts(
452
+ artifact_alerts,
453
+ ignore_warning=ignore_warning,
454
+ ignore_severity=ignore_severity,
455
+ sensitivity=sensitivity,
456
+ )
457
+ if not artifact_decision.allow_install:
458
+ return artifact_decision
459
+ return artifact_decision
460
+
461
+
462
+ def _install_plan_error_has_returncode(exc: Exception) -> bool:
463
+ return hasattr(exc, "returncode")
464
+
465
+
466
+ def _severity_ignored(
467
+ ignore_severity: Severity | None,
468
+ severity: Severity,
469
+ ) -> bool:
470
+ return ignore_severity is not None and severity <= ignore_severity
@@ -0,0 +1,44 @@
1
+ {
2
+ "project_count": 38,
3
+ "projects": [
4
+ "black",
5
+ "build",
6
+ "celery",
7
+ "cryptography",
8
+ "django",
9
+ "fastapi",
10
+ "flask",
11
+ "httpx",
12
+ "jinja2",
13
+ "matplotlib",
14
+ "mypy",
15
+ "numpy",
16
+ "packaging",
17
+ "pandas",
18
+ "pillow",
19
+ "pip",
20
+ "poetry",
21
+ "protobuf",
22
+ "pydantic",
23
+ "pyjwt",
24
+ "pytest",
25
+ "python-dateutil",
26
+ "pyyaml",
27
+ "requests",
28
+ "rich",
29
+ "scikit-learn",
30
+ "scipy",
31
+ "setuptools",
32
+ "six",
33
+ "sqlalchemy",
34
+ "tox",
35
+ "twine",
36
+ "typing-extensions",
37
+ "urllib3",
38
+ "uv",
39
+ "uvicorn",
40
+ "virtualenv",
41
+ "wheel"
42
+ ],
43
+ "source": "bootstrap"
44
+ }