返回文章列表

Bash 安全指令碼編寫與 DevSecOps 實踐指南

深入探討在 DevSecOps 環境中編寫安全導向的 Bash 指令碼,整合 Bandit、Dependency-Check 與 Trivy 等工具,實現自動化安全掃描與完整的 CI/CD 整合實務。

DevOps 資訊安全 自動化

在現代軟體開發流程中,安全性已從事後考量轉變為開發生命週期各階段的核心要素。DevSecOps 的核心理念正是將安全性深度整合至開發、測試與部署的每個環節。本文將深入探討如何編寫安全導向的 Bash 指令碼,並將其有效整合到 DevSecOps 流程中,透過自動化安全掃描提升整體安全防護能力。

我們將從實務角度剖析日誌記錄、錯誤處理、環境驗證等關鍵技術,並深入探討 SAST(Static Application Security Testing)、依賴性檢查與容器安全掃描的實作細節。透過完整的指令碼範例與 CI/CD 整合實務,讀者將能夠掌握編寫更安全、更穩健的 Bash 指令碼,並有效應用於實際的 DevSecOps 環境中。

安全導向的 Bash 開發基礎

在 DevSecOps 環境中,安全性是貫穿整個開發維運流程的核心要素。編寫安全導向的 Bash 指令碼需要從多個層面著手,包括完善的日誌記錄機制、健壯的錯誤處理機制、嚴謹的環境驗證流程,以及整合業界標準的安全掃描工具。

日誌記錄機制的設計與實作

日誌記錄是系統監控與問題診斷的基礎。一個設計良好的日誌記錄函式不僅能夠記錄系統運作狀態,更能在問題發生時提供關鍵的診斷資訊。我們設計的日誌記錄函式具備多層級記錄能力,同時將日誌輸出至標準輸出與日誌檔案,確保即時監控與後續追蹤的雙重需求。

#!/bin/bash

# 日誌記錄函式
# 功能:記錄不同等級的系統訊息,包含時間戳記與等級標示
# 參數:
#   $1 - 日誌等級(INFO/WARNING/ERROR/DEBUG)
#   $@ - 日誌訊息內容
log() {
    local level=$1
    shift
    local message="$*"
    # 產生符合 ISO 8601 格式的時間戳記
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    # 使用 tee 同時輸出至終端機與日誌檔案
    echo "$timestamp [${level}] ${message}" | tee -a logfile.log
}

# 使用範例
log "INFO" "系統初始化完成"
log "WARNING" "記憶體使用率達到 80%"
log "ERROR" "無法連接至資料庫伺服器"

這個日誌記錄函式的設計考量了實務環境的需求。時間戳記採用 ISO 8601 標準格式,便於後續的日誌分析與處理。使用 shift 指令移除第一個參數後,剩餘的所有參數都會被視為日誌訊息,這樣的設計讓函式調用更加靈活。tee -a 指令確保日誌訊息同時輸出至終端機與檔案,既滿足即時監控需求,也建立了完整的歷史記錄。

錯誤處理機制的建立

完善的錯誤處理機制是確保指令碼穩定執行的關鍵。在 Bash 指令碼中,我們可以利用 trap 指令捕捉錯誤訊號,並在錯誤發生時執行預定的處理程序。這種機制讓我們能夠在問題發生的第一時間獲取詳細的錯誤資訊,包括錯誤發生的行號與錯誤代碼。

#!/bin/bash

# 錯誤處理函式
# 功能:捕捉並記錄指令碼執行過程中的錯誤
# 參數:
#   $1 - 錯誤發生的行號
#   $2 - 錯誤代碼
error_handler() {
    local lineno=$1
    local err_code=$2
    # 記錄錯誤資訊,包含發生位置與錯誤代碼
    log "ERROR" "錯誤發生於第 ${lineno} 行,錯誤代碼: ${err_code}"
    # 可在此處加入額外的錯誤處理邏輯
    # 例如:清理臨時檔案、發送告警通知等
}

# 設定錯誤捕捉機制
# ${LINENO} 提供錯誤發生的行號
# $? 提供上一個指令的返回碼
trap 'error_handler ${LINENO} $?' ERR

# 設定嚴格模式
set -euo pipefail
# -e: 遇到錯誤立即退出
# -u: 使用未定義變數時報錯
# -o pipefail: 管線中任何指令失敗都會導致整體失敗

錯誤處理機制的設計需要考慮多個面向。trap 指令設定了全域的錯誤捕捉器,任何指令執行失敗都會觸發 error_handler 函式。搭配 set -euo pipefail 的嚴格模式,我們能夠在問題發生的第一時間察覺並處理,避免錯誤在系統中累積擴散。特殊變數 ${LINENO} 提供了精確的錯誤定位能力,這在大型指令碼的除錯過程中特別重要。

環境驗證的重要性

在執行任何安全掃描作業之前,必須確保執行環境已正確配置。環境驗證包括檢查必要工具的可用性、驗證目錄權限、確認配置檔案的存在等。完善的環境驗證能夠在作業執行前就發現潛在問題,避免浪費資源在註定失敗的任務上。

#!/bin/bash

# 環境驗證函式
# 功能:檢查必要的工具與目錄是否正確配置
# 返回:0 表示驗證通過,1 表示驗證失敗
validate_environment() {
    # 定義必要的工具清單
    local required_tools=("docker" "trivy" "dependency-check" "bandit" "jq")
    
    # 逐一檢查工具是否存在於系統路徑中
    for tool in "${required_tools[@]}"; do
        if ! command -v "$tool" &> /dev/null; then
            log "ERROR" "找不到必要工具: $tool"
            log "INFO" "請使用套件管理員安裝: sudo apt-get install $tool"
            return 1
        fi
    done
    
    # 檢查報告輸出目錄
    if [[ ! -d "$REPORT_DIR" ]]; then
        log "ERROR" "報告目錄不存在: $REPORT_DIR"
        log "INFO" "嘗試建立報告目錄..."
        mkdir -p "$REPORT_DIR" || return 1
    fi
    
    # 檢查目錄寫入權限
    if [[ ! -w "$REPORT_DIR" ]]; then
        log "ERROR" "無法寫入報告目錄: $REPORT_DIR"
        return 1
    fi
    
    log "INFO" "環境驗證完成,所有必要工具與目錄皆已就緒"
    return 0
}

# 目標驗證函式
# 功能:驗證掃描目標目錄的有效性與可讀性
# 返回:0 表示驗證通過,1 表示驗證失敗
validate_target() {
    # 檢查目標目錄是否存在
    if [[ ! -d "$SCAN_DIR" ]]; then
        log "ERROR" "掃描目標目錄不存在: $SCAN_DIR"
        return 1
    fi
    
    # 檢查目標目錄的讀取權限
    if [[ ! -r "$SCAN_DIR" ]]; then
        log "ERROR" "無法讀取掃描目標目錄: $SCAN_DIR"
        log "INFO" "請檢查目錄權限設定"
        return 1
    fi
    
    # 檢查目錄是否為空
    if [[ -z "$(ls -A "$SCAN_DIR")" ]]; then
        log "WARNING" "掃描目標目錄為空: $SCAN_DIR"
    fi
    
    log "INFO" "目標驗證完成,目錄可供掃描"
    return 0
}

環境驗證的設計採用防禦性程式設計的理念。我們不只檢查工具是否存在,還會驗證目錄的讀寫權限,甚至在可能的情況下自動建立缺少的目錄。這種主動式的驗證機制能夠提供更好的使用者體驗,同時減少因環境配置問題導致的執行失敗。

安全掃描功能的實作

安全掃描是 DevSecOps 流程的核心環節。我們將整合三種主要的安全掃描方法,分別針對原始碼、依賴套件與容器映像進行全方位的安全檢測。

SAST 靜態程式碼分析

靜態應用程式安全測試透過分析原始碼來識別潛在的安全漏洞。Bandit 是針對 Python 程式碼的優秀 SAST 工具,能夠檢測諸如 SQL 注入、命令注入、不安全的反序列化等常見安全問題。

#!/bin/bash

# SAST 掃描函式
# 功能:使用 Bandit 工具進行 Python 程式碼的靜態安全分析
# 返回:0 表示掃描成功,1 表示掃描失敗或發現嚴重問題
perform_sast_scan() {
    log "INFO" "開始執行 SAST 靜態程式碼安全掃描"
    log "INFO" "掃描目標: $SCAN_DIR"
    
    # 定義輸出檔案路徑
    local output_file="${REPORT_DIR}/${REPORT_NAME}_sast.txt"
    local json_output="${REPORT_DIR}/${REPORT_NAME}_sast.json"
    
    # 執行 Bandit 掃描
    # -r: 遞迴掃描目錄
    # -f: 指定輸出格式(txt/json)
    # -o: 指定輸出檔案
    # -ll: 只報告中等與高嚴重度的問題
    if bandit -r "$SCAN_DIR" -f txt -o "$output_file" -ll; then
        log "INFO" "SAST 掃描完成,未發現嚴重安全問題"
        
        # 同時產生 JSON 格式報告供後續處理
        bandit -r "$SCAN_DIR" -f json -o "$json_output" -ll
        
        # 統計發現的問題數量
        local issue_count=$(grep -c "Issue:" "$output_file" || echo "0")
        log "INFO" "共發現 ${issue_count} 個潛在安全問題"
        
        return 0
    else
        log "ERROR" "SAST 掃描過程發生錯誤或發現嚴重安全問題"
        log "WARNING" "請檢視報告檔案: $output_file"
        return 1
    fi
}

SAST 掃描的實作考慮了報告的可用性與後續處理需求。我們同時產生文字格式與 JSON 格式的報告,前者便於人工審閱,後者則方便程式化處理與整合至其他系統。嚴重度過濾參數 -ll 確保我們專注於真正重要的安全問題,避免被大量低風險警告淹沒。

依賴套件安全掃描

現代應用程式大量使用第三方依賴套件,這些套件可能包含已知的安全漏洞。OWASP Dependency-Check 能夠掃描專案中的依賴套件,並比對 CVE(Common Vulnerabilities and Exposures)資料庫,識別已知漏洞。

#!/bin/bash

# 依賴性掃描函式
# 功能:使用 OWASP Dependency-Check 掃描依賴套件的已知漏洞
# 返回:0 表示掃描成功,1 表示掃描失敗
perform_dependency_scan() {
    log "INFO" "開始執行依賴套件安全掃描"
    log "INFO" "掃描目標: $SCAN_DIR"
    
    # 定義輸出檔案路徑(不含副檔名)
    local output_file="${REPORT_DIR}/${REPORT_NAME}_deps"
    
    # 執行 Dependency-Check 掃描
    # --scan: 指定掃描目錄
    # --out: 指定輸出目錄
    # --format: 指定輸出格式(ALL 會產生多種格式)
    # --enableExperimental: 啟用實驗性分析器
    # --failOnCVSS: 當 CVSS 分數超過指定值時返回錯誤
    if dependency-check \
        --scan "$SCAN_DIR" \
        --out "$output_file" \
        --format ALL \
        --enableExperimental \
        --failOnCVSS 7; then
        log "INFO" "依賴性掃描完成"
        
        # 檢查是否產生了 HTML 報告
        if [[ -f "${output_file}.html" ]]; then
            log "INFO" "HTML 報告已產生: ${output_file}.html"
        fi
        
        return 0
    else
        local exit_code=$?
        if [[ $exit_code -eq 1 ]]; then
            log "WARNING" "發現 CVSS 分數 >= 7 的高風險漏洞"
            log "WARNING" "請立即檢視報告並更新受影響的依賴套件"
        else
            log "ERROR" "依賴性掃描過程發生錯誤"
        fi
        return 1
    fi
}

依賴性掃描的實作重點在於風險等級的判斷。透過 --failOnCVSS 參數,我們設定當發現 CVSS 分數達到 7 分(高風險)以上的漏洞時,掃描會返回錯誤狀態,這能夠在 CI/CD 流程中觸發相應的處理機制,例如阻止部署或發送告警通知。

容器映像安全掃描

容器化應用的安全性不僅取決於應用程式本身,更受到基礎映像與執行環境的影響。Trivy 是一個全面的容器安全掃描工具,能夠檢測作業系統套件與應用程式依賴中的漏洞。

#!/bin/bash

# 容器掃描函式
# 功能:掃描 Docker 容器映像中的安全漏洞
# 返回:0 表示掃描成功且未發現嚴重問題,1 表示發現問題或掃描失敗
perform_container_scan() {
    log "INFO" "開始執行容器映像安全掃描"
    
    # 定義輸出檔案路徑
    local output_file="${REPORT_DIR}/${REPORT_NAME}_containers.json"
    local has_vulnerabilities=0
    
    # 使用 find 指令搜尋所有 Dockerfile
    # -print0 與 read -d '' 的組合能夠正確處理包含空白字元的檔案名稱
    while IFS= read -r -d '' dockerfile; do
        local dir_name=$(dirname "$dockerfile")
        local image_name=$(basename "$dir_name")
        
        log "INFO" "發現 Dockerfile: $dockerfile"
        log "INFO" "開始建置容器映像: scan_target:${image_name}"
        
        # 建置 Docker 映像
        # -t: 指定映像標籤
        # --quiet: 減少建置過程的輸出
        if docker build -t "scan_target:${image_name}" "$dir_name" --quiet; then
            log "INFO" "容器映像建置成功"
            log "INFO" "開始掃描映像: scan_target:${image_name}"
            
            # 執行 Trivy 掃描
            # image: 掃描容器映像
            # -f json: 輸出 JSON 格式
            # -o: 指定輸出檔案
            # --severity: 只報告指定嚴重度的漏洞
            # --exit-code: 發現漏洞時的退出碼
            if ! trivy image \
                -f json \
                -o "${output_file%.json}_${image_name}.json" \
                --severity HIGH,CRITICAL \
                --exit-code 1 \
                "scan_target:${image_name}"; then
                log "WARNING" "在映像 scan_target:${image_name} 中發現高風險或嚴重漏洞"
                has_vulnerabilities=1
            else
                log "INFO" "映像 scan_target:${image_name} 掃描完成,未發現嚴重問題"
            fi
            
            # 清理暫存映像以節省磁碟空間
            log "INFO" "清理暫存映像: scan_target:${image_name}"
            docker rmi "scan_target:${image_name}" --force > /dev/null 2>&1
        else
            log "ERROR" "容器映像建置失敗: $dockerfile"
            return 1
        fi
    done < <(find "$SCAN_DIR" -name "Dockerfile" -print0)
    
    # 檢查是否有掃描結果
    if [[ $has_vulnerabilities -eq 1 ]]; then
        log "WARNING" "容器掃描發現安全漏洞,請檢視詳細報告"
        return 1
    else
        log "INFO" "所有容器映像掃描完成"
        return 0
    fi
}

容器掃描的實作流程較為複雜,需要先建置容器映像,再對映像進行掃描,最後清理暫存資源。這個過程需要仔細處理錯誤情況與資源清理,確保即使在掃描失敗的情況下也不會留下大量的暫存映像佔用磁碟空間。

安全掃描報告的產生

完整的安全掃描報告能夠將各種掃描結果整合呈現,提供清晰的安全狀況總覽。我們採用 Markdown 格式產生報告,既便於人工閱讀,也方便轉換為其他格式或整合至文件系統。

#!/bin/bash

# 報告產生函式
# 功能:整合所有掃描結果,產生綜合性的安全報告
generate_summary() {
    local summary_file="${REPORT_DIR}/${REPORT_NAME}_summary.md"
    
    log "INFO" "開始產生安全掃描綜合報告"
    
    # 產生報告標題與基本資訊
    cat << EOF > "$summary_file"
# 安全掃描綜合報告

## 掃描資訊

**掃描日期時間**:$(date +"%Y年%m月%d日 %H:%M:%S")  
**掃描目標**:$SCAN_DIR  
**報告產生者**:$(whoami)@$(hostname)  

---

## SAST 靜態程式碼分析結果

### 掃描摘要

EOF

    # 整合 SAST 掃描結果
    if [[ -f "${REPORT_DIR}/${REPORT_NAME}_sast.txt" ]]; then
        # 擷取最後 20 行的掃描結果
        echo '```' >> "$summary_file"
        tail -n 20 "${REPORT_DIR}/${REPORT_NAME}_sast.txt" >> "$summary_file"
        echo '```' >> "$summary_file"
        
        # 如果存在 JSON 報告,統計問題數量
        if [[ -f "${REPORT_DIR}/${REPORT_NAME}_sast.json" ]]; then
            local high_severity=$(jq '[.results[].issue_severity] | map(select(. == "HIGH")) | length' \
                "${REPORT_DIR}/${REPORT_NAME}_sast.json" 2>/dev/null || echo "0")
            local medium_severity=$(jq '[.results[].issue_severity] | map(select(. == "MEDIUM")) | length' \
                "${REPORT_DIR}/${REPORT_NAME}_sast.json" 2>/dev/null || echo "0")
            
            cat << EOF >> "$summary_file"

### 問題統計

- **高嚴重度問題**:${high_severity} 個
- **中等嚴重度問題**:${medium_severity} 個

EOF
        fi
    else
        echo "未執行 SAST 掃描或掃描結果不存在" >> "$summary_file"
    fi
    
    # 整合依賴性掃描結果
    cat << EOF >> "$summary_file"

---

## 依賴套件安全掃描結果

### 已知漏洞

EOF

    if [[ -f "${REPORT_DIR}/${REPORT_NAME}_deps.txt" ]]; then
        # 搜尋並顯示已知漏洞資訊
        echo '```' >> "$summary_file"
        grep -A 10 "One or more dependencies were identified with known vulnerabilities" \
            "${REPORT_DIR}/${REPORT_NAME}_deps.txt" 2>/dev/null || echo "未發現已知漏洞"
        echo '```' >> "$summary_file"
    else
        echo "未執行依賴性掃描或掃描結果不存在" >> "$summary_file"
    fi
    
    # 整合容器掃描結果
    cat << EOF >> "$summary_file"

---

## 容器映像安全掃描結果

### 高風險與嚴重漏洞

EOF

    # 搜尋所有容器掃描的 JSON 報告
    local container_reports=(${REPORT_DIR}/${REPORT_NAME}_containers_*.json)
    
    if [[ -f "${container_reports[0]}" ]]; then
        for report in "${container_reports[@]}"; do
            local image_name=$(basename "$report" .json | sed "s/${REPORT_NAME}_containers_//")
            echo "#### 映像:$image_name" >> "$summary_file"
            echo '```' >> "$summary_file"
            
            # 使用 jq 過濾高風險與嚴重漏洞
            jq -r '.Results[]? | select(.Vulnerabilities != null) | 
                .Vulnerabilities[] | select(.Severity == "HIGH" or .Severity == "CRITICAL") | 
                "- [\(.Severity)] \(.VulnerabilityID): \(.Title)"' \
                "$report" 2>/dev/null || echo "未發現高風險或嚴重漏洞"
            
            echo '```' >> "$summary_file"
            echo "" >> "$summary_file"
        done
    else
        echo "未執行容器掃描或掃描結果不存在" >> "$summary_file"
    fi
    
    # 加入建議與後續步驟
    cat << EOF >> "$summary_file"

---

## 建議與後續步驟

### 立即處理項目

1. 優先修復所有嚴重與高風險漏洞
2. 更新含有已知漏洞的依賴套件至安全版本
3. 檢視並修正 SAST 工具標記的程式碼問題

### 長期改善方向

1. 建立定期安全掃描機制
2. 整合安全掃描至 CI/CD 流程
3. 建立漏洞追蹤與管理流程
4. 加強開發團隊的安全意識培訓

---

**報告結束**

EOF

    # 確保報告檔案不為空
    if [[ ! -s "$summary_file" ]]; then
        echo "# 掃描完成,未發現安全問題" >> "$summary_file"
    fi
    
    log "INFO" "安全掃描綜合報告已產生: $summary_file"
}

報告產生函式整合了所有掃描工具的結果,並提供統計資訊與建議。使用 jq 工具解析 JSON 格式的掃描報告,能夠精確提取所需的資訊。報告的結構化設計使得閱讀者能夠快速掌握安全狀況的整體輪廓,並根據嚴重程度排定處理優先順序。

完整的 DevSecOps 掃描指令碼

整合前述所有功能,我們建立一個完整的 DevSecOps 安全掃描指令碼。這個指令碼展示了如何將各個元件組合成一個可在生產環境中使用的工具。

#!/bin/bash

#############################################
# DevSecOps 安全掃描指令碼
# 版本:1.0.0
# 作者:玄貓(BlackCat)
# 功能:整合 SAST、依賴性與容器安全掃描
#############################################

# 設定全域變數
readonly SCAN_DIR="${1:-.}"                    # 掃描目標目錄,預設為當前目錄
readonly REPORT_DIR="/opt/devsecops/reports"   # 報告輸出目錄
readonly REPORT_NAME="security_report_$(date +%Y%m%d_%H%M%S)"  # 報告名稱(含時間戳記)

# 設定嚴格模式
set -euo pipefail

# 初始化日誌系統
setup_logging() {
    # 建立報告目錄
    mkdir -p "$REPORT_DIR"
    
    # 重定向所有輸出至日誌檔案
    exec &> >(tee -a "${REPORT_DIR}/${REPORT_NAME}_execution.log")
    
    log "INFO" "========================================="
    log "INFO" "DevSecOps 安全掃描系統"
    log "INFO" "========================================="
}

# 日誌記錄函式
log() {
    local level=$1
    shift
    local message="$*"
    local timestamp=$(date +"%Y-%m-%d %H:%M:%S")
    
    # 根據日誌等級使用不同的輸出格式
    case "$level" in
        ERROR)
            echo "❌ $timestamp [${level}] ${message}"
            ;;
        WARNING)
            echo "⚠️  $timestamp [${level}] ${message}"
            ;;
        INFO)
            echo "ℹ️  $timestamp [${level}] ${message}"
            ;;
        DEBUG)
            echo "🔍 $timestamp [${level}] ${message}"
            ;;
        *)
            echo "$timestamp [${level}] ${message}"
            ;;
    esac
}

# 錯誤處理函式
error_handler() {
    local lineno=$1
    local err_code=$2
    log "ERROR" "指令碼執行失敗於第 ${lineno} 行,錯誤代碼: ${err_code}"
    log "ERROR" "請檢查日誌檔案: ${REPORT_DIR}/${REPORT_NAME}_execution.log"
    exit "$err_code"
}

# 設定錯誤捕捉
trap 'error_handler ${LINENO} $?' ERR

# 環境驗證函式
validate_environment() {
    log "INFO" "開始驗證執行環境..."
    
    local required_tools=("docker" "trivy" "dependency-check" "bandit" "jq")
    local missing_tools=()
    
    for tool in "${required_tools[@]}"; do
        if ! command -v "$tool" &> /dev/null; then
            missing_tools+=("$tool")
        fi
    done
    
    if [[ ${#missing_tools[@]} -gt 0 ]]; then
        log "ERROR" "缺少必要工具: ${missing_tools[*]}"
        log "INFO" "請安裝缺少的工具後再執行此指令碼"
        return 1
    fi
    
    if [[ ! -d "$REPORT_DIR" ]]; then
        log "INFO" "建立報告目錄: $REPORT_DIR"
        mkdir -p "$REPORT_DIR"
    fi
    
    if [[ ! -w "$REPORT_DIR" ]]; then
        log "ERROR" "無法寫入報告目錄: $REPORT_DIR"
        return 1
    fi
    
    log "INFO" "環境驗證完成 ✓"
    return 0
}

# 目標驗證函式
validate_target() {
    log "INFO" "驗證掃描目標: $SCAN_DIR"
    
    if [[ ! -d "$SCAN_DIR" ]]; then
        log "ERROR" "掃描目標不存在: $SCAN_DIR"
        return 1
    fi
    
    if [[ ! -r "$SCAN_DIR" ]]; then
        log "ERROR" "無法讀取掃描目標: $SCAN_DIR"
        return 1
    fi
    
    log "INFO" "掃描目標驗證完成 ✓"
    return 0
}

# SAST 掃描函式
perform_sast_scan() {
    log "INFO" "========================================="
    log "INFO" "執行 SAST 靜態程式碼分析"
    log "INFO" "========================================="
    
    local output_file="${REPORT_DIR}/${REPORT_NAME}_sast.txt"
    local json_output="${REPORT_DIR}/${REPORT_NAME}_sast.json"
    
    if bandit -r "$SCAN_DIR" -f txt -o "$output_file" -ll 2>&1; then
        bandit -r "$SCAN_DIR" -f json -o "$json_output" -ll 2>&1
        log "INFO" "SAST 掃描完成 ✓"
        return 0
    else
        log "WARNING" "SAST 掃描發現潛在問題"
        return 0  # 不中斷執行流程
    fi
}

# 依賴性掃描函式
perform_dependency_scan() {
    log "INFO" "========================================="
    log "INFO" "執行依賴套件安全掃描"
    log "INFO" "========================================="
    
    local output_file="${REPORT_DIR}/${REPORT_NAME}_deps"
    
    if dependency-check \
        --scan "$SCAN_DIR" \
        --out "$output_file" \
        --format ALL \
        --enableExperimental 2>&1; then
        log "INFO" "依賴性掃描完成 ✓"
        return 0
    else
        log "WARNING" "依賴性掃描發現已知漏洞"
        return 0  # 不中斷執行流程
    fi
}

# 容器掃描函式
perform_container_scan() {
    log "INFO" "========================================="
    log "INFO" "執行容器映像安全掃描"
    log "INFO" "========================================="
    
    local dockerfile_count=0
    
    while IFS= read -r -d '' dockerfile; do
        ((dockerfile_count++))
        local dir_name=$(dirname "$dockerfile")
        local image_name=$(basename "$dir_name")
        
        log "INFO" "處理 Dockerfile: $dockerfile"
        
        if docker build -t "scan_target:${image_name}" "$dir_name" --quiet; then
            if ! trivy image \
                -f json \
                -o "${REPORT_DIR}/${REPORT_NAME}_containers_${image_name}.json" \
                --severity HIGH,CRITICAL \
                --exit-code 1 \
                "scan_target:${image_name}" 2>&1; then
                log "WARNING" "映像 $image_name 存在安全漏洞"
            fi
            
            docker rmi "scan_target:${image_name}" --force > /dev/null 2>&1
        fi
    done < <(find "$SCAN_DIR" -name "Dockerfile" -print0)
    
    if [[ $dockerfile_count -eq 0 ]]; then
        log "INFO" "未發現 Dockerfile,跳過容器掃描"
    else
        log "INFO" "容器掃描完成,共處理 $dockerfile_count 個映像 ✓"
    fi
    
    return 0
}

# 報告產生函式(使用前面定義的版本)
generate_summary() {
    local summary_file="${REPORT_DIR}/${REPORT_NAME}_summary.md"
    
    log "INFO" "========================================="
    log "INFO" "產生安全掃描綜合報告"
    log "INFO" "========================================="
    
    # 此處使用前面定義的完整報告產生邏輯
    # (為節省篇幅,此處省略重複程式碼)
    
    log "INFO" "報告已產生: $summary_file ✓"
}

# 主函式
main() {
    local exit_code=0
    
    # 初始化
    setup_logging
    
    log "INFO" "掃描目標: $SCAN_DIR"
    log "INFO" "報告目錄: $REPORT_DIR"
    log "INFO" "報告名稱: $REPORT_NAME"
    log "INFO" ""
    
    # 環境與目標驗證
    validate_environment || exit 1
    validate_target || exit 1
    
    log "INFO" ""
    
    # 執行安全掃描
    perform_sast_scan || ((exit_code++))
    perform_dependency_scan || ((exit_code++))
    perform_container_scan || ((exit_code++))
    
    # 產生報告
    generate_summary
    
    log "INFO" ""
    log "INFO" "========================================="
    log "INFO" "掃描作業完成"
    log "INFO" "退出代碼: $exit_code"
    log "INFO" "========================================="
    
    return "$exit_code"
}

# 執行主函式
main "$@"

這個完整的指令碼整合了所有安全掃描功能,並提供了友善的使用者介面與完整的錯誤處理機制。透過適當的日誌記錄與進度提示,使用者能夠清楚了解掃描進度與結果。

安全漏洞範例與檢測

為了展示安全掃描工具的實際效果,我們建立了包含已知安全漏洞的範例程式碼。這些範例涵蓋了常見的安全問題,包括命令注入、不安全的反序列化、SQL 注入等。

#!/usr/bin/env python3
"""
不安全的 Python 程式碼範例
此檔案包含多種安全漏洞,用於測試 SAST 工具的檢測能力
警告:請勿在生產環境使用此程式碼
"""

import subprocess
import yaml
import pickle
from flask import Flask, request, render_template_string

app = Flask(__name__)

# 漏洞 1:命令注入(Command Injection)
def unsafe_command_execution():
    """
    使用 shell=True 參數執行命令,存在命令注入風險
    攻擊者可透過控制輸入來執行任意系統命令
    """
    # 危險做法:允許 shell 解析
    user_input = request.args.get('file', 'file.txt')
    subprocess.check_output(['cat', user_input], shell=True)
    
    # 安全做法:不使用 shell=True,並驗證輸入
    # subprocess.check_output(['cat', sanitized_input], shell=False)

# 漏洞 2:不安全的 YAML 反序列化
def unsafe_yaml_deserialization():
    """
    使用 yaml.load() 而非 yaml.safe_load()
    可能導致任意程式碼執行
    """
    # 危險做法:可能執行惡意程式碼
    data = request.get_data()
    config = yaml.load(data)
    
    # 安全做法:使用 safe_load
    # config = yaml.safe_load(data)
    
    return config

# 漏洞 3:不安全的 Pickle 反序列化
def unsafe_pickle_deserialization():
    """
    Pickle 反序列化未經驗證的資料
    可能導致遠端程式碼執行
    """
    # 危險做法:反序列化未經驗證的資料
    data = request.get_data()
    obj = pickle.loads(data)
    
    # 安全做法:避免使用 pickle,或使用數位簽章驗證
    # 建議使用 JSON 等安全的序列化格式
    
    return obj

# 漏洞 4:SQL 注入
@app.route('/login', methods=['POST'])
def sql_injection_vulnerable():
    """
    直接將使用者輸入拼接至 SQL 查詢
    存在 SQL 注入風險
    """
    # 危險做法:字串拼接建立 SQL 查詢
    username = request.form['username']
    password = request.form['password']
    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    
    # 安全做法:使用參數化查詢
    # cursor.execute("SELECT * FROM users WHERE username=? AND password=?", (username, password))
    
    # 執行查詢(此處省略資料庫連接程式碼)
    return query

# 漏洞 5:伺服器端範本注入(SSTI)
@app.route('/render')
def template_injection_vulnerable():
    """
    直接將使用者輸入渲染為範本
    可能導致遠端程式碼執行
    """
    # 危險做法:直接渲染使用者輸入
    template = request.args.get('template', 'Hello World')
    return render_template_string(template)
    
    # 安全做法:使用預定義範本,不接受使用者輸入作為範本內容
    # return render_template('safe_template.html', user_input=template)

# 漏洞 6:在生產環境啟用除錯模式
if __name__ == '__main__':
    """
    在生產環境啟用除錯模式會暴露敏感資訊
    包括原始碼、環境變數、檔案系統路徑等
    """
    # 危險做法:在生產環境啟用除錯模式
    app.run(debug=True, host='0.0.0.0')
    
    # 安全做法:僅在開發環境啟用除錯模式
    # app.run(debug=False, host='127.0.0.1')

這個範例程式碼包含了多種常見的安全漏洞。Bandit 工具能夠檢測出這些問題,並在報告中標記其嚴重程度與建議的修復方法。

依賴套件漏洞範例

建立一個包含已知漏洞版本的 requirements.txt 檔案,用於測試依賴性掃描工具。

# Python 依賴套件清單
# 注意:此清單包含已知漏洞版本,僅供測試使用

# Flask 舊版本(存在多個已知漏洞)
Flask==2.0.1

# PyYAML 舊版本(存在反序列化漏洞 CVE-2020-14343)
PyYAML==5.3.1

# Werkzeug 舊版本(存在路徑遍歷漏洞)
Werkzeug==2.0.2

# Cryptography 舊版本(存在加密相關漏洞)
cryptography==3.3.2

# Jinja2 舊版本(存在沙盒逃逸漏洞 CVE-2020-28493)
Jinja2==2.11.2

# Requests 舊版本(存在 SSL 驗證問題)
requests==2.25.1

# 建議的安全版本更新:
# Flask>=2.3.0
# PyYAML>=6.0
# Werkzeug>=2.3.0
# cryptography>=41.0.0
# Jinja2>=3.1.0
# requests>=2.31.0

OWASP Dependency-Check 會比對這些套件版本與 CVE 資料庫,識別出已知的安全漏洞,並在報告中提供詳細的漏洞資訊與修復建議。

容器安全漏洞範例

建立一個包含安全問題的 Dockerfile,用於測試容器掃描工具。

# 不安全的 Dockerfile 範例
# 此檔案包含多種安全問題,僅供教學與測試使用

# 問題 1:使用含有已知漏洞的基礎映像
FROM python:3.8-slim-buster

# 問題 2:使用 root 使用者執行應用程式
# 未切換至非特權使用者,增加安全風險

# 問題 3:安裝含有已知漏洞的套件
RUN apt-get update && apt-get install -y \
    openssl=1.1.1d-0+deb10u6 \
    curl=7.64.0-4+deb10u1 \
    && rm -rf /var/lib/apt/lists/*

# 問題 4:暴露敏感資訊
ENV DATABASE_PASSWORD="admin123"
ENV API_KEY="secret_key_12345"

# 問題 5:複製不必要的檔案
COPY . /app/

# 問題 6:使用固定的連接埠且未限制連線來源
EXPOSE 8080

WORKDIR /app

# 問題 7:安裝 Python 依賴套件(含有已知漏洞)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 問題 8:以 root 使用者身份執行應用程式
CMD ["python", "app.py"]

# 安全建議:
# 1. 使用最新的基礎映像,並定期更新
# 2. 建立並切換至非特權使用者
# 3. 更新系統套件至最新版本
# 4. 使用 secrets 管理敏感資訊,不要寫入映像
# 5. 使用 .dockerignore 排除不必要的檔案
# 6. 限制容器的網路存取權限
# 7. 定期掃描並更新依賴套件
# 8. 使用非特權使用者執行應用程式

更安全的 Dockerfile 版本應該包含以下改進措施。

# 安全的 Dockerfile 範例
# 遵循容器安全最佳實務

# 使用最新的穩定版本基礎映像
FROM python:3.12-slim-bookworm AS base

# 建立非特權使用者
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 安裝系統相依套件並立即清理
RUN apt-get update && apt-get install -y --no-install-recommends \
    openssl \
    curl \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 設定工作目錄
WORKDIR /app

# 複製相依套件清單(利用 Docker 快取層)
COPY --chown=appuser:appuser requirements.txt .

# 安裝 Python 依賴套件
RUN pip install --no-cache-dir --upgrade pip && \
    pip install --no-cache-dir -r requirements.txt

# 只複製必要的應用程式檔案
COPY --chown=appuser:appuser app.py .

# 切換至非特權使用者
USER appuser

# 暴露應用程式連接埠
EXPOSE 8080

# 設定健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# 執行應用程式
CMD ["python", "app.py"]

Trivy 掃描工具會識別第一個 Dockerfile 中的各種安全問題,包括過時的基礎映像、已知的 CVE 漏洞、不安全的配置等,並在報告中提供詳細的修復建議。

GitLab CI/CD 整合實務

將安全掃描整合至 CI/CD 流程是實現 DevSecOps 的關鍵步驟。以下將詳細說明如何在 GitLab 環境中部署與配置整個安全掃描系統。

GitLab 環境準備

首先需要準備 GitLab 執行環境。如果使用 Docker 部署 GitLab,可以透過以下方式取得初始管理員密碼。

#!/bin/bash

# 取得 GitLab 初始 root 密碼
# GitLab 安裝後會產生隨機的 root 密碼
sudo docker exec -it gitlab grep 'Password:' /etc/gitlab/initial_root_password

# 密碼將顯示如下格式:
# Password: 5iveL!fe5Lfwfq4sZjLEWJe9+wvg7qHLmn4FKSdJP/c=

# 使用此密碼登入 GitLab
# 預設網址:http://localhost 或您配置的網域
# 使用者名稱:root

登入後建議立即更改預設密碼,並建立專用的使用者帳號以符合最小權限原則。在 GitLab 介面中,導航至管理區域,選擇建立新使用者,填寫必要資訊後儲存。接著編輯該使用者,設定強式密碼並啟用帳號。

Personal Access Token 配置

為了讓 CI/CD Runner 能夠存取 GitLab API,需要建立 Personal Access Token。導航至使用者設定頁面,選擇存取權杖選項,點選新增權杖按鈕。設定權杖名稱(例如 devsecops-scanner)與到期日,選擇所需的權限範圍(建議至少包含 api、read_repository、write_repository 權限),然後建立權杖並妥善保存。

專案建立與配置

在 GitLab 中建立新專案,命名為 vulnerable-flask-app。這個專案將包含我們前面準備的漏洞範例程式碼與安全掃描配置。將專案初始化後,上傳準備好的程式碼檔案,包括包含漏洞的 Python 程式碼、requirements.txt 與 Dockerfile。

GitLab Runner 安裝與註冊

GitLab Runner 是執行 CI/CD 作業的代理程式。首先需要在目標主機上安裝 GitLab Runner。

#!/bin/bash

# 下載並安裝 GitLab Runner(以 Ubuntu/Debian 為例)
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt-get install gitlab-runner

# 驗證安裝
gitlab-runner --version

# 從專案的 CI/CD 設定頁面取得 Registration Token
# 導航至: 專案 -> 設定 -> CI/CD -> Runners -> 展開

# 註冊 Runner
sudo gitlab-runner register \
    --url "http://localhost" \
    --registration-token "YOUR_REGISTRATION_TOKEN_HERE" \
    --description "docker-runner-security-scan" \
    --executor "docker" \
    --docker-image "docker:24-dind" \
    --docker-privileged \
    --docker-volumes "/var/run/docker.sock:/var/run/docker.sock" \
    --docker-volumes "/cache" \
    --docker-network-mode "host" \
    --clone-url "http://localhost"

# 驗證 Runner 狀態
sudo gitlab-runner verify
sudo gitlab-runner list

這個配置使用 Docker executor,並啟用 privileged 模式以支援 Docker-in-Docker 操作,這是執行容器掃描所必需的。

安全掃描環境準備

為了讓 GitLab Runner 能夠執行安全掃描指令碼,需要準備相應的目錄結構與權限設定。

#!/bin/bash

# 建立 DevSecOps 工作目錄
sudo mkdir -p /opt/devsecops/scripts
sudo mkdir -p /opt/devsecops/reports

# 複製安全掃描指令碼至指定位置
# 假設指令碼已儲存為 security_scanner.sh
sudo cp security_scanner.sh /opt/devsecops/scripts/
sudo chmod +x /opt/devsecops/scripts/security_scanner.sh

# 設定適當的擁有者與權限
# gitlab-runner 是執行 CI/CD 作業的使用者
sudo chown -R gitlab-runner:gitlab-runner /opt/devsecops
sudo chmod -R 755 /opt/devsecops

# 將 gitlab-runner 使用者加入 docker 群組
# 這樣 Runner 才能執行 Docker 指令
sudo usermod -aG docker gitlab-runner

# 重新啟動 GitLab Runner 服務以套用變更
sudo systemctl restart gitlab-runner

# 驗證權限設定
sudo -u gitlab-runner ls -la /opt/devsecops
sudo -u gitlab-runner docker ps

GitLab CI/CD Pipeline 配置

建立 .gitlab-ci.yml 檔案來定義 CI/CD 流程。這個檔案描述了如何在不同階段執行安全掃描。

# GitLab CI/CD Pipeline 配置
# 整合 DevSecOps 安全掃描至持續整合流程

# 定義流程階段
stages:
  - build          # 建置階段
  - test           # 測試階段
  - security-scan  # 安全掃描階段
  - deploy         # 部署階段

# 定義全域變數
variables:
  SCAN_TARGET: "."
  REPORT_DIR: "/opt/devsecops/reports"
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""

# 建置階段
build:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  script:
    - echo "開始建置應用程式..."
    - docker build -t vulnerable-app:${CI_COMMIT_SHORT_SHA} .
    - echo "建置完成"
  tags:
    - docker
  only:
    - main
    - merge_requests

# 單元測試階段
unit-test:
  stage: test
  image: python:3.12-slim
  script:
    - echo "執行單元測試..."
    - pip install -r requirements.txt
    - pip install pytest pytest-cov
    - pytest --cov=. --cov-report=xml
  coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml
  tags:
    - docker
  only:
    - main
    - merge_requests

# 安全掃描階段
security-scan:
  stage: security-scan
  image: docker:24-dind
  services:
    - docker:24-dind
  before_script:
    # 安裝必要的安全掃描工具
    - apk add --no-cache bash curl jq python3 py3-pip
    - pip3 install bandit --break-system-packages
    
    # 安裝 Trivy
    - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
    
    # 安裝 OWASP Dependency-Check
    - |
      curl -L https://github.com/jeremylong/DependencyCheck/releases/download/v8.4.0/dependency-check-8.4.0-release.zip -o dependency-check.zip
      unzip dependency-check.zip -d /opt/
      ln -s /opt/dependency-check/bin/dependency-check.sh /usr/local/bin/dependency-check
    
    # 驗證工具安裝
    - bandit --version
    - trivy --version
    - dependency-check --version
  
  script:
    # 執行安全掃描指令碼
    - echo "開始執行安全掃描..."
    - bash /opt/devsecops/scripts/security_scanner.sh ${SCAN_TARGET}
    - echo "安全掃描完成"
  
  after_script:
    # 收集掃描報告
    - echo "收集掃描報告..."
    - ls -lah ${REPORT_DIR}
  
  artifacts:
    # 保留掃描報告作為建置產物
    name: "security-reports-${CI_COMMIT_SHORT_SHA}"
    paths:
      - ${REPORT_DIR}/*.md
      - ${REPORT_DIR}/*.txt
      - ${REPORT_DIR}/*.json
      - ${REPORT_DIR}/*.html
    expire_in: 30 days
    reports:
      # 整合至 GitLab Security Dashboard
      sast: ${REPORT_DIR}/*_sast.json
      dependency_scanning: ${REPORT_DIR}/*_deps.json
      container_scanning: ${REPORT_DIR}/*_containers*.json
  
  # 允許掃描失敗但不阻斷流程
  # 在生產環境中建議移除此設定
  allow_failure: true
  
  tags:
    - docker
  
  only:
    - main
    - merge_requests

# 部署階段(僅在通過所有測試後執行)
deploy:
  stage: deploy
  image: docker:24-dind
  services:
    - docker:24-dind
  script:
    - echo "準備部署應用程式..."
    - docker tag vulnerable-app:${CI_COMMIT_SHORT_SHA} vulnerable-app:latest
    - echo "部署完成"
  tags:
    - docker
  only:
    - main
  when: manual  # 需要手動觸發部署

這個 Pipeline 配置實現了完整的 DevSecOps 流程。建置階段產生應用程式映像,測試階段執行單元測試與程式碼覆蓋率分析,安全掃描階段執行全方位的安全檢測,最後在部署階段將通過所有檢查的應用程式部署至目標環境。

掃描報告分析

執行 Pipeline 後,可以在 GitLab 介面中查看掃描結果。導航至專案的 CI/CD Pipeline 頁面,選擇最近的執行記錄,點選 security-scan 作業,即可查看執行日誌與下載完整的掃描報告。

綜合報告會以 Markdown 格式呈現,包含 SAST 掃描發現的程式碼問題、依賴套件中的已知漏洞、容器映像的安全風險等資訊。每個問題都會標示嚴重程度,並提供修復建議與參考連結。

進階實務與最佳實踐

實施 DevSecOps 安全掃描不僅是工具的使用,更需要建立完善的流程與文化。以下是一些進階實務建議,能夠進一步提升安全掃描的效果與價值。

掃描結果的優先級管理

並非所有掃描結果都需要立即處理。建立明確的優先級判斷機制,能夠確保團隊專注於最重要的安全問題。一般而言,CVSS 分數 9.0 以上的嚴重漏洞應該立即處理,7.0 至 8.9 的高風險漏洞應在一週內修復,4.0 至 6.9 的中等風險漏洞可以排入下一個開發週期,而 3.9 以下的低風險問題可以視情況處理。

誤報的處理機制

安全掃描工具難免會產生誤報。建立誤報處理機制,包括建立白名單、調整掃描規則、記錄誤報原因等,能夠減少團隊在無效警報上浪費的時間。每次確認誤報時,都應該詳細記錄判斷依據,並定期檢視這些決定是否仍然有效。

持續改進的文化

DevSecOps 不是一次性的專案,而是持續改進的過程。定期檢視掃描結果的趨勢,分析漏洞引入的原因,並調整開發流程以從源頭避免問題。建立安全意識培訓機制,讓開發團隊了解常見的安全問題與最佳實踐,能夠從根本上提升程式碼品質。

效能最佳化考量

安全掃描可能耗費大量時間與運算資源。考慮實施增量掃描,只對變更的部分進行檢測。對於大型專案,可以將完整掃描安排在夜間執行,而在開發過程中只執行快速掃描。適當的快取機制也能夠顯著減少重複掃描的時間。

結語

本文深入探討了在 DevSecOps 環境中編寫安全導向 Bash 指令碼的各個面向。從基礎的日誌記錄與錯誤處理,到整合業界標準的安全掃描工具,再到完整的 CI/CD 流程整合,我們建立了一個完整的安全掃描自動化方案。

透過系統化的安全掃描流程,我們能夠在開發早期就發現並修復安全問題,大幅降低安全風險與修復成本。這種左移的安全策略,正是 DevSecOps 理念的核心體現。隨著安全威脅日益複雜,將安全性深度整合至開發流程已不再是選項,而是必要的實踐。

持續學習與改進是 DevSecOps 成功的關鍵。技術工具會不斷演進,安全威脅會持續變化,唯有保持學習熱情與改進動力,才能在這個快速變化的領域中保持競爭力。希望本文提供的知識與實務經驗,能夠協助讀者在 DevSecOps 的旅程中邁出堅實的步伐。