Đọc báo cáo ATTT mới nhất tại đây

VCI
RED TEAM

Python Package Poisoning: Những nơi Python tự chạy code mà bạn không hề import

Python Package Poisoning: Những nơi Python tự chạy code mà bạn không hề import"Không import mà code vẫn chạy?"Tháng 3/2026, LiteLLM 1.82.7/1.82.8 và Telnyx SDK bị nhét mã độc. Không cần exploit CVE, c ...

30-09-2026 09:54 GMT Thời gian đọc: 18 phútRED TEAM
 Python Package Poisoning: Những nơi Python tự chạy code mà bạn không hề import

Python Package Poisoning: Những nơi Python tự chạy code mà bạn không hề import

"Không import mà code vẫn chạy?"

Tháng 3/2026, LiteLLM 1.82.7/1.82.8 và Telnyx SDK bị nhét mã độc. Không cần exploit CVE, cũng không cần import LiteLLM - chỉ cần package độc hại được cài vào environment, rồi một Python interpreter khởi động, payload trong .pth có thể được kích hoạt. Riêng các version LiteLLM bị compromise đã được tải hơn 119K lần trước khi bị quarantine sau 2 giờ 32 phút; Telnyx bị quarantine sau 3 giờ 42 phút.

Điều thú vị không phải là "PyPI bị tấn công" mà là chuỗi tấn công không hề bắt đầu từ LiteLLM. Bài này trả lời câu hỏi: Python còn bao nhiêu chỗ chạy code "ngầm" như vậy dưới góc nhìn red team.

Kill-chain nhìn từ góc red team

Trước khi vào kỹ thuật, mình muốn đặt lại câu hỏi theo hướng khác hẳn cách người ta hay viết về supply chain attack. Không phải "hãy cẩn thận", mà là: nếu target vào một công ty có pipeline CI/CD chạy Python thì ta có thể đi đường nào?

[Initial Access]
Typosquatting / Dependency Confusion / Mirror Poisoning / CI Supply Chain
↓
[Execution]
Interpreter-level: .pth / sitecustomize / usercustomize
Framework-level:  entry_points / plugin auto-loading
→ Interpreter hooks chạy ở Python startup;
framework plugins được auto-load khi framework khởi động
↓
[Execution + Detection Gap]
Startup hooks / plugin auto-loading
→ Không nhất thiết để lại tín hiệu trong CVE scanning
↓
[Collection → Exfil → Lateral Movement]
Credentials trong CI runner → pivot sang production infra

Một số mapping đáng chú ý qua MITRE ATT&CK:

  • T1195.001 - Supply Chain Compromise: Software Dependencies and Development Tools
  • T1546.018 - Event Triggered Execution: Python Startup Hooks (.pth, sitecustomize.py, usercustomize.py)
  • T1552.001 - Unsecured Credentials: Credentials In Files

Giờ đi vào từng bước.

Bước 1: Vào được cửa trước đã

Ba con đường phổ biến nhất, mình lướt nhanh vì mỗi cái xứng đáng một bài riêng:

Typosquatting. Gõ reqeusts thay vì requests. urllib3-security thay vì urllib3. Một lần gõ nhầm là đủ để kéo về package không mong muốn. Typosquatting trên PyPI vẫn diễn ra thường xuyên, với nhiều chiến dịch nhắm vào các package phổ biến.

Dependency Confusion. Alex Birsan năm 2021 demo cái này với Apple, Microsoft, PayPal. Chỉ cần biết tên package nội bộ của công ty, rồi upload package trùng tên lên PyPI public với version number cao hơn. pip mặc định ưu tiên version cao nhất, kể cả khi nó đến từ nguồn "lạ". Kỹ thuật này vẫn còn ăn tiền nếu công ty không cấu hình đúng --index-url hoặc --extra-index-url.

CI/CD Supply Chain: đây chính là đường LiteLLM/Telnyx bị đánh. Theo incident report của PyPI, attacker không tấn công thẳng vào hai package này. Họ khai thác một API token bị lộ ra từ Trivy - con scanner vốn được dùng để soi lỗ hổng container - trong CI pipeline. Từ token đó, họ publish thẳng version độc hại lên PyPI dưới danh nghĩa maintainer hợp lệ. Nói cách khác: họ không hack LiteLLM, họ hack cái thứ đang canh gác LiteLLM.

Nếu muốn hiểu sâu hơn về dependency confusion, bài gốc của Birsan trên HackerOne blog (2021) vẫn là tài liệu chi tiết và dễ đọc nhất. Về vụ LiteLLM/Telnyx, incident report của PyPI có đủ timeline và kỹ thuật: https://blog.pypi.org/posts/2026-04-02-incident-report-litellm-telnyx-supply-chain-attack/

Bước 2: Execution

Đây là trái tim của bài. Python có nhiều điểm thực thi "ẩn" hơn số đông developer nghĩ, và chúng không nằm ở cùng một tầng. Mình chia làm hai nhóm: interpreter-level (Python tự chạy) và framework-level (một tool khác tự load plugin).

Hình 1: Thứ tự Python load các execution hook khi khởi động - interpreter-level (.pth → sitecustomize → usercustomize) và framework-level (pytest11 entry points, chỉ khi framework chạy).

Nhóm 1 - Interpreter-level: Python tự chạy code

.pth file: ai cũng có, ít ai để ý

Mỗi lần Python khởi động, module site sẽ tự động đọc mọi file .pth nằm trong site-packages. Mục đích ban đầu rất vô hại: cho phép package thêm thư mục vào sys.path. Nhưng có một hành vi ít người biết:

Nếu một dòng trong file .pth bắt đầu bằng chữ import, Python sẽ thực thi luôn dòng đó.

Đây không phải bug, cũng chẳng phải mẹo undocumented gì cả. Nó nằm ngay trong source code của Lib/site.py. Nhưng với một attacker, đây chính là execution primitive: code chạy ngay lúc interpreter start, không cần victim viết một dòng import nào.

Mình dựng lab để tự tay kiểm chứng: Python 3.11.15, pip 24.0, virtualenv cô lập, chặn hẳn network ra ngoài. Lưu ý: extra_path là cơ chế cũ của setuptools (chính setuptools cũng dùng nó để cài distutils-precedence.pth), nên hành vi có thể khác nhau giữa các phiên bản pip/setuptools. Trước khi reproduce, nên ghi lại version chính xác đang dùng:

python3 --version
pip --version
python3 -c “import setuptools; print(setuptools.__version__)”

Nếu không ra kết quả tương tự, việc đầu tiên nên đối chiếu là version.

# malicious_pkg/pth_payload.pth
# setup.py sẽ dùng extra_path để tạo pth_payload.pth trong site-packages
import os; open('/tmp/pth_marker.txt', 'w').write('triggered at startup')

# setup.py

from setuptools import setup
from setuptools.command.install import install
class install_with_pth(install):
"""Dùng extra_path để setuptools tạo .pth trong site-packages,
thay vì coi nội dung đó như một installation prefix."""
_pth_name = 'pth_payload'
_pth_contents = open('malicious_pkg/pth_payload.pth').read()
def initialize_options(self):
install.initialize_options(self)
self.extra_path = (self._pth_name, self._pth_contents)
def finalize_options(self):
install.finalize_options(self)
import os
suffix = os.path.relpath(self.install_lib, self.install_libbase)
if suffix.strip() == self._pth_contents.strip():
self.install_lib = self.install_libbase
setup(
name='malicious_pkg',
version='1.0.0',
packages=['malicious_pkg'],
cmdclass={'install': install_with_pth},
)

# Cài package - quan trọng: phải regular install, không phải -e (editable)

pip install ./malicious_pkg/
# Không import gì hết, chỉ chạy Python suông
python -c “pass”
# Xem kết quả
cat /tmp/pth_marker.txt

Kết quả chạy trên lab:

Hình 2: Tạo pth_payload.pth, setup.py với custom install command (extra_path), rồi tạo virtualenv cô lập để test.

Hình 3: Trước khi cài find *.pth không có gì. Sau pip install ./: pth_payload.pth xuất hiện trong site-packages. Chạy python -c "pass", không import gì cả, cat /tmp/pth_marker.txt hiện triggered at startup.

Đúng cơ chế này, trong vụ LiteLLM, file độc hại được đặt tên litellm_init.pth.

Muốn xem cơ chế này hoạt động như thế nào ở cấp source code, có thể tham khảo function addpackage() trong Lib/site.py của CPython ở link này: https://github.com/python/cpython/blob/3.13/Lib/site.py

Attack surface này đang được thu hẹp, nhưng không biến mất. PEP 829 (Python 3.15) đưa ra lộ trình loại bỏ dần việc thực thi import line trong .pth: cơ chế này vẫn được xử lý bình thường trong giai đoạn chuyển tiếp (3.15–3.17), sau đó bị bỏ qua âm thầm (3.18–3.19), và cuối cùng phát warning từ 3.20 trở đi. Đồng thời, PEP 829 giới thiệu .start - một file format mới định nghĩa entry point chạy lúc Python khởi động, thay thế cho import line trong .pth.

Quan trọng về mặt security: chính PEP cũng nêu rõ trong mục Security Implications rằng thay đổi này không loại bỏ toàn bộ pre-start code execution attack surface - một package độc hại vẫn có thể thực thi code tùy ý qua entry point, chỉ là cơ chế mới có cấu trúc, dễ audit và dễ áp policy control hơn.

Vì vậy, vấn đề cần quan sát không chỉ là một primitive cụ thể như .pth, mà là toàn bộ execution surface mà Python packaging và framework ecosystem cung cấp cho code chạy ngoài luồng import trực tiếp của ứng dụng.

sitecustomize.py / usercustomize.py: Python tự chạy hộ bạn

Python có một file đặc biệt tên sitecustomize.py. Nếu module này nằm trên sys.path, Python sẽ tự thử import nó trong quá trình site initialization, trước khi chạy bất kỳ script nào của user. Cơ chế này sinh ra để sysadmin tùy biến môi trường Python toàn hệ thống, hoàn toàn hợp lệ. Vấn đề là một attacker có thể dùng đúng cơ chế đó.

Trên một số môi trường Linux, sitecustomize.py có thể đã được hệ thống cung cấp sẵn. Khi đó, nếu system site-packages nằm trước path mà attacker kiểm soát trong sys.path, module hệ thống sẽ được resolve trước - đây thuần túy là vấn đề thứ tự sys.path, không phải một cơ chế bảo vệ. Trong lab, mình dùng biến thể usercustomize.py - load từ user site-packages (~/.local/lib/python3.x/site-packages/), không cần root, vẫn trigger trước mọi script của user.

Lưu ý quan trọng: usercustomize.py bị vô hiệu hóa bên trong virtualenv (ENABLE_USER_SITE: False). Khi victim làm việc trong venv, vector này không trigger, nhưng khi họ dùng system Python (chạy CI bằng system Python, gọi script nhanh bên ngoài venv), nó vẫn hoạt động đầy đủ.

# usercustomize.py - plant vào ~/.local/lib/python3.11/site-packages/
import os
def _beacon():
try:
marker = os.path.expanduser("~/.python_init_marker")
with open(marker, "a") as f:
f.write(f"pid={os.getpid()} ppid={os.getppid()}\n")
except Exception:
pass
_beacon()

# Không cần root - ghi thẳng vào user site-packages
mkdir -p ~/.local/lib/python3.11/site-packages
cp usercustomize.py ~/.local/lib/python3.11/site-packages/

# Xác nhận path đúng
python3 -c “import site; print(site.getusersitepackages())”

# Trigger
rm -f ~/.python_init_marker
python3 -c "pass"
cat ~/.python_init_marker

Kết quả:

Hình 4: Không có import, không có script nào được gọi, chỉ python3 -c "pass" thôi, đủ để beacon ghi pid=1125 ppid=539 vào marker file. Lưu ý: python3 --version KHÔNG trigger: CPython xử lý flag đó trước cả bước site-init.

Điều đáng chú ý nhất ở vector này: khi site initialization và user site được bật, nó có thể được kích hoạt trong nhiều invocation của Python: chạy script, shell tương tác, hoặc khi các tool như pytest/pip được thực thi bằng chính interpreter đó - cơ chế nằm ở interpreter, không phải ở bản thân pytest hay pip. Chạy với đúng quyền của user hiện tại, không cần leo thang gì cả, và không để lại dấu vết gì trong output terminal bình thường. Gõ python3 -m site để xem toàn bộ thứ tự lookup trên máy thật.

Nhóm 2  Framework-level: một tool khác tự load plugin cho bạn

Khác với .pth hay sitecustomize.py (nơi chính interpreter Python là bên thực thi), nhóm này thực thi thông qua một framework hay tool khác đang chạy trên nền Python. Cơ chế: package đăng ký (register) vào một entry point group, rồi framework tự động khám phá (discover) và load nó, không cần ai gọi import thủ công.

entry_points trong pyproject.toml là ví dụ điển hình. Đa số developer biết nó dùng để tạo CLI command. Ít ai để ý rằng một số nhóm entry point được framework tự động load ngay khi khởi động.

# pyproject.toml của package độc hại
[project.entry-points."pytest11"]
my_plugin = "malicious_pkg.plugin_hook"

pytest11 là nhóm entry point dành cho plugin của pytest. Mỗi lần pytest chạy, nó discover và load các plugin được đăng ký qua nhóm pytest11, không đụng gì tới conftest.py của bạn cả. Nếu CI pipeline của công ty chạy test bằng pytest, và attacker chỉ cần cài được một package có entry point kiểu này vào environment, code chạy ngay khi lệnh pytest được gọi.

# evil_plugin/__init__.py - payload chạy ngay khi module được import
import os

_marker = os.path.expanduser("~/.ep_marker")
with open(_marker, "w") as f:
f.write("entry_point triggered by pytest plugin load\n")

# pyproject.toml
[project.entry-points."pytest11"]
evil = “evil_plugin”
pip install -e .
rm -f ~/.ep_marker
pytest --trace-config --co -q  # --trace-config xác nhận plugin registration
cat ~/.ep_marker

Kết quả chạy pytest:

Hình 5: Với --trace-config, pytest in rõ PLUGIN registered: malicious_pkg.plugin_hook, xác nhận plugin tự được load mà không cần một dòng import nào trong codebase. cat ~/.ep_marker sau đó xác nhận code đã thực thi ngay lúc registration.

Điểm quan trọng: thực thi ngầm không chỉ nằm trong Python interpreter mà còn ở bất kỳ framework nào build trên nền Python - mỗi cơ chế plugin là một attack surface riêng.

Muốn xem toàn bộ entry point đang active trên máy, chạy importlib.metadata.entry_points(); đây là attack surface mà hầu hết pipeline chưa audit kỹ. Về mitigation, pytest hỗ trợ biến PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 để tắt hoàn toàn auto-loading third-party plugin; nếu pipeline không cần tính năng đó, đây là một control nhỏ nhưng có giá trị.

Bước 3: Vì sao scanner không thấy gì cả

Hai câu hỏi khác nhau

Dễ nói kiểu "Trivy không detect được cái này" rồi kết luận scanner dở, nhưng vậy không công bằng và cũng không đúng bản chất. Một vulnerability scanner có thể hoàn thành đúng nhiệm vụ của nó mà vẫn không phát hiện được một package độc hại. Đó là vì vulnerability scanning và malicious-package detection là hai bài toán khác nhau:

  • Vulnerability scanning (ví dụ trivy fs . --scanners vuln) trả lời: "Component này có chứa known vulnerability không?"
  • Supply-chain / malware analysis cần trả lời: "Artifact này có chứa hành vi ngoài ý muốn không?"

Đây là hai câu hỏi hoàn toàn khác nhau. .pth file, sitecustomize.py, plugin autoload: đây là behavior/integrity problem, không có CVE tương ứng, nên không nằm trong phạm vi câu hỏi thứ nhất.

Trivy hỏi:      Package này có dính CVE đã biết không?
↓
Không

Attacker hỏi:  Package này có chạy code ngoài ý muốn không?
↓
Có

.pth file không phải bản thân là một CVE. sitecustomize.py thêm vào không có CVE. Payload ở đây không khai thác lỗ hổng nào trong code. Nó dùng đúng những cơ chế hợp pháp của Python. Một vulnerability scanner tập trung vào CVE matching hoàn toàn có thể báo cáo "zero vulnerabilities" một cách chính xác, trong khi hành vi độc hại vẫn nằm nguyên trong package.

Mình thử ngay trong lab: để evil_plugin (package có code độc hại chạy lúc import) cùng thư mục với requirements.txt chứa các package hợp lệ, rồi cho Trivy quét toàn bộ project folder.

# requirements.txt gồm requests==2.28.0, numpy==1.24.0, evil-plugin==1.0.0
trivy fs . --scanners vuln

Kết quả scan:

Hình 6: Với --scanners vuln, Trivy tìm thấy 4 CVE thật trong requests==2.28.0 (đúng phạm vi nhiệm vụ), nhưng evil_plugin/__init__.py với code chạy ngay khi import không để lại một dòng nào trong report. Trivy có các mode khác (misconfiguration, secret, SBOM), nhưng phát hiện Python startup hook hoặc hành vi thực thi bất thường trong file cấu hình không phải bài toán mà vulnerability scanner trong cấu hình này được thiết kế để giải quyết.

Semgrep hay Bandit có thể bắt được các pattern này nếu có custom rule riêng cho .pth/sitecustomize, nhưng config mặc định thì không. Đây là một trong những lý do vulnerability scanning đơn thuần không đủ để phát hiện class supply-chain malware này.

Bước 4: Chuyện gì thật sự xảy ra trong vụ LiteLLM/Telnyx

Mình tách rõ hai nguồn: PyPI incident report (chính thức) và phân tích của bên thứ ba - Wiz Research, Tencent Zhuque Lab.

Hình 7: LiteLLM/Telnyx attack flow - từ API token bị lộ trong CI đến credential collection, với persistence qua litellm_init.pth và lateral movement vào K8s (conditional).

Theo PyPI (xác nhận chính thức):

  • Root cause: PyPI xác nhận một API token bị lộ sau sự cố Trivy bị compromise. Tencent Zhuque Lab phân tích cụ thể hơn: Trivy độc hại lấy trộm token PYPI_PUBLISH từ environment variable ngay trong CI/CD pipeline của LiteLLM
  • Theo PyPI incident report: malware trong các package bị compromise thực thi trong quá trình cài đặt, thu thập credential và file nhạy cảm, rồi exfiltrate về remote API
  • Các phiên bản LiteLLM bị compromise được tải hơn 119.000 lượt trước khi bị gỡ; LiteLLM bị quarantine sau 2 giờ 32 phút, Telnyx sau 3 giờ 42 phút kể từ lúc upload

Theo phân tích của Wiz Research và Tencent Zhuque Lab (đào sâu vào payload):

  • Riêng với LiteLLM 1.82.8 (khác với 1.82.7 — bản này payload chỉ chạy khi gọi litellm --proxy hoặc import litellm.proxy.proxy_server): một phần payload được persistence qua litellm_init.pth, khiến code độc hại tiếp tục chạy mỗi khi một Python interpreter được khởi động, kể cả không import LiteLLM
  • Giai đoạn thu thập: quét ~/.ssh/, ~/.aws/credentials, ~/.kube/config, file .env, shell history, ví tiền điện tử đã mã hóa, toàn bộ environment variables, và gọi vào metadata endpoint của cloud (169.254.169.254 cho AWS, metadata.google.internal cho GCP)
  • Giai đoạn exfil: Tencent xác nhận cụ thể mã hóa dữ liệu bằng AES-256-CBC với session key ngẫu nhiên, session key đó lại được mã hóa tiếp bằng RSA-4096 public key gắn cứng, gói thành tar rồi POST về một domain do attacker sở hữu, đặt tên cố tình giống hạ tầng chính thức
  • Giai đoạn mở rộng trong Kubernetes (lateral movement + persistence, không chỉ di chuyển ngang): nếu tìm thấy Kubernetes service account token, malware đọc secrets trên mọi namespace, tạo privileged container (alpine:latest) trên mọi node kube-system, mount host directory vào trong container đó, rồi cài backdoor mức systemd tại /root/.config/sysmon/sysmon.py

Phần "mở rộng trong Kubernetes" (đọc secrets, tạo privileged container, cài backdoor) là chi tiết từ phân tích riêng của Wiz/Tencent, chưa nằm trong incident report gốc của PyPI, nên coi đây là "reported" chứ không phải "confirmed" ở tầng PyPI. Đọc thêm: https://matrix.tencent.com/en/2026/03/31/the-litellm-poisoning-incident-with-480-million-downloads-a-look-at-ai-infrastructure-security-attack-and-defense và https://www.wiz.io/blog/threes-a-crowd-teampcp-trojanizes-litellm-in-continuation-of-campaign

Giá trị của case study không nằm ở quy mô lây nhiễm, mà ở chuỗi tấn công: package compromise → code execution → credential collection. Các bước sau đó phụ thuộc vào credential và quyền mà môi trường nạn nhân cung cấp; đây không phải chuỗi leo thẳng lên cluster trong mọi trường hợp; CI runner có quyền truy cập K8s API và credential đủ quyền thì mới có điều kiện để đi tiếp tới lateral movement.

Trong lab của mình: thay vì mã hóa AES + RSA rồi exfil thật, mình chỉ đơn giản ghi env vars ra một file marker local và gửi beacon về localhost:8888. Cơ chế giống hệt, chỉ là không có gì rời khỏi máy.

Lời kết: Một vài gợi ý phòng thủ

Sau khi hiểu các execution surface trên, phần này điểm qua những chỗ đáng soi - cái thiếu thường là khả năng nhìn thấy, không phải thiếu công cụ.

Script triage nhanh (đây là hunting script, không phải detection hoàn chỉnh):

# 0. Lấy toàn bộ site dir cần scan - cả system site lẫn user site
SITE_DIRS=$(python3 -c "
import site
print(' '.join(site.getsitepackages() + [site.getusersitepackages()]))
")

# 1. Tìm file .pth có chứa dòng import
find $SITE_DIRS -name "*.pth" -exec grep -l "^import" {} \;

# 2. Tìm sitecustomize.py / usercustomize.py "lạ" - không phải của hệ thống
find $SITE_DIRS \( -name "sitecustomize.py" -o -name "usercustomize.py" \) | while read f; do
echo "=== $f ==="; head -5 "$f"
done

# 3. Liệt kê toàn bộ entry point đang active
python3 -c "
from importlib.metadata import entry_points
for ep in entry_points():
print(f'{ep.group}: {ep.name} -> {ep.value} ({ep.dist.name})')
" | grep -v “^distutils\|^setuptools”

Kết quả audit:

Hình 8: Ba section đều hit đúng mục tiêu, .pth file có import line, usercustomize.py đã plant, và entry_points.txt của evil-plugin lộ ra trong dist-info. Script này có thể chạy trực tiếp trên các môi trường Linux có Python và các tiện ích shell cơ bản, không phụ thuộc vào một security tool chuyên dụng nào.

Lưu ý quan trọng: Output của script này là tín hiệu cần xem xét, không phải kết luận. .pth có dòng import không đồng nghĩa là malicious, vì nhiều package hợp pháp dùng đúng cơ chế này. sitecustomize.py có thể là của hệ thống. Entry point có thể có rất nhiều item hợp lệ. Suspicious ≠ malicious, cần xem xét context, xuất xứ package và nội dung thực tế trước khi kết luận.

Hash pinning: hữu ích, nhưng đừng coi là đủ:

pip-compile requirements.in --generate-hashes -o requirements.txt

requests==2.31.0 \
--hash=sha256:58cd2187423839... \
--hash=sha256:942c5a7...

Hash pinning đảm bảo artifact bạn cài hôm nay giống hệt artifact đã audit - control rẻ và dễ triển khai. Giới hạn: nếu upstream bị compromise trước khi bạn generate hash (đúng kịch bản LiteLLM/Telnyx), hash pinning sẽ chốt luôn version độc hại. Nó ngăn drift, không ngăn compromise ở nguồn.

Một số điểm đáng chú ý ở các tầng khác nhau:

  • Developer: pin dependency và dùng lockfile giúp giảm rủi ro version bị swap; review changelog khi dependency update thay vì approve tự động.
  • CI/CD: đây chính xác là điểm bị khai thác trong vụ LiteLLM - API token tồn tại lâu bị lộ. Ephemeral runner, least privilege và OIDC/Trusted Publishing là hướng giảm exposure.
  • Runtime: làm inventory site-packages định kỳ, chú ý các file .pth/sitecustomize/usercustomize không rõ nguồn gốc, kiểm soát outbound network từ CI runner
  • Supply chain: Trusted Publishing kết hợp attestation và provenance. PyPI hiện đã hỗ trợ digital attestation theo PEP 740, cho phép gắn một release với đúng identity/workflow đã tạo ra nó, để downstream/consumer verify provenance của artifact trước khi đưa vào môi trường

Muốn đi theo hướng attestation, PyPI có trang hướng dẫn verify theo PEP 740 khá đầy đủ: https://docs.pypi.org/attestations/

Tham khảo:

May đo bảo mật theo
quy mô & nhu cầu của Tổ chức

Tìm kiếm đơn vị Bảo vệ An ninh mạng cho tổ chức của bạn?