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 +5 -0
- secpipw/__main__.py +4 -0
- secpipw/cache_refresh.py +47 -0
- secpipw/cli.py +470 -0
- secpipw/data/pypi-project-names.json +44 -0
- secpipw/install_checks.py +579 -0
- secpipw/install_plan.py +180 -0
- secpipw/package_install.py +241 -0
- secpipw/pip_bridge.py +62 -0
- secpipw/pip_guard.py +565 -0
- secpipw/pth_monitor.py +1089 -0
- secpipw/pypi_api.py +725 -0
- secpipw/release_checks.py +1589 -0
- secpipw/severity.py +28 -0
- secpipw/terminal.py +16 -0
- secpipw/tool_bridge.py +1324 -0
- secpipw/typo.py +488 -0
- secpipw/warning_gate.py +139 -0
- secpipw-1.0.0.dist-info/METADATA +217 -0
- secpipw-1.0.0.dist-info/RECORD +24 -0
- secpipw-1.0.0.dist-info/WHEEL +5 -0
- secpipw-1.0.0.dist-info/entry_points.txt +5 -0
- secpipw-1.0.0.dist-info/licenses/LICENSE +21 -0
- secpipw-1.0.0.dist-info/top_level.txt +1 -0
secpipw/__init__.py
ADDED
secpipw/__main__.py
ADDED
secpipw/cache_refresh.py
ADDED
|
@@ -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
|
+
}
|