在软件开发中,保持敏感数据的安全和隐私至关重要。应用程序日志是常见的信息泄露途径之一,因此需要小心保护,避免在日志中出现敏感信息。同样的关注和风险也适用于测试日志,因为它们可能泄露密码或访问令牌。CI 工作流中的工具通常提供机制来屏蔽日志中的敏感数据,这些工具使用简单且高效。然而,在某些情况下,这些工具可能不足以提供全面的保护。
## 为什么 CI 工作流的屏蔽机制可能不够
例如,GitHub Actions 在处理敏感信息方面表现不错。任何在工作流中定义的机密信息都会自动从捕获的输出中屏蔽,这种机制非常有效。然而,和所有 CI 系统一样,它也有其局限性。如果输出报告通过不同路径保存,例如保存到文件、生成 JUnit 报告或发送到远程日志存储,GitHub Actions 无法检查这些内容并屏蔽其中的敏感信息。
此外,测试并不总是在 CI 工作流中运行,即使在本地运行测试时,敏感信息也可能需要隐藏。想象一下,你在本地运行测试并分享日志以讨论问题,却在不经意间包含了带有访问令牌的 URL。
因此,在测试日志中处理敏感数据的机制在各个层次上都是必不可少的。最好的方法是在测试级别或测试框架中直接实现这一机制,确保敏感信息不会从源头泄露,从而防止它们在系统中上传递。
## 在适当的层次增加保护
直接在测试中维护敏感信息的屏蔽机制可能相当耗时且容易出错,通常感觉像是在打一场失败的仗。例如,想象一下,你需要设计一个带有令牌参数的 URL。这个 URL 必须在请求中使用时与在日志中显示的方式不同。
相比之下,在测试框架内拦截报告生成过程,提供了一个理想的机会来挂钩进程,并修改记录以消除敏感信息。这种方法对测试透明,无需修改测试代码,就像 CI 工作流中的秘密屏蔽功能一样——简单地运行它,然后无需再管理敏感信息。它自动化了过程,确保敏感数据得到保护,而不会增加测试设置的复杂性。
这正是 `pytest-mask-secrets` 所做的,当然前提是你在使用 pytest 进行测试执行。pytest 提供了丰富而灵活的插件系统,允许你在任何日志生成之前挂钩到进程中,此时所有数据都已收集完毕。这使得在输出记录之前搜索并删除敏感值变得非常容易。
## 实际演示:如何操作
为了说明其工作原理,最有效的方法是一个简单的示例。下面是一个可能不代表真实测试场景的简单测试,但它很好地展示了 `pytest-mask-secrets` 的功能。
import logging
import os
def test_password_length():
password = os.environ["PASSWORD"]
logging.info("Tested password: %s", password)
assert len(password) > 18
```
在这个示例中,存在一个可能会失败的断言(事实上它确实会失败),并且包含了一个包含敏感信息的日志消息。虽然在日志中包含敏感信息似乎有些愚蠢,但想象一下一个场景,你的 URL 中包含一个令牌参数,并且启用了详细的调试日志记录。在这种情况下,类似 `requests` 的库可能会无意中以这种方式记录敏感信息。
现在进行测试。首先,设置测试所需的秘密信息:
```shell
(venv) $ export PASSWORD="TOP-SECRET"
```
接下来,运行测试:
```shell
(venv) $ pytest --log-level=info test.py
============================= test session starts ==============================
platform linux -- Python 3.12.4, pytest-8.3.2, pluggy-1.5.0
rootdir: /tmp/tmp.AvZtz7nHZS
collected 1 item
test.py F [100%]
=================================== FAILURES ===================================
_____________________________ test_password_length _____________________________
def test_password_length():
password = os.environ["PASSWORD"]
logging.info("Tested password: %s", password)
> assert len(password) > 18
E AssertionError: assert 10 > 18
E + where 10 = len('TOP-SECRET')
test.py:8: AssertionError
------------------------------ Captured log call -------------------------------
INFO root:test.py:7 Tested password: TOP-SECRET
=========================== short test summary info ============================
FAILED test.py::test_password_length - AssertionError: assert 10 > 18
============================== 1 failed in 0.03s ===============================
```
默认情况下,秘密值在输出中出现了两次:一次是在捕获的日志消息中,另一次是在失败的断言中。
但是,如果安装了 `pytest-mask-secrets` 会怎样呢?
```shell
(venv) $ pip install pytest-mask-secrets
```
并进行相应配置。它需要知道保存秘密信息的环境变量列表。通过设置 `MASK_SECRETS` 变量来完成此操作:
```shell
(venv) $ export MASK_SECRETS=PASSWORD
```
现在,重新运行测试:
```shell
(venv) $ pytest --log-level=info test.py
============================= test session starts ==============================
platform linux -- Python 3.12.4, pytest-8.3.2, pluggy-1.5.0
rootdir: /tmp/tmp.AvZtz7nHZS
plugins: mask-secrets-1.2.0
collected 1 item
test.py F [100%]
=================================== FAILURES ===================================
_____________________________ test_password_length _____________________________
def test_password_length():
password = os.environ["PASSWORD"]
logging.info("Tested password: %s", password)
> assert len(password) > 18
E AssertionError: assert 10 > 18
E + where 10 = len('*****')
test.py:8: AssertionError
------------------------------ Captured log call -------------------------------
INFO root:test.py:7 Tested password: *****
=========================== short test summary info ============================
FAILED test.py::test_password_length - AssertionError: assert 10 > 18
============================== 1 failed in 0.02s ===============================
```
现在,秘密值不会被直接输出,而是显示为星号。任务完成,测试报告中不再包含敏感数据。
## 总结
从示例中看,`pytest-mask-secrets` 似乎并没有比 GitHub Actions 默认执行的更多的操作,使得这些努力看起来有些多余。然而,如前所述,CI 工作流执行工具仅屏蔽捕获的输出中的敏感信息,而不会修改 JUnit 文件和其他报告。如果没有 `pytest-mask-secrets`,敏感数据仍可能暴露在这些文件中——这适用于任何由 pytest 生成的报告。另一方面,`pytest-mask-secrets` 不会屏蔽 `log_cli` 选项启用时的直接输出,因此 CI 工作流的屏蔽功能仍然有用。通常,最好结合使用这两个工具,以确保敏感数据得到充分保护。