面向 | 工具 | 資料庫來源 | 特色 | 報告格式 | 是否可修復 |
---|---|---|---|---|---|
弱點掃描 | pip-audit | PyPI/OSV | 官方系,預設準確、更新快 | text/json/cyclonedx | --fix 可嘗試升級 |
弱點掃描 | Safety | 專有與社群 DB | 覆蓋廣、規模化用得多 | text/json/sarif | 需付費帳號才吃全庫 |
授權清單 | pip-licenses | wheel metadata | 產 Markdown/CSV 表格 | md/csv/json | 不適用 |
SBOM | cyclonedx-bom / cyclonedx-py | 解析環境/lock | 產 CycloneDX(業界通吃) | json/xml | 不適用 |
建議:兩個弱點掃描都跑。資料庫不同、誤報與覆蓋互補。SBOM 請用 CycloneDX,之後丟掃描器或雲端平台都省話。
pyproject.toml
)把安全工具放進 dev extra,延續你前面的分層做法。
[project.optional-dependencies]
sec = [
"pip-audit>=2.7",
"safety>=3.2",
"pip-licenses>=4.5",
"cyclonedx-bom>=4.0", # 或 cyclonedx-py>=3
]
[tool.hatch.envs.sec]
features = ["sec"]
延續 Day 7,你已經有 uv.lock。掃描時要嘛掃「已安裝環境」,要嘛掃「鎖檔/需求檔」;兩種都跑最穩。
把所有檢查變成一行指令,別讓同事背咒語。
[tool.hatch.envs.sec.scripts]
# 1) pip-audit:掃已安裝環境與(可選)需求檔
audit = [
"pip-audit -l --desc --progress-spinner off",
# 若也想掃需求/鎖檔(擇一即可):
# "pip-audit -r requirements.txt --desc --progress-spinner off",
]
# 2) Safety:顯示完整報告並以非 0 退出碼擋住
safety = "safety check --full-report --continue-on-error 0"
# 3) 授權報表:輸出 Markdown,丟進 repo 供法務看
licenses = "pip-licenses --format markdown --with-authors --with-urls --output-file LICENSES-3RD-PARTY.md"
# 4) 產 SBOM(CycloneDX):CI 會上傳 artifact
sbom = "cyclonedx-bom -o sbom.cdx.json -F json"
# 5) 全部來一發
check-all = [
"pip-audit -l --desc --progress-spinner off",
"safety check --full-report --continue-on-error 0",
"pip-licenses --format markdown --with-authors --with-urls --output-file LICENSES-3RD-PARTY.md",
"cyclonedx-bom -o sbom.cdx.json -F json"
]
小眉角
pip-audit -l
掃「目前環境」;若你偏好「鎖檔真相」,可在乾淨 venv 先uv sync --locked
再掃。- Safety 的資料庫若有商用訂閱,把 API key 放 CI secrets,別寫死在檔案裡(Day 12 的祕密原則還記得吧)。
# noxfile.py
import nox
@nox.session(python=["3.12"])
def security(session):
session.install(".[sec]")
session.run("pip-audit", "-l", "--desc", "--progress-spinner", "off")
session.run("safety", "check", "--full-report", "--continue-on-error", "0")
session.run("pip-licenses", "--format", "markdown", "--with-authors", "--with-urls",
"--output-file", "LICENSES-3RD-PARTY.md")
session.run("cyclonedx-bom", "-o", "sbom.cdx.json", "-F", "json")
# .pre-commit-config.yaml(節錄)
repos:
- repo: https://github.com/pypa/pip-audit
rev: v2.7.3
hooks:
- id: pip-audit
args: ["-l", "--progress-spinner", "off"]
- repo: https://github.com/pyupio/safety
rev: 3.2.0
hooks:
- id: safety
args: ["check", "--continue-on-error", "0"]
已經在 Day 9 用過 pre-commit。現在只是在 commit 前再加兩把刀。
在 ci.yml
的作業後面追加(或做成獨立 job 與 test 併行):
security:
runs-on: ubuntu-latest
needs: build-test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: astral-sh/setup-uv@v3
with:
enable-cache: true
- name: Sync deps from lock (no dev)
run: uv sync --locked --no-dev
- name: Install security extras
run: uv pip install ".[sec]"
- name: pip-audit
run: uv run pip-audit -l --desc --progress-spinner off
- name: Safety (use token if you have it)
env:
SAFETY_API_KEY: ${{ secrets.SAFETY_API_KEY }}
run: uv run safety check --full-report --continue-on-error 0
- name: Generate licenses & SBOM
run: |
uv run pip-licenses --format markdown --with-authors --with-urls --output-file LICENSES-3RD-PARTY.md
uv run cyclonedx-bom -o sbom.cdx.json -F json
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
LICENSES-3RD-PARTY.md
sbom.cdx.json
想對 PR 更兇一點?把 pip-audit --strict 或 Safety 的「嚴格模式」打開,直接 fail 掉。再搭配 coverage 門檻,雙保險。
延續多階段 Dockerfile。
# Stage 1: builder 已做 uv sync --frozen
RUN pip install pip-audit safety
RUN pip-audit -l --progress-spinner off
# 可選 Safety(若你有金鑰,改在 CI 跑會比較好管理)
把 sbom.cdx.json
產在 CI,交給平台掃描;或在 CI 跑容器掃描器(例如 Trivy):
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:dev
依賴掃描與映像掃描是兩件事。前者看 Python 包,後者看 OS 套件與已打包的層。兩個都要,才不會補東牆漏西牆。
pip-licenses
已在 scripts。ALLOWED_LICENSES = {"MIT","BSD","Apache-2.0","MPL-2.0"}
scripts/check_licenses.py
,CI 跑):import json, sys, subprocess
ALLOWED = {"MIT","BSD","Apache-2.0","MPL-2.0"}
raw = subprocess.check_output(["pip-licenses", "--format", "json"]).decode()
pkgs = json.loads(raw)
bad = [(p["Name"], p["License"]) for p in pkgs if p["License"] not in ALLOWED]
if bad:
print("Forbidden licenses detected:")
for name, lic in bad:
print(f"- {name}: {lic}")
sys.exit(1)
print("Licenses OK")
症狀 | 可能原因 | 快速修正 |
---|---|---|
本地與 CI 掃描結果不同 | 沒吃同一套依賴 | CI 一律 uv sync --locked 再掃;本地用 hatch run 固定入口 |
pip-audit 報可修但升級會炸 | 相依鏈互咬 | 先在分支 bump 並跑 Day 8 的 check-all 與 Day 11 測試;必要時 constraints 封頂 |
Safety 找不到私有套件 | 私有 index 未設定 | 用 PIP_INDEX_URL /extra-index-url 並在 CI 注入(Secrets 管好) |
授權標示缺失/未知 | 某些包 metadata 爛 | 在報表註記例外清單,或改用替代套件;不要假裝沒看到 |
SBOM 掃不到東西 | 只掃到空環境 | 先 uv sync --locked --no-dev 再跑 cyclonedx-bom |
pip-licenses
生成第三方授權清單,白名單守門,法務不會突然把你當驚喜盒。你現在有一套不花俏、但夠硬的安全與授權防線。不是銀彈,卻足以擋住 80% 的爛事,剩下 20% 就留給你和命運摔角。