如何获取有关已安装Python软件包的出处的信息
#python #安全 #community #containers

让我们看一下如何获取有关Python生态系统中安装软件包的出处的信息。这个想法是截至目前处于草案状态的PEP-710的一部分。

Image description

iŽidlochovice - Rozhledna Akátová věž; Czech republic作者的图像。


教程使用github.com/fridex/pip-provenance的文件。

让我们使用Chainguard's Python image创建一个简单的Python应用程序。此应用程序将是一个简单的烧瓶Hello World应用程序。 app.py脚本将具有以下内容:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def index():
    return 'Hello, world!'

app.run(host='0.0.0.0', port=8080)

此外,我们将创建一个带有以下内容的requirements.in文件:

flask

我们将使用pip-tools锁定对特定版本的依赖性,以获得可重复性。此外,我们将保留安装的Python分布的哈希:

pip-compile --generate-hashes

上面的命令将创建一个requirements.txt文件。这样的文件的一个示例可以是found here

接下来,让我们使用应用程序创建一个容器化的环境。

使用上游PIP

首先,我们将使用上游PIP,该PIP也在Chainguard的图像中发货。我们可以直接采用最小更改的Dockerfile as written by Chainguard,以确保我们有一个容器化的应用程序:

FROM cgr.dev/chainguard/python:latest-dev as builder

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt --user

FROM cgr.dev/chainguard/python:latest

WORKDIR /app
# Make sure you update Python version in path
COPY --from=builder /home/nonroot/.local/lib/python3.11/site-packages /home/nonroot/.local/lib/python3.11/site-packages
COPY app.py .

ENTRYPOINT ["python", "/app/app.py"]

可以构建容器化的应用程序:

podman build -f raw/Dockerfile -t pip-provenance:raw .

随后,可以在locahost:8080
上运行并访问构建的应用程序

podman run -p 8080:8080 pip-provenance:raw

现在,让我们想象有人将此图像发布给注册表,我们想获取有关已安装软件包的信息。我们可以拉动pip-provenance:raw图像并运行pip freeze。不幸的是,pip freeze仅显示安装Python包及其版本:

$ pip freeze                     
click==8.1.3
Flask==2.2.3
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
Werkzeug==2.2.3

我们没有任何实际安装这些软件包的信息。另外,我们没有有关这些软件包摘要的任何信息。例外是使用PEP-610之后使用直接URL安装的软件包,但在我们的示例中并非如此。

使用修补的PIP

PEP-710中有一个建议,用于存储有关使用其名称识别的安装软件包的出处信息,并可以选择地将其版本(这是我们的示例)。让我们看一下存储了哪些信息以及如何访问它的信息。

首先,让我们调整我们的Dockerfile以使用遵循PEP-710a patched version of pip

FROM cgr.dev/chainguard/python:latest-dev as builder

WORKDIR /app
COPY requirements.txt .
# ----->%------
USER root
RUN pip install --force-reinstall pip install git+https://github.com/fridex/pip.git@provenance-url
USER nonroot
# -----%<------
RUN pip install -r requirements.txt --user

FROM cgr.dev/chainguard/python:latest

WORKDIR /app
# Make sure you update Python version in path
COPY --from=builder /home/nonroot/.local/lib/python3.11/site-packages /home/nonroot/.local/lib/python3.11/site-packages
COPY app.py .

ENTRYPOINT ["python", "/app/app.py"]

让我们构建此应用程序:

podman build -f patched/Dockerfile -t pip-provenance:patched .

我们可以运行该应用程序并在localhost:8080上访问它,PIP中引入的更改对此没有影响:

podman run -p 8080:8080 pip-provenance:patched

PEP-710之后,PIP存储了位于site-packages中的*.dist-info目录中的出处的信息。让我们从容器化的环境中复制site-packages目录,以便我们可以检查那里安装的内容(用上一个示例中运行的容器化环境的哈希替换[CONTAINER_HASH]):

podman cp [CONTAINER_HASH]:/home/nonroot/.local/lib/python3.11/site-packages site-packages

我们可以查看flaskprovenance_url.json文件*:

$ cat ./site-packages/Flask-2.2.3.dist-info/provenance_url.json | jq
{
  "archive_info": {
    "hash": "sha256=c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d",
    "hashes": {
      "sha256": "c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d"
    }
  },
  "url": "https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl"
}

此文件由修补程序创建,在PEP-710中进行了更详细的描述。

一种称为pip-preserve的小工具,可以读取site-packages目录的内容,并了解安装每个Python软件包的provenance_url.json。此外,如果使用直接URL安装软件包,则该工具还可以如PEP-610中所述读取direct_url.json,以完全重建环境。让我们从容器化的环境中使用site-packages目录上的工具:

$ pip install pip-preserve
...
$ pip-preserve --ignore-errors --site-packages ./site-packages      
#
# This file is autogenerated by pip-preserve version 0.0.2.post1 with Python 3.10.6.
#
click==8.1.3 \
  --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
flask==2.2.3 \
  --hash=sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d
itsdangerous==2.1.2 \
  --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44
jinja2==3.1.2 \
  --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
markupsafe==2.1.2 \
  --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6
werkzeug==2.2.3 \
  --hash=sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612

您可以看到,该工具重建了requirements.txt文件,列出了所有安装的软件包及其版本和哈希。

读者可以注意到,重建的文件每个软件包只有一个哈希。原因是PIP仅安装一个软件包。我们原始的requirements.txt文件lists multiple hashes that correspond to Python distributions as published on PyPI在运行pip-compile命令时。在安装时间内,PIP采用与安装Python分布的环境相匹配的PIP。例如,PIP获取了为flask==2.2.3发布的轮毂文件,而不是PYPI上可用的源分布(您可以通过检查伪影哈希进行验证)。使用PIP的修补版本,我们可以指出已安装的确切工件。

如果我们将--direct-url选项传递到pip-preserve工具,则可以从安装Python软件包的地方获得精确的URL:

$ pip-preserve --ignore-errors --direct-url --site-packages ./site-packages
#
# This file is autogenerated by pip-preserve version 0.0.2.post1 with Python 3.10.6.
#
https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl \
  --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48
https://files.pythonhosted.org/packages/95/9c/a3542594ce4973786236a1b7b702b8ca81dbf40ea270f0f96284f0c27348/Flask-2.2.3-py3-none-any.whl \
  --hash=sha256:c0bec9477df1cb867e5a67c9e1ab758de9cb4a3e52dd70681f59fa40a62b3f2d
https://files.pythonhosted.org/packages/68/5f/447e04e828f47465eeab35b5d408b7ebaaaee207f48b7136c5a7267a30ae/itsdangerous-2.1.2-py3-none-any.whl \
  --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44
https://files.pythonhosted.org/packages/bc/c3/f068337a370801f372f2f8f6bad74a5c140f6fda3d9de154052708dd3c65/Jinja2-3.1.2-py3-none-any.whl \
  --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
https://files.pythonhosted.org/packages/5a/94/d056bf5dbadf7f4b193ee2a132b3d49ffa1602371e3847518b2982045425/MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl \
  --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6
https://files.pythonhosted.org/packages/f6/f8/9da63c1617ae2a1dec2fbf6412f3a0cfe9d4ce029eccbda6e1e4258ca45f/Werkzeug-2.2.3-py3-none-any.whl \
  --hash=sha256:56433961bc1f12533306c624f3be5e744389ac61d722175d543e1751285da612

为什么这有用?

好吧,现在我们知道我们可以使用PEP-710获得有关安装软件包的出处的信息。如果我们查看其他软件包,例如TensorFlow,我们可以看到已发布的multiple wheel files-每个包裹与特定环境相对应。如果我们只是pip install tensorflow,实际上使用了哪个轮子文件(假设我们无需访问安装日志)?

另外,请注意,可以在私有Python软件包索引上托管的Python软件包的特定构建。这些轮子可以使用wheel tags表示无法表达的选项构建。如果您使用的是Python环境(不一定是容器化的环境),您如何知道已安装Python软件包的出处(无需访问安装日志,或最终任何构建配置)?

本文中使用的构建集装箱环境可在docker.io/fridex/pip-provenance上获得:

podman pull fridex/pip-provenance:raw
podman pull fridex/pip-provenance:patched

您可以在discuss.python.org上进行有关PEP-710的相关讨论。


*,即使修补程序生成的provenance_url.json文件保留了hash键,pep-710也不能定义它。修补的PIP实现使用PEP-610(直接URL)定义的代码。现在在PEP-610引入的direct_url.json文件中删除了hash密钥。