株式会社ネットワールドのエンジニアがお届けする技術情報ブログです。
各製品のエキスパートたちが旬なトピックをご紹介します。

Kaspersky製品ナレッジ 第43回 ~Jenkinsを利用したKESLによるDockerコンテナスキャンジョブの実装~


皆様、こんにちは。カスペルスキー製品担当SEの小池です。

第42回のブログにて、Kaspersky Endpoint Security for Linux のDockerコンテナスキャン機能におけるオンデマンドスキャンについて記載しました。
blogs.networld.co.jp
今回は第42回からの派生で、CI/CDツールのJenkinsからKESLのオンデマンドスキャンを実行する方法について紹介・検証します。
筆者はJenkinsについては素人なので至らない点があったらすみません!

f:id:networld-blog-post:20210824144624p:plain

今回の内容は以下の通りです。


今回の記事は以下のバージョンにて検証し、画面ショットを取得しております。
●管理サーバー
 OS:Windows Server 2019
 DB:Microsof SQL Server 2017 Express
 Kaspersky Security Center:13.0.0.111247
 Kaspersky Security Center Web Console:13.0.10285
 Kaspersky Endpoint Security 11.1.0 for Linux のプラグイン:11.2.0.136
●Docker 兼 Jenkins環境※ 兼 KESL環境
 OS:CentOS 7
 Jenkins:2.289.3 (インストール時に推奨されるプラグインのみ利用。)
 Kaspersky Endpoint Security for Linux 11.2.0.4528
●利用ライセンス
 Kaspersky Hybrid Cloud Security Enterprise, CPU Japanese Edition.
※…Masterノードのみ。

1. KESLとJenkinsの統合の概要

まず、KESLにはkesl-controlという製品管理コマンドがあります。このkesl-controlを使うことで、JenkinsのジョブからKESLのコンテナスキャンタスクを生成+実行することが可能です。
このkesl-controlを使ったコンテナスキャンは、第42回で紹介したKESLのDockerコンテナとイメージのオンデマンドスキャンタスクをローカルレベルで生成+実行しているので、KESLにはKHCS Enterpriseライセンスが必須です。(2021/8/23時点。)
KESLのライセンスがKHCS Enterpriseであれば、KSCで一元管理していても、スタンドアロン環境であっても、Jenkinsからのkesl-controlコマンドによるコンテナスキャンは可能です。ただし、KSCで一元管理してる場合はポリシーで1か所変更が必要になります。これについては後述の章をご参照ください。
Jenkins側のジョブ設定はいたってシンプルで、ビルド>ジョブの実行でこのkesl-controlコマンドを実行するだけです。ぶっちゃけ難しいことなど何もしておりませぬ。
なお、今回のKESLとJenkinsの統合については、以下のKESLオンラインヘルプを参考にしております。
https://support.kaspersky.com/KES4Linux/11.2.0/ja-JP/198031.htm

2. オンラインヘルプの補足

先述致しました通り、KESLとJenkinsの統合についてはオンラインヘルプに記載があります。
https://support.kaspersky.com/KES4Linux/11.2.0/ja-JP/198031.htm
ここではこのオンラインヘルプの内容 (2021/8/23時点の日本語版) について補足します。

まず、このオンラインヘルプの実装例は、基本的にDockerとKESLとJankinesが同居していることが前提の例です
よってこの記事の検証環境でもDockeとKESLとJenkinsは同居させております。

次にこのオンラインヘルプには3つのシェルスクリプトのサンプルが掲載されています。
まず1つ目はこちら。
このサンプルはコードを読んでいただくとわかる通り、指定したイメージ (既存か否かは問わず) を docker run -d で起動するだけの内容です。
4行目の docker run -d コマンドの引数の内容は完全にサンプルなので、ここは変更する必要があります。
9行目で、起動したコンテナIDを最初に宣言した一時ファイルに書き出しています。この一時ファイルは後続の2つ目のサンプルコードで利用しますので、1つ目と2つ目のサンプルコードは2つで1つと考えてよいかと存じます。
f:id:networld-blog-post:20210824150022p:plain

2つ目はこちら。
前述した通り、このサンプルは1つ目のサンプルコードの続きです。1つ目のサンプルで生成された一時ファイルと起動済のコンテナが存在することが前提となっています。
最初~11行目で1つ目のコードで作成した一時ファイルの存在確認と、一時ファイルからコンテナIDの取得のチェックをしています。
13行目で kesl-controlで実行中のコンテナのスキャンを実施しています。この時 "THREATS_AMOUNT" に入る値は、コンテナスキャンタスクで検知した脅威の数です。
で、"THREATS_AMOUNT" (検知した脅威の数) が0か否かをチェックしてから、コンテナを停止+削除して終了という内容になっています。
f:id:networld-blog-post:20210824150127p:plain
2つ目のサンプルコードを利用する場合は、KESLのOSのロケールにご注意ください。
13行目のパイプで grep 'Total detected objects' と処理しているのですが、この 'Total detected objects' が日本語のロケールの場合 "検知されたオブジェクトの総数" となってしまいますので (下図参照)、grepでひっかけることができなくなります。ご注意ください。
f:id:networld-blog-post:20210824150204p:plain

最後、3つ目のサンプルはこちら。
こちらは前に紹介した1つ目と2つ目のサンプルの続きではなく、完全に独立した内容です。
概要としては、パブリックに公開されているDockerfileの内容を一時ファイルに落とし込み、そこからイメージのビルドをして、そのイメージをスキャンする内容となっています。
最初~12行目がDockerfileの取得・Dockerfileの作成・ビルド処理ですが、この辺りは自社のCI/CDの流れに併せて変更する必要があります。
14行目でイメージスキャンをしています。ローカルに既に存在するイメージのスキャンならいきなり14行目からの処理でもOKです。
また、このコマンドを見ていただくとわかるのですが、コンテナのスキャンもイメージのスキャンも、コマンドは kesl-control --scan-container で共通です。
f:id:networld-blog-post:20210824150349p:plain

3. KSCの管理下にあるKESLの前準備

先述致しました通り、今回ご紹介するJenkinsとの統合については、KESLがKSCで一元管理されていてもスタンドアロン環境でも利用できます。
ただし、KSCで一元管理されている場合、ポリシーで以下の箇所を変更する必要があります
デフォルトのポリシーのままだと、kesl-controlコマンドでローカルタスクを作成する権限がなく、コンテナスキャンができません。
[デバイス]>[ポリシーとプロファイル]を開き、KESLのポリシー名をクリックします。
f:id:networld-blog-post:20210824150514p:plain
[アプリケーション設定]タブ>[ローカルタスク]>[タスク管理]をクリックします。
f:id:networld-blog-post:20210824150528p:plain
[ローカルタスクの表示と管理をユーザーに許可する] にチェックを入れ、強制適用をONにして、[OK]をクリックします。
f:id:networld-blog-post:20210824150540p:plain
これで、KSCで一元管理しているKESLでもkesl-control --scan-containerコマンドを打てるようになります。

4. 検証 (指定したイメージからコンテナを起動してスキャン)

まずは指定したイメージからコンテナを起動し、そのコンテナをスキャンし、最終的にコンテナを停止・削除するジョブを作成してみました。
参考にしたのは、オンラインヘルプに載っている1つ目のサンプルコードと2つ目のサンプルコードです。サンプルコードはこちら

ジョブ>ビルド>シェルの実行を2個入れました。1個目がスキャン対象のコンテナ実行と一時ファイル作成、2個目がスキャン実行とコンテナ終了+削除となります。
(ブログの記事として書くにあたり便宜上2つにシェルスクリプトを分けていますが、1つのシェルスクリプトで記載可能な内容です。)

↓1個目のシェルスクリプト。スキャン対象のコンテナ実行と一時ファイル作成。

echo '****************STEP01 START****************'
TMP_FILE="/tmp/kesl_cs_info"
EXIT_CODE=0
echo "Start container from image: '${TEST_CONTAINER_IMAGE}'"
CONTAINER_ID=$(docker run -d --name ${CONTAINER_NAME} ${TEST_CONTAINER_IMAGE})

if [ -z "${CONTAINER_ID}" ] ; then
	echo "Cannot start container from image ${TEST_CONTAINER_IMAGE}"
    echo '****************STEP01 FINISH (Error01)****************'
	exit 1
fi

echo "${CONTAINER_ID}" > ${TMP_FILE}
echo '****************STEP01 FINISH****************'
exit ${EXIT_CODE}

f:id:networld-blog-post:20210824151037p:plain

↓2個目のシェルスクリプト。スキャン実行とコンテナ終了+削除。

echo '****************STEP02 START****************'
TMP_FILE="/tmp/kesl_cs_info"
EXIT_CODE=0

if [ ! -f "${TMP_FILE}" ] ; then
	echo "Cannot find temporary file with container ID: '${TMP_FILE}'"
	exit 1
    echo '****************STEP02 FINISH (Error01)****************'
fi

CONTAINER_ID=$(cat ${TMP_FILE})

if [ -z "${CONTAINER_ID}" ] ; then
	echo "Cannot find container ID in the temporary file: '${TMP_FILE}'"
	exit 1
    echo '****************STEP02 FINISH (Error02)****************'
fi

echo "Start anti-virus scan for: '${CONTAINER_ID}'"
THREATS_AMOUNT=$(kesl-control --scan-container ${CONTAINER_ID}|grep 'Total detected objects'|awk '{print $5}')

if [ "${THREATS_AMOUNT}" != "0" ] ; then
	echo "ATTENTION! ${THREATS_AMOUNT} threats detected at: '${CONTAINER_ID}'"
	EXIT_CODE=1
else
	echo "Not threats found"
fi

echo "Remove container: {${CONTAINER_ID}}"
docker stop ${CONTAINER_ID}
docker rm -f ${CONTAINER_ID}
rm -f ${TMP_FILE}
echo '****************STEP02 FINISH****************'
exit ${EXIT_CODE}

f:id:networld-blog-post:20210824151158p:plain

パラメーターは文字列で以下2つです。
TEST_CONTAINER_IMAGE:スキャンするコンテナのイメージ名。タグ指定可。
CONTAINER_NAME:スキャン用に起動するコンテナ名。
f:id:networld-blog-post:20210824151238p:plain

ローカルリポジトリのイメージは以下の通りです。
f:id:networld-blog-post:20210824151325p:plain
せっかくなので、ローカルに無いイメージ "hello-world:latest" で試行します。
f:id:networld-blog-post:20210824151355p:plain
正常終了したのでコンソール出力を確認してみます。
1個目のシェルスクリプトでDockerイメージのPull、パラメーターで指定した名前でコンテナ起動してID取得、コンテナIDを一時ファイルに出力まで実行しています。
f:id:networld-blog-post:20210824151444p:plain
2個目のシェルスクリプトでは、一時ファイルからコンテナID取得、スキャンの実行、検知したオブジェクト数のチェック、コンテナの停止と削除を実施しています。
今回の場合、検知した脅威数は0個でした。
f:id:networld-blog-post:20210824151500p:plain
終了した時点のDockerホストの状況です。
イメージにはスキャンで指定したhello-world:latestが出来ています。
コンテナは2個目のシェルスクリプトで停止+削除をしたので、スキャン用に起動したコンテナ "Kaspersky_Blog42_01" は完全に削除されています。
f:id:networld-blog-post:20210824151518p:plain

5. 検証 (既存のイメージのスキャン)

この検証ではローカルリポジトリにあるイメージを指定し、そのままスキャンして結果を出力する内容で検証してみました。
参考にしたのは、オンラインヘルプに載っている3つ目のサンプルコードです。
オンラインヘルプはこちらです。

ジョブ>ビルド>シェルの実行で、以下のシェルスクリプトを設定しました。
↓指定したイメージがローカルに存在するかチェック、スキャンと結果出力して終了。

echo '****************Image Scan Start****************'
EXISTENCE_CHECK=$(docker images ${IMAGE_NAME_TAG} | wc -l)
echo ${EXISTENCE_CHECK}

if [ "${EXISTENCE_CHECK}" = "1" ] ; then
    echo "There is no matching container image: ${IMAGE_NAME_TAG}"
    echo '****************Image Scan Finish (Error01)****************'
    exit 1
fi

echo "Scan docker image: ${IMAGE_NAME_TAG}"
SCAN_RESULT=$(/opt/kaspersky/kesl/bin/kesl-control --scan-container ${IMAGE_NAME_TAG})
echo "Scan done: ${IMAGE_NAME_TAG}"
echo '****************Image Scan Finish****************'

f:id:networld-blog-post:20210824151927p:plain

パラメーターは文字列で "IMAGE_NAME_TAG" のみです。こちらはスキャンするイメージ名とタグ指定をしています
f:id:networld-blog-post:20210824151938p:plain

検証前の時点でローカルにあるイメージは以下の通りです。
f:id:networld-blog-post:20210824152030p:plain
既存のイメージのうち wordpress:php7.3 をスキャンするように指定して実行してみます。
f:id:networld-blog-post:20210824152039p:plain
正常終了したのでコンソール出力を見てみます。
このシェルスクリプトでは結果出力のみで、結果による処理分岐はありません。
今回の検証のスキャン結果は wordpress:php7.3 のイメージには脅威は0個、スキャンエラー1個でした。
(スキップ1個と表示されていますが、これは "スキャンエラーが起きたからスキップした" の意ですので、スキャンエラーを出したオブジェクトとスキップしたオブジェクトは同じオブジェクトです。)
f:id:networld-blog-post:20210824152059p:plain

スキャンエラーを出したオブジェクトについてはイベントログから確認できます。
kesl-controlコマンドで調べる場合は、タスクタイプ "ContainerScan" で直近10個程度を表示すればわかると思います。

$ kesl-control -E --query "TaskType == 'ContainerScan'" -n 10
f:id:networld-blog-post:20210824152206p:plain
スキップしたオブジェクトはこれです。
Read Errorであることと、ファイル名を見る限り、多重アーカイブ (このファイルだと四重) が原因でスキップしたものと思われます。
f:id:networld-blog-post:20210824152220p:plain
KSCで一元管理している場合は、[監視とレポート]>[イベントの抽出]>[最近のイベント]で確認できます。
f:id:networld-blog-post:20210824152232p:plain

6. 総括

結論としてKESLの kesl-control --scan-container コマンドを使ってJenkinsからコンテナスキャン および イメージスキャンすることは、かなり簡単に実装できました
原始的なやり方にはなりますが、kesl-control --scan-container の戻り値を自動判定して、通知を出したり、後続処理を変更することも十分可能でしょう。
ただし、kesl-control --scan-container で何か脅威を検知した際や、スキップしたオブジェクトがある場合、そのオブジェクトを特定するには kesl-control -E かKSCでイベントを確認しないといけません。
KESLによるスキャンをJenkinsで実装する場合は、スキャン自体だけでなく問題のあったオブジェクトを特定するロジックまでをジョブに入れ込んだ方がよいだろうと感じました



今回は第42回からの派生で、CI/CDツールのJenkinsからKESLのオンデマンドスキャンを実行する方法について紹介・検証しました。
筆者はJenkinsの利用経験が全くなかったので、オンラインヘルプに書いてあることを理解して検証するのは苦労しました…。
この記事ではJenkinsからCLIでKESLのコンテナスキャンを実行させていますが、もちろん同じコマンドをKESLのローカルでポチポチ実行することも可能です。
Jenkinsだけでなく、ほかのCI/CDツールからKESLのコンテナスキャンを実行する際の参考になれば幸いにございます。

この度は最後まで記事をご覧いただき誠にありがとうございました。
記載事項へのご指摘、ご不明点、ご質問等ございましたら、以下からご連絡いただければと存じます。
https://www.networld.co.jp/product/kaspersky/

それでは次回の記事でお会いしましょう!