iT邦幫忙

2025 iThome 鐵人賽

DAY 26
0

我們要達成什麼

  • 在本地與 CI 以 pip-auditSafety 掃出已知弱點,能產出報告並以非 0 退出碼擋 PR。
  • 建立 授權清單SBOM(CycloneDX),確保依賴授權不會把你公司法務變成戰地醫院。
  • 把這些流程「一鍵化」(Hatch scripts × Nox)並塞進 CI 範本,以及 容器建置。

工具快速定位(懶人表)

面向 工具 資料庫來源 特色 報告格式 是否可修復
弱點掃描 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。掃描時要嘛掃「已安裝環境」,要嘛掃「鎖檔/需求檔」;兩種都跑最穩。


一鍵化:Hatch scripts

把所有檢查變成一行指令,別讓同事背咒語。

[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 的祕密原則還記得吧)。

Nox:幫 CI 或本地跑一致流程

# 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:在你把坑推上 PR 前先絆倒你

# .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:把 workflow 加上「security」步驟

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 門檻,雙保險。


容器建置:在 builder 階段掃,或掃映像

延續多階段 Dockerfile。

A) 在 builder 中掃已解決依賴

# Stage 1: builder 已做 uv sync --frozen
RUN pip install pip-audit safety
RUN pip-audit -l --progress-spinner off
# 可選 Safety(若你有金鑰,改在 CI 跑會比較好管理)

B) 映像級掃描

sbom.cdx.json 產在 CI,交給平台掃描;或在 CI 跑容器掃描器(例如 Trivy):

trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:dev

依賴掃描與映像掃描是兩件事。前者看 Python 包,後者看 OS 套件與已打包的層。兩個都要,才不會補東牆漏西牆。


授權治理:白名單與報表

  1. 產授權報表:pip-licenses 已在 scripts。
  2. 設白名單(例):ALLOWED_LICENSES = {"MIT","BSD","Apache-2.0","MPL-2.0"}
  3. 簡單守門腳本(可放 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-audit × Safety 兩條腿走路,本地、pre-commit、CI 全面佈署。
  • 授權治理pip-licenses 生成第三方授權清單,白名單守門,法務不會突然把你當驚喜盒。
  • 可觀測與可重現:鎖檔 + 一鍵化腳本 + CI 失敗即禁止合併。
  • 容器安全:建置時掃依賴,映像再掃一次 OS 層,別讓 runtime 背黑鍋。

你現在有一套不花俏、但夠硬的安全與授權防線。不是銀彈,卻足以擋住 80% 的爛事,剩下 20% 就留給你和命運摔角。


上一篇
Day 25 -CI/CD 範本:GitHub Actions(lint → test → build → publish)
下一篇
Day 27 -部署選項速覽:Gunicorn/Uvicorn、Cloud Run / K8s
系列文
30 天 Python 專案工坊:環境、結構、測試到部署全打通27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言