今回は、Jenkinsのパイプラインを作る機能の一つであるフリースタイルジョブ
を使って、ビルドされた成果物に対して、SBOM生成、脆弱性検知を行うビルドパイプラインを作成していきます。
Jenkinsのジョブの初期設定については、前回の記事をご覧ください。
本記事では、パイプラインの動作を記述したshellスクリプトについて解説を行います。
このパイプラインでは、以下のような前提を想定しています。
-
main
ブランチ一つだけ利用し、開発を進める - ブランチへのタグ付けは行わない
- ビルド成果物はコンテナイメージとする
本パイプラインで実行されるシェルスクリプトは、以下2つに分かれています。
- Script1: SBOMを生成する
- Script2: 脆弱性検知を行い、その結果をSlackへ通知する
これらのスクリプトをJenkinsジョブの設定にあるBuild Steps
へshellスクリプトを書き込みます。
- パイプラインアーキテクチャ (作成中)
- 全スクリプト (作成途中)
- Script1: SBOM生成
- Script4 : 脆弱性検知、結果通知用スクリプト
- Slackのwebhookの設定
- Slackへの通知結果
- Teamsへ通知を行う場合
パイプラインアーキテクチャ (作成中)
全スクリプト (作成途中)
こちら、全スクリプトです。
SBOM用スクリプト
#現在のタグとその前のタグの環境変数を定義
#VERSION_TAG_LIST=`git tag -l 'v*'` || true
#TAG_HASH=`git rev-list ${VERSION_TAG_LIST} --max-count=1` || true
#TAG_NAME=`git describe --abbrev=0 --tags ${TAG_HASH}` || true
#PRE_TAG_HASH=`git rev-list ${VERSION_TAG_LIST} --skip=1 --max-count=1` || true
#PRE_TAG_NAME=`git describe --abbrev=0 --tags ${PRE_TAG_HASH}` || true
#echo $TAG_NAME || true
#echo $PRE_TAG_NAME || true
BRANCH_NAME=`echo $GIT_BRANCH | cut -d '/' -f 2`
REPOSITORY_NAME='Jenkins_pipeline_test'
#sbomディレクトリを作成し、最新ブランチに対してsbom_${BRANCH_NAME}.jsonを生成
SBOM_DIR=sbom/
SBOM_DIR_CURRE=sbom_curre/
SBOM_DIR_PAST=sbom_past/
SBOM_FILE="sbom_${REPOSITORY_NAME}_${BRANCH_NAME}"
SBOM_LIST_DIR=sbom_list/
SBOM_LIST_DIR_CURRE=sbom_list_curre/
SBOM_LIST_DIR_PAST=sbom_list_past/
SBOM_FILE_ARRANGED="sbom_list_${REPOSITORY_NAME}_${BRANCH_NAME}"
#ディレクトリを作る
if [ ! -d ${SBOM_DIR} ]; then
mkdir -p ${SBOM_DIR}${SBOM_DIR_CURRE}
mkdir -p ${SBOM_DIR}${SBOM_DIR_PAST}
fi
if [ ! -d ${SBOM_LIST_DIR} ]; then
mkdir -p ${SBOM_LIST_DIR}${SBOM_LIST_DIR_CURRE}
mkdir -p ${SBOM_LIST_DIR}${SBOM_LIST_DIR_PAST}
fi
if [ ! -d ${SBOM_LIST_DIR}${SBOM_LIST_DIR_CURRE}${SBOM_FILE_ARRANGED} ]; then
mkdir -p ${SBOM_LIST_DIR}${SBOM_LIST_DIR_CURRE}${SBOM_FILE_ARRANGED}
fi
#vulnディレクトリを作成し、現在のタグに対してvuln_table_${BRANCH_NAME}.txtを生成
VULN_DIR="vuln/"
GRYPE_FILE="vuln_table_${REPOSITORY_NAME}_${BRANCH_NAME}"
VULN_DIR_CURRE="vuln_curre/"
VULN_DIR_PAST="vuln_past/"
if [ ! -d ${VULN_DIR} ]; then
mkdir -p ${VULN_DIR}${VULN_DIR_CURRE}
mkdir -p ${VULN_DIR}${VULN_DIR_PAST}
fi
#Docker build
imageName=poetry
imageTag="sbom"_$BUILD_ID
TARGET=$imageName:$imageTag
docker build -t ${TARGET} .
#syftでSBOMを生成する
sbom_create() {
local _target=$1
local _sbom_curre_file=$2
local _sbom_past_file=$3
syft --version
if [ -e "${_sbom_curre_file}" ]; then
cp "${_sbom_curre_file}" "${_sbom_past_file}"
fi
syft image:"${_target}" -o cyclonedx-json="${_sbom_curre_file}"
}
echo "---> start syft scan"
sbom_create ${TARGET} ${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json ${SBOM_DIR}${SBOM_DIR_PAST}${SBOM_FILE}_prev.json
docker image rm ${TARGET}
脆弱性検知
#脆弱性検知
DAY=`date +%Y%m%d%H%M`
BRANCH_NAME=`echo $GIT_BRANCH | cut -d '/' -f 2`
REPOSITORY_NAME='Jenkins_pipeline_test'
#sbomディレクトリを作成し、現在のタグに対してsbom_${TAG_NAME}.jsonを生成
#sbomディレクトリ
SBOM_DIR=sbom/
SBOM_DIR_CURRE=sbom_curre/
SBOM_DIR_PAST=sbom_past/
SBOM_FILE="sbom_${REPOSITORY_NAME}_${BRANCH_NAME}"
SBOM_DIFF_DIR=sbom_diff/
#日付処理
#cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt ${VULN_DIR}${VULN_DIR_PAST}${GRYPE_FILE}_${DAY}.txt
cp ${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json ${SBOM_DIR}${SBOM_DIR_PAST}${SBOM_FILE}_${DAY}.json
#grype sbom:./${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json --only-fixed -o table=${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}_fixed.txt
#grype sbom:./${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json --only-notfixed -o table=${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}_notfixed.txt
VULN_DIR="vuln/"
GRYPE_FILE="vuln_table_${REPOSITORY_NAME}_${BRANCH_NAME}"
VULN_DIR_CURRE="vuln_curre/"
VULN_DIR_PAST="vuln_past/"
if [ ! -d ${VULN_DIR} ]; then
mkdir -p ${VULN_DIR}${VULN_DIR_CURRE}
mkdir -p ${VULN_DIR}${VULN_DIR_PAST}
fi
#grypeで最新developブランチのSBOMをスキャンする
grype_vuln_scan() {
local _sbom_target=$1
local _results_file=$2
#grype db status
#export GRYPE_DB_AUTO_UPDATE=false
#export GRYPE_CHECK_FOR_APP_UPDATE=false
grype sbom:./"${_sbom_target}" -o table="${_results_file}".txt
grype sbom:./"${_sbom_target}" -o json="${_results_file}".json
}
detect_high_critical_vulnerabilities_fixed() {
local _fixed_state="$1"
cd "${_fixed_state}"
grep -E 'AV:N|AV:A' vuln_"${_fixed_state}"_tmp.csv | grep -E 'High|Critical' > vuln_"${_fixed_state}".csv || true
# 直前のコマンドの終了コードを取得
exit_code=$?
echo ${exit_code}
# 終了コードが0以外(エラーが発生した)場合はスキップする
if [ $exit_code -ne 0 ]; then
echo "${_fixed_state}" ": High or Critical are not found. Skipping the execution of the shell script."
#exit $exit_code
else
#cp vulntmp.csv vulntmp1.csv
rm vuln_"${_fixed_state}"_tmp.csv
awk -F '","' '!seen[$1 $3]++' vuln_"${_fixed_state}".csv > vuln_"${_fixed_state}"_tmp.csv
#sed -i '1s/^/Package name,version, purl, fixed-in, VULNERABILITY, SEVERITY, CVSS1, CVSS2, CVSS3, CVSS4\n/' vulntmp.csv
awk -F '","' '{printf("| %-15s | %-12s | %-13s | %-12s | %s |\n", $1, $2, $3, $4, $5)}' vuln_"${_fixed_state}"_tmp.csv > marktable_"${_fixed_state}".txt
sed -i '1s/^/|パッケージ名|バージョン|修正バージョン|VULNERABILITY|SEVERITY|\n/' marktable_"${_fixed_state}".txt
sed -i '2 i\|-----------------|--------------|--------------|----------------------|----------|' marktable_"${_fixed_state}".txt
sed 's/^\|"//g; s/"$//g' marktable_"${_fixed_state}".txt > marktable_"${_fixed_state}"0.txt
VULNERABILITY_DATA=\`\`\`$(cat marktable_"${_fixed_state}"0.txt)\`\`\`
rm marktable_"${_fixed_state}".txt marktable_"${_fixed_state}"0.txt
fi
cd ..
}
detect_high_critical_vulnerabilities_other() {
local _fixed_state="$1"
cd "${_fixed_state}"
grep -E 'AV:N|AV:A' vuln_"${_fixed_state}"_tmp.csv | grep -E 'High|Critical' > vuln_"${_fixed_state}".csv || true
# 直前のコマンドの終了コードを取得
exit_code=$?
echo ${exit_code}
# 終了コードが0以外(エラーが発生した)場合はスキップする
if [ $exit_code -ne 0 ]; then
echo "${_fixed_state}" ": High or Critical are not found. Skipping the execution of the shell script."
#exit $exit_code
else
#cp vulntmp.csv vulntmp1.csv
rm vuln_"${_fixed_state}"_tmp.csv
awk -F '","' '!seen[$1 $3]++' vuln_"${_fixed_state}".csv > vuln_"${_fixed_state}"_tmp.csv
#sed -i '1s/^/Package name,version, purl, fixed-in, VULNERABILITY, SEVERITY, CVSS1, CVSS2, CVSS3, CVSS4\n/' vulntmp.csv
awk -F '","' '{printf("| %-15s | %-12s | %-13s | %-12s |\n", $1, $2, $3, $4)}' vuln_"${_fixed_state}"_tmp.csv > marktable_"${_fixed_state}".txt
sed -i '1s/^/|パッケージ名|バージョン|VULNERABILITY|SEVERITY|\n/' marktable_"${_fixed_state}".txt
sed -i '2 i\|-----------------|--------------|--------------|----------|' marktable_"${_fixed_state}".txt
sed 's/^\|"//g; s/"$//g' marktable_"${_fixed_state}".txt > marktable_"${_fixed_state}"0.txt
VULNERABILITY_DATA=\`\`\`$(cat marktable_"${_fixed_state}"0.txt)\`\`\`
rm marktable_"${_fixed_state}".txt marktable_"${_fixed_state}"0.txt
fi
cd ..
}
echo "---> grype vulnerability-scan"
grype_vuln_scan ${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}
CRITICAL_NUM=`grep -o "Critical" ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt | wc -l`
HIGH_NUM=`grep -o "High" ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt | wc -l`
DIRECTORY_NAME=${GRYPE_FILE}/
if [ ! -d "${DIRECTORY_NAME}" ]; then
mkdir -p ${DIRECTORY_NAME}
fi
if [ -e "${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.json" ]; then
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt ${DIRECTORY_NAME}
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.json ${DIRECTORY_NAME}
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt ${VULN_DIR}${VULN_DIR_PAST}${GRYPE_FILE}_${DAY}.txt
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.json ${VULN_DIR}${VULN_DIR_PAST}${GRYPE_FILE}_${DAY}.json
fi
cd $DIRECTORY_NAME
# make fixed directory to store fixed vulnerabilities.
if [ ! -d "fixed" ]; then
mkdir -p fixed
fi
# FIXED
if cat ${GRYPE_FILE}.json | jq -r '.matches[] | .vulnerability.fix.state | contains("fixed")' | grep true >/dev/null; then
cat ${GRYPE_FILE}.json | jq -r '
.matches[]
| select(.vulnerability.fix.state == "fixed")
| [
.artifact.name,
.artifact.version,
.vulnerability.fix.versions[],
if (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss = null) and (.relatedVulnerabilities[0].id as $CVE | .vulnerability.id == $CVE)
then
.vulnerability.id,
.vulnerability.severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
elif (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss != null)
then
.vulnerability.id,
.vulnerability.severity,
(.vulnerability.cvss | last |.version),
(.vulnerability.cvss | last |.vector)
else
.relatedVulnerabilities[0].id,
.relatedVulnerabilities[0].severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
end
] | @csv' > fixed/vuln_fixed_tmp.csv
detect_high_critical_vulnerabilities_fixed fixed
VULNERABILITY_DATA_FIXED=${VULNERABILITY_DATA}
else
# .vulnerability.fix.stateが"fixed"を含まない場合、メッセージを表示
echo "The 'fixed' string is not found. Skipping the execution of the shell script."
fi
#NON-FIXED
if [ ! -d "not-fixed" ]; then
mkdir -p not-fixed
fi
if cat ${GRYPE_FILE}.json | jq -r '.matches[] | .vulnerability.fix.state | contains("not-fixed")' | grep true >/dev/null; then
cat ${GRYPE_FILE}.json | jq -r '
.matches[]
| select(.vulnerability.fix.state == "not-fixed")
| [
.artifact.name,
.artifact.version,
if (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss = null) and (.relatedVulnerabilities[0].id as $CVE | .vulnerability.id == $CVE)
then
.vulnerability.id,
.vulnerability.severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
elif (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss != null)
then
.vulnerability.id,
.vulnerability.severity,
(.vulnerability.cvss | last |.version),
(.vulnerability.cvss | last |.vector)
else
.relatedVulnerabilities[0].id,
.relatedVulnerabilities[0].severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
end
] | @csv' > not-fixed/vuln_not-fixed_tmp.csv
detect_high_critical_vulnerabilities_other not-fixed
VULNERABILITY_DATA_NOTFIXED=${VULNERABILITY_DATA}
else
# .vulnerability.fix.stateが"not-fixed"を含まない場合、メッセージを表示
echo "The 'not-fixed' string is not found. Skipping the execution of the shell script."
fi
# UNKNOWN
if [ ! -d "unknown" ]; then
mkdir -p unknown
fi
if cat ${GRYPE_FILE}.json | jq -r '.matches[] | .vulnerability.fix.state | contains("unknown") // contains("wont-fix")' | grep true >/dev/null; then
cat ${GRYPE_FILE}.json | jq -r '
.matches[]
| select(.vulnerability.fix.state == "unknown" or .vulnerability.fix.state == "wont-fix")
| [
.artifact.name,
.artifact.version,
if (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss = null) and (.relatedVulnerabilities[0].id as $CVE | .vulnerability.id == $CVE)
then
.vulnerability.id,
.vulnerability.severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
elif (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss != null)
then
.vulnerability.id,
.vulnerability.severity,
(.vulnerability.cvss | last |.version),
(.vulnerability.cvss | last |.vector)
else
.relatedVulnerabilities[0].id,
.relatedVulnerabilities[0].severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
end
] | @csv' > unknown/vuln_unknown_tmp.csv
detect_high_critical_vulnerabilities_other unknown
VULNERABILITY_DATA_UNKOWN=${VULNERABILITY_DATA}
else
# .vulnerability.fix.stateが"unknown"を含まない場合、メッセージを表示
echo "The 'unknown' string is not found. Skipping the execution of the shell script."
fi
cd ..
LIB=`awk 'BEGIN {
# テーブルヘッダーを出力
print "| NAME | INSTALLED-VERSION |"
print "|------|-----------|"
}
NR>1 {
# NAME と INSTALLED 列だけを出力
printf("| %s | %s |\n", $1, $2)
}' ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt | uniq`
JSON_INFO=`cat << EOF
{
"text": "レベルHigh, Criticalの脆弱性が見つかりました。",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "レベルHigh, Criticalの脆弱性が見つかりました。:"
}
},
{
"type": "section",
"block_id": "section789",
"fields": [
{
"type": "mrkdwn",
"text": "*日付*: ${DAY}"
},
{
"type": "mrkdwn",
"text": "*branch*: ${BRANCH_NAME}"
},
{
"type": "mrkdwn",
"text": "*JOB name*: ${JOB_NAME} #${BUILD_ID}"
},
{
"type": "mrkdwn",
"text": "*JOBのURL*: ${BUILD_URL}"
},
{
"type": "mrkdwn",
"text": "*CRITICALの件数*: ${CRITICAL_NUM}"
},
{
"type": "mrkdwn",
"text": "*HIGHの件数*: ${HIGH_NUM}"
},
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "FIXED-INに記されたバージョンへアップデートをお願いします"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${VULNERABILITY_DATA_FIXED}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "次の脆弱性については、修正バージョンが公開されていません。"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${VULNERABILITY_DATA_NOTFIXED}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "次の脆弱性については、脆弱性対策が知られていません。"
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${VULNERABILITY_DATA_UNKOWN}"
}
}
]
}
EOF`
#プロキシを設定する場合は、プロキシを環境変数で設定
#export HTTP_PROXY=
#export HTTPS_PROXY=
#Web hook用URLの設定
WEBHOOK_URL=https://hooks.slack.com/services/
if grep -q "Critical" ${DIRECTORY_NAME}${GRYPE_FILE}.json || grep -q "High" ${DIRECTORY_NAME}${GRYPE_FILE}.json; then
echo "alerm"
curl -X POST -H 'Content-type: application/json' -d "${JSON_INFO}" "${WEBHOOK_URL}"
#curl -H "Content-Type: application/json" -d "${JSON_INFO}" "${WEBHOOK_URL}"
fi
Script1: SBOM生成
Step1-1 : gitのブランチ、タグ情報の抽出と環境変数の設定
まず、パイプラインで生成されるディレクトリ、ファイル名にgitのリポジトリ、ブランチの情報を付与するため、これらの情報の環境変数を設定します。
このスクリプトで使用する環境変数を以下表にまとめました。
環境変数 | 説明 |
---|---|
VERSION_TAG_LIST | バージョンづけされたタグのリスト |
TAG_HASH | タグのリストから現在で最新バージョンタグのハッシュ値を取得する |
TAG_NAME | 最新バージョンタグの値を取得する |
PRE_TAG_HASH | 前回作成したバージョンタグのハッシュ値を取得する |
PRE_TAG_NAME | 前回作成したバージョンタグの値を取得する |
BRANCH_NAME | 現在のブランチ名を取得する |
REPOSITORY_NAME | リポジトリ名を設定する |
echo "Hello, Jenkins."
echo "---> set environment variable"
BRANCH_NAME=`echo $GIT_BRANCH | cut -d '/' -f 2`
REPOSITORY_NAME='Jenkins_pipeline_test'
Step1-2: ディレクトリ作成
次に、SBOMのファイル名、また、そのファイルを保存するためのディレクトリ名を環境変数として設定します。
環境変数 | 説明 |
---|---|
SBOM_DIR | SBOMを保管するためのディレクトリ |
SBOM_DIR_CURRE | 最新のSBOMを保管するためのディレクトリ |
SBOM_DIR_PAST | 過去のSBOMを保管するためのディレクトリ |
SBOM_FILE | 今回のジョブで生成するSBOMのファイル名 |
環境変数 | 説明 |
---|---|
SBOM_LIST_DIR | SBOMから作成したソフトウェアリストを保管するためのディレクトリ |
SBOM_LIST_DIR_CURRE | 最新のソフトウェアリストを保管するためのディレクトリ |
SBOM_LIST_DIR_PAST | 過去のソフトウェアリストを保管するためのディレクトリ |
SBOM_LIST_FILE | 今回作成したSBOMから作成したソフトウェアリスト |
環境変数 | 説明 |
---|---|
SBOM_DIFF_DIR | SBOMの差分を保管するためのディレクトリ |
SBOM_DIFF_FILE | SBOMの差分結果が記述されたファイル |
設定した環境変数に従い、ディレクトリを生成します。
#sbomディレクトリを作成し、mainブランチの最新のコミットに対してsbom_${REPOSITORY_NAME}_${BRANCH_NAME}.jsonを生成
SBOM_DIR=sbom/
SBOM_DIR_CURRE=sbom_curre/
SBOM_DIR_PAST=sbom_past/
SBOM_FILE="sbom_${REPOSITORY_NAME}_${BRANCH_NAME}"
#SBOMを保管するディレクトリを作成
if [ ! -d ${SBOM_DIR} ]; then
mkdir -p ${SBOM_DIR}${SBOM_DIR_CURRE}
mkdir -p ${SBOM_DIR}${SBOM_DIR_PAST}
fi
#脆弱性検知結果を保管するディレクトリを作成
VULN_DIR="vuln/"
GRYPE_FILE="vuln_table_${REPOSITORY_NAME}_${BRANCH_NAME}"
VULN_DIR_CURRE="vuln_curre/"
VULN_DIR_PAST="vuln_past/"
if [ ! -d ${VULN_DIR} ]; then
mkdir -p ${VULN_DIR}${VULN_DIR_CURRE}
mkdir -p ${VULN_DIR}${VULN_DIR_PAST}
fi
Step1-3: コンテナイメージのビルド
コンテナイメージをビルドします。ここでビルドされたコンテナイメージをsyft
でスキャンし、SBOMを生成します。
環境変数 | 説明 |
---|---|
IMAGE_NAME | イメージ名 |
IMAGE_TAG | イメージのタグ名 |
TARGET | syftでスキャンするコンテナイメージ |
#Docker build
IMAGE_NAME=poetry
IMAGE_TAG="sbom"_$BUILD_ID
TARGET=$IMAGE_NAME:$IMAGE_TAG
docker build -t ${TARGET} .
Step1-4: SBOM生成
syftを使い、SBOMを生成します。ここでは、syftによるSBOM生成をsbom_create()
という関数にしています。
#syftでSBOMを生成する
sbom_create() {
local _target=$1
local _sbom_curre_file=$2
local _sbom_past_file=$3
syft --version
#新しいSBOMをスキャンする前に、前回生成したSBOMはSBOM名_prev.jsonへコピー
if [ -e "${_sbom_curre_file}" ]; then
cp "${_sbom_curre_file}" "${_sbom_past_file}"
fi
syft image:"${_target}" -o cyclonedx-json="${_sbom_curre_file}"
}
echo "---> start syft scan"
sbom_create ${TARGET} ${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json ${SBOM_DIR}${SBOM_DIR_PAST}${SBOM_FILE}_prev.json
#スキャン後、イメージは削除
docker image rm ${TARGET}
前回のSBOMが存在する場合、新しいSBOMをスキャンする前に、前回生成したSBOMをSBOM名_prev.jsonというファイルへコピーしています。
上のスクリプトにおいて、syftの各オプション、引数は次の意味があります。
-
image:"${_target}"
- コンテナイメージに対して、スキャンを実施。
:
の後にSBOM生成対象となるコンテナイメージを指定する
- コンテナイメージに対して、スキャンを実施。
-
-o cyclonedx-json="${_sbom_curre_file}"
- SBOMファイルの出力をcyclonedx形式かつjsonフォーマットで出力する
Script4 : 脆弱性検知、結果通知用スクリプト
Step1: 環境変数設定
DAY=`date +%Y%m%d%H%M`
BRANCH_NAME=`echo $GIT_BRANCH | cut -d '/' -f 2`
REPOSITORY_NAME='Jenkins_pipeline_test'
#sbomディレクトリを作成し、現在のタグに対してsbom_${TAG_NAME}.jsonを生成
#sbomディレクトリ
SBOM_DIR=sbom/
SBOM_DIR_CURRE=sbom_curre/
SBOM_DIR_PAST=sbom_past/
SBOM_FILE="sbom_${REPOSITORY_NAME}_${BRANCH_NAME}"
SBOM_DIFF_DIR=sbom_diff/
SBOM_DIFF_FILE1=sbom_diff_${REPOSITORY_NAME}_New_${BRANCH_NAME}_Prev_${BRANCH_NAME}
#SBOM_DIFF_FILE2=sbom_diff_${REPOSITORY_NAME}_New_${BRANCH_NAME}_${TAG_NAME}
生成したファイルに日付情報を付与し、保管する
#日付処理
if [ -e ${SBOM_DIFF_DIR}${SBOM_DIFF_FILE1}.txt ]; then
cp ${SBOM_DIFF_DIR}${SBOM_DIFF_FILE1}.txt ${SBOM_DIFF_DIR}${SBOM_DIFF_FILE1}_${DAY}.txt
echo "END"
fi
cp ${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json ${SBOM_DIR}${SBOM_DIR_PAST}${SBOM_FILE}_${DAY}.json
脆弱性検知結果を管理するためのディレクトリ作成
環境変数 | 説明 |
---|---|
VULN_DIR | 脆弱性検知結果を保管するためのディレクトリ |
GRYPE_FILE | 脆弱性検知結果のファイル名 |
VULN_DIR_CURRE | 最新の脆弱性検知結果を保管するためのディレクトリ |
VULN_DIR_PAST | 過去の脆弱性検知結果を保管するためのディレクトリ |
#脆弱性検知
#vulnディレクトリを作成し、現在のタグに対してvuln_table_${BRANCH_NAME}.txtを生成
VULN_DIR="vuln/"
GRYPE_FILE="vuln_table_${REPOSITORY_NAME}_${BRANCH_NAME}"
VULN_DIR_CURRE="vuln_curre/"
VULN_DIR_PAST="vuln_past/"
if [ ! -d ${VULN_DIR} ]; then
mkdir -p ${VULN_DIR}${VULN_DIR_CURRE}
mkdir -p ${VULN_DIR}${VULN_DIR_PAST}
fi
Step: 脆弱性検知用関数の作成
#grypeで最新developブランチのSBOMをスキャンする
grype_vuln_scan() {
local _sbom_target=$1
local _results_file=$2
#grype db status
#export GRYPE_DB_AUTO_UPDATE=false
#export GRYPE_CHECK_FOR_APP_UPDATE=false
grype sbom:./"${_sbom_target}" -o table="${_results_file}".txt
grype sbom:./"${_sbom_target}" -o json="${_results_file}".json
}
以下の環境変数の設定は、grypeがSBOMをスキャンする前に、データベースをアップデートされる操作をオフにする設定です。
export GRYPE_DB_AUTO_UPDATE=false
export GRYPE_CHECK_FOR_APP_UPDATE=false
grypeコマンドのオプションにはそれぞれ次のような意味があります
-
sbom:./${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json
SBOMのスキャンを行う -
-o table=${SCAN_DIR}${SCAN_DIR_CURRE}${GRYPE_FILE}.txt
出力形式を指定。今回はtxtファイルでテーブルを出力する - jsonで出力したい場合は
-o json=${SCAN_DIR}${SCAN_DIR_CURRE}${GRYPE_FILE}.json
とする
Step: 脆弱性検知結果をトリアージする
grypeで出力した脆弱性結果から、
- 脆弱性レベルがHigh、Criticalのもの
- アタックベクトルがネットワーク経由で攻撃可能なもの
AV:N
を抽出します。 - 次に、fixedされているか、いないかで場合分けを行っています。
detect_high_critical_vulnerabilities_fixed() {
local _fixed_state="$1"
cd "${_fixed_state}"
grep -E 'AV:N|AV:A' vuln_"${_fixed_state}"_tmp.csv | grep -E 'High|Critical' > vuln_"${_fixed_state}".csv || true
# 直前のコマンドの終了コードを取得
exit_code=$?
echo ${exit_code}
# 終了コードが0以外(エラーが発生した)場合はスキップする
if [ $exit_code -ne 0 ]; then
echo "${_fixed_state}" ": High or Critical are not found. Skipping the execution of the shell script."
#exit $exit_code
else
#cp vulntmp.csv vulntmp1.csv
rm vuln_"${_fixed_state}"_tmp.csv
awk -F '","' '!seen[$1 $3]++' vuln_"${_fixed_state}".csv > vuln_"${_fixed_state}"_tmp.csv
#sed -i '1s/^/Package name,version, purl, fixed-in, VULNERABILITY, SEVERITY, CVSS1, CVSS2, CVSS3, CVSS4\n/' vulntmp.csv
awk -F '","' '{printf("| %-15s | %-12s | %-13s | %-12s | %s |\n", $1, $2, $3, $4, $5)}' vuln_"${_fixed_state}"_tmp.csv > marktable_"${_fixed_state}".txt
sed -i '1s/^/|パッケージ名|バージョン|修正バージョン|VULNERABILITY|SEVERITY|\n/' marktable_"${_fixed_state}".txt
sed -i '2 i\|-----------------|--------------|--------------|----------------------|----------|' marktable_"${_fixed_state}".txt
sed 's/^\|"//g; s/"$//g' marktable_"${_fixed_state}".txt > marktable_"${_fixed_state}"0.txt
VULNERABILITY_DATA=\`\`\`$(cat marktable_"${_fixed_state}"0.txt)\`\`\`
rm marktable_"${_fixed_state}".txt marktable_"${_fixed_state}"0.txt
fi
cd ..
}
detect_high_critical_vulnerabilities_other() {
local _fixed_state="$1"
cd "${_fixed_state}"
grep -E 'AV:N|AV:A' vuln_"${_fixed_state}"_tmp.csv | grep -E 'High|Critical' > vuln_"${_fixed_state}".csv || true
# 直前のコマンドの終了コードを取得
exit_code=$?
echo ${exit_code}
# 終了コードが0以外(エラーが発生した)場合はスキップする
if [ $exit_code -ne 0 ]; then
echo "${_fixed_state}" ": High or Critical are not found. Skipping the execution of the shell script."
#exit $exit_code
else
#cp vulntmp.csv vulntmp1.csv
rm vuln_"${_fixed_state}"_tmp.csv
awk -F '","' '!seen[$1 $3]++' vuln_"${_fixed_state}".csv > vuln_"${_fixed_state}"_tmp.csv
#sed -i '1s/^/Package name,version, purl, fixed-in, VULNERABILITY, SEVERITY, CVSS1, CVSS2, CVSS3, CVSS4\n/' vulntmp.csv
awk -F '","' '{printf("| %-15s | %-12s | %-13s | %-12s |\n", $1, $2, $3, $4)}' vuln_"${_fixed_state}"_tmp.csv > marktable_"${_fixed_state}".txt
sed -i '1s/^/|パッケージ名|バージョン|VULNERABILITY|SEVERITY|\n/' marktable_"${_fixed_state}".txt
sed -i '2 i\|-----------------|--------------|--------------|----------|' marktable_"${_fixed_state}".txt
sed 's/^\|"//g; s/"$//g' marktable_"${_fixed_state}".txt > marktable_"${_fixed_state}"0.txt
VULNERABILITY_DATA=\`\`\`$(cat marktable_"${_fixed_state}"0.txt)\`\`\`
rm marktable_"${_fixed_state}".txt marktable_"${_fixed_state}"0.txt
fi
cd ..
}
echo "---> grype vulnerability-scan"
grype_vuln_scan ${SBOM_DIR}${SBOM_DIR_CURRE}${SBOM_FILE}.json ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}
CRITICAL_NUM=`grep -o "Critical" ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt | wc -l`
HIGH_NUM=`grep -o "High" ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt | wc -l`
DIRECTORY_NAME=${GRYPE_FILE}/
if [ ! -d "${DIRECTORY_NAME}" ]; then
mkdir -p ${DIRECTORY_NAME}
fi
if [ -e "${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.json" ]; then
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt ${DIRECTORY_NAME}
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.json ${DIRECTORY_NAME}
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt ${VULN_DIR}${VULN_DIR_PAST}${GRYPE_FILE}_${DAY}.txt
cp ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.json ${VULN_DIR}${VULN_DIR_PAST}${GRYPE_FILE}_${DAY}.json
fi
cd $DIRECTORY_NAME
# make fixed directory to store fixed vulnerabilities.
if [ ! -d "fixed" ]; then
mkdir -p fixed
fi
# FIXED
if cat ${GRYPE_FILE}.json | jq -r '.matches[] | .vulnerability.fix.state | contains("fixed")' | grep true >/dev/null; then
cat ${GRYPE_FILE}.json | jq -r '
.matches[]
| select(.vulnerability.fix.state == "fixed")
| [
.artifact.name,
.artifact.version,
.vulnerability.fix.versions[],
if (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss = null) and (.relatedVulnerabilities[0].id as $CVE | .vulnerability.id == $CVE)
then
.vulnerability.id,
.vulnerability.severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
elif (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss != null)
then
.vulnerability.id,
.vulnerability.severity,
(.vulnerability.cvss | last |.version),
(.vulnerability.cvss | last |.vector)
else
.relatedVulnerabilities[0].id,
.relatedVulnerabilities[0].severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
end
] | @csv' > fixed/vuln_fixed_tmp.csv
detect_high_critical_vulnerabilities_fixed fixed
VULNERABILITY_DATA_FIXED=${VULNERABILITY_DATA}
else
# .vulnerability.fix.stateが"fixed"を含まない場合、メッセージを表示
echo "The 'fixed' string is not found. Skipping the execution of the shell script."
fi
#NON-FIXED
if [ ! -d "not-fixed" ]; then
mkdir -p not-fixed
fi
if cat ${GRYPE_FILE}.json | jq -r '.matches[] | .vulnerability.fix.state | contains("not-fixed")' | grep true >/dev/null; then
cat ${GRYPE_FILE}.json | jq -r '
.matches[]
| select(.vulnerability.fix.state == "not-fixed")
| [
.artifact.name,
.artifact.version,
if (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss = null) and (.relatedVulnerabilities[0].id as $CVE | .vulnerability.id == $CVE)
then
.vulnerability.id,
.vulnerability.severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
elif (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss != null)
then
.vulnerability.id,
.vulnerability.severity,
(.vulnerability.cvss | last |.version),
(.vulnerability.cvss | last |.vector)
else
.relatedVulnerabilities[0].id,
.relatedVulnerabilities[0].severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
end
] | @csv' > not-fixed/vuln_not-fixed_tmp.csv
detect_high_critical_vulnerabilities_other not-fixed
VULNERABILITY_DATA_NOTFIXED=${VULNERABILITY_DATA}
else
# .vulnerability.fix.stateが"not-fixed"を含まない場合、メッセージを表示
echo "The 'not-fixed' string is not found. Skipping the execution of the shell script."
fi
# UNKNOWN
if [ ! -d "unknown" ]; then
mkdir -p unknown
fi
if cat ${GRYPE_FILE}.json | jq -r '.matches[] | .vulnerability.fix.state | contains("unknown") // contains("wont-fix")' | grep true >/dev/null; then
cat ${GRYPE_FILE}.json | jq -r '
.matches[]
| select(.vulnerability.fix.state == "unknown" or .vulnerability.fix.state == "wont-fix")
| [
.artifact.name,
.artifact.version,
if (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss = null) and (.relatedVulnerabilities[0].id as $CVE | .vulnerability.id == $CVE)
then
.vulnerability.id,
.vulnerability.severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
elif (.vulnerability.id | contains("CVE-")) and (.vulnerability.cvss != null)
then
.vulnerability.id,
.vulnerability.severity,
(.vulnerability.cvss | last |.version),
(.vulnerability.cvss | last |.vector)
else
.relatedVulnerabilities[0].id,
.relatedVulnerabilities[0].severity,
(.relatedVulnerabilities[0].cvss | last | .version),
(.relatedVulnerabilities[0].cvss | last | .vector)
end
] | @csv' > unknown/vuln_unknown_tmp.csv
detect_high_critical_vulnerabilities_other unknown
VULNERABILITY_DATA_UNKOWN=${VULNERABILITY_DATA}
else
# .vulnerability.fix.stateが"unknown"を含まない場合、メッセージを表示
echo "The 'unknown' string is not found. Skipping the execution of the shell script."
fi
cd ..
Teams通知メッセージの作成
上でトリアージし、修正されているか、いないかで場合分けした脆弱性を通知するためのメッセージを作成します。今回は、Slackへ通知していますが、Teamsへ通知することも可能です。
LIB=`awk 'BEGIN {
# テーブルヘッダーを出力
print "| NAME | INSTALLED-VERSION |"
print "|------|-----------|"
}
NR>1 {
# NAME と INSTALLED 列だけを出力
printf("| %s | %s |\n", $1, $2)
}' ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt | uniq`
JSON_INFO=`cat << EOF
{
"text": "レベルHigh, Criticalの脆弱性が見つかりました。",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "レベルHigh, Criticalの脆弱性が見つかりました。:"
}
},
{
"type": "section",
"block_id": "section789",
"fields": [
{
"type": "mrkdwn",
"text": "*日付*: ${DAY}"
},
{
"type": "mrkdwn",
"text": "*branch*: ${BRANCH_NAME}"
},
{
"type": "mrkdwn",
"text": "*JOB name*: ${JOB_NAME} #${BUILD_ID}"
},
{
"type": "mrkdwn",
"text": "*JOBのURL*: ${BUILD_URL}"
},
{
"type": "mrkdwn",
"text": "*CRITICALの件数*: ${CRITICAL_NUM}"
},
{
"type": "mrkdwn",
"text": "*HIGHの件数*: ${HIGH_NUM}"
},
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "FIXED-INに記されたバージョンへアップデートをお願いします"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${VULNERABILITY_DATA_FIXED}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "次の脆弱性については、修正バージョンが公開されていません。"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${VULNERABILITY_DATA_NOTFIXED}"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "次の脆弱性については、脆弱性対策が知られていません。"
},
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${VULNERABILITY_DATA_UNKOWN}"
}
}
]
}
EOF`
#プロキシを設定する場合は、プロキシを環境変数で設定
#export HTTP_PROXY=
#export HTTPS_PROXY=
#Web hook用URLの設定
WEBHOOK_URL=https://hooks.slack.com/services/
if grep -q "Critical" ${DIRECTORY_NAME}${GRYPE_FILE}.json || grep -q "High" ${DIRECTORY_NAME}${GRYPE_FILE}.json; then
echo "alerm"
curl -X POST -H 'Content-type: application/json' -d "${JSON_INFO}" "${WEBHOOK_URL}"
#curl -H "Content-Type: application/json" -d "${JSON_INFO}" "${WEBHOOK_URL}"
fi
Slackのwebhookの設定
Slackのグループ、チャンネルを作成してください。今回、私はjenkinsの通知テスト
というチャンネルを作成しました。
以下のURLに行き、Webhook用のURLを作成・コピーしてください。
https://slack.com/services/new/incoming-webhook
Slackへの通知結果
Jenkinsのジョブを実行し、Finished: SUCCESS
と出ていれば、スクリプトの実行は成功し、Slackへ通知メッセージが送信されているはずです。
通知メッセージは以下のように送信されます。1段目にJenkinsのジョブや、脆弱数などの基本情報を、その後に、検知された脆弱性の情報を載せています。
Teamsへ通知を行う場合
本記事では、Slackへ通知を行いましたが、脆弱性検知結果をTeamsへ通知することもできます。ここでは、Teamsへの通知方法についても解説します。
まず、以下のURL記載の方法でteamsのwebhookを作成します。
https://learn.microsoft.com/ja-jp/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook?tabs=newteams%2Cdotnet
webhookのURLを変数WEBHOOK_URL
に代入し、以下のjsonメッセージをURLへ渡せば、Teamsへ通知できます。
Teamsの方がMarkdownテーブルが使えるので、きれいに通知できます。
{
"schema": "https://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.0",
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "d70048",
"summary": "Grype脆弱性情報通知",
"title": "Grype脆弱性情報通知",
"sections": [
{
"title": "Details",
"markdown": true,
"text": "レベルHigh, Criticalの脆弱性が見つかりました。以下が対象のファイルです: \r - **SBOMリスト : ${SBOM_LIST_DIR}${SBOM_LIST_DIR_CURRE}${SBOM_FILE_ARRANGED}.txt** \r - **vuln結果 : ${VULN_DIR}${VULN_DIR_CURRE}${GRYPE_FILE}.txt**",
"facts": [
{
"name": "日付",
"value": "${DAY}"
},
{
"name": "ブランチ",
"value": "main"
},
{
"name": "job名",
"value": "${JOB_NAME} #${BUILD_ID}"
},
{
"name": "CRITICALの件数",
"value": "${CRITICAL_NUM}"
},
{
"name": "HIGHの件数",
"value": "${HIGH_NUM}"
},
],
},
{
"markdown": true ,
"text": "以下のライブラリに対処が必要な脆弱性が検知されました。ご確認ください。",
},
{
"markdown": true,
"text": "${LIB}"
},
{
"markdown": true ,
"text": "FIXED-INに記されたバージョンへアップデートをお願いします"
},
{
"markdown": true,
"text": "${VULNERABILITY_DATA_FIXED}"
},
{
"markdown": true ,
"text": "次の脆弱性については、修正バージョンが公開されていません。"
},
{
"markdown": true,
"text": "${VULNERABILITY_DATA_NOTFIXED}"
},
{
"markdown": true ,
"text": "次の脆弱性については、脆弱性対策が知られていません。"
},
{
"markdown": true,
"text": "${VULNERABILITY_DATA_UNKOWN}"
},
{
"markdown": true ,
"text": "以下からジョブを確認できます"
}
],
"potentialAction": [
{
"@type": "OpenUri",
"name": "Jenkinsでジョブ結果を確認する",
"targets": [
{
"os": "default",
"uri": "${BUILD_URL}"
}
]
}
]
}