Zabbix技術者ブログ

HOME >

Zabbixでインストール済みアプリケーション一覧を取得する

はじめに

Zabbixが扱えるのは、CPU使用率やログといった、いわゆる監視データだけではありません。 エージェントが値を返せるものであれば、たいていのものは収集の対象にできます。
そこで今回は、監視対象にインストール済みのアプリケーションを取得し、一覧表示する検証を行いました。導入が必須のアプリケーションが入っているか、逆に不要なアプリケーションが入っていないか。そうした棚卸しに使えそうです。

項目検証環境
ZabbixZabbix 7.0.29
監視対象Windows 11 Pro(Entra ID参加)
エージェントZabbixエージェント 7.0.27
取得方式UserParameterでスクリプトを実行

目指したのは次の2つです。

  • ・特定のホストにインストールされているアプリケーションの一覧を表示する
  • ・アプリケーション名で検索し、該当するホストの一覧を表示する

アプリ一覧をどうやって取得するか

Windowsでインストール済みのアプリを列挙する方法はいくつかあります。今回は3つの方法を検討し、レジストリのUninstallキーを直接読む方式を採用しました。

方式概要評価
wmi.getall[] Zabbixエージェント標準キーでWQLを実行し、Win32_Productを列挙する MSIでインストールしたものしか列挙できず取得漏れが出ます。また、Win32_Productへのクエリではインストール済みパッケージの整合性チェックが開始され、処理に時間がかかったり、イベントログに記録されたりする場合があります。
winget list Windows Package Managerでインストール済みパッケージを一覧する 実行ユーザーのコンテキストで見えるアプリのみが対象となり、エージェント経由では取得範囲が限られます。(詳細は次項)
レジストリのUninstallキー HKLMおよびHKEY_USERS配下のUninstallキーを直接読む 外部依存がなく、オフラインで完結します。SYSTEM権限で読み取れます。

WinGetを採用しなかった理由

Windowsでアプリの一覧を取るなら、まずWinGetが思い浮かぶところです。 ただ今回は、次の理由から候補から外しました。
ひとつは、取得できる範囲です。winget list が返すのは、実行しているユーザーのコンテキストで見えるアプリだけです。 今回の環境ではZabbixエージェントがLocalSystemで動作しているため、返ってくるのはSYSTEMから見える範囲に限られます。ユーザー単位でインストールされたアプリは拾えません。
もうひとつは、実行環境です。SYSTEMコンテキストでのWinGet実行は、Microsoftの公式GitHubで機能要望として挙がっている段階です。回避策として実行ファイルを直接呼ぶ例もありますが、今回は採用を見送りました。

全体の構成

今回の構成は「マスターアイテム+依存アイテム+依存ディスカバリルール」というパターンです。マスターアイテムでスクリプトを1回だけ実行し、そのJSONを依存アイテムと依存ディスカバリルールが共有して使う形になります。

要素役割
マスターアイテムエージェント経由でスクリプトを実行し、アプリ一覧をJSONで受け取ります。データの取得はこのアイテムだけで行います。
依存アイテムマスターアイテムが取得したJSONから、スクリプトが一時的に読み込んだレジストリが残っていないかを示す値を取り出します。
依存ディスカバリルールマスターアイテムが取得したJSONからアプリの一覧を取り出し、アプリを1つずつ検出します。
アイテムプロトタイプ依存ディスカバリルールで検出したアプリごとに、アイテムを自動作成するための設定です。アイテム名にはアプリ名とバージョンが、値にはインストール日が入ります。

いずれもマスターアイテムの値を使うため、スクリプトの実行は1回で済みます。依存アイテムはマスターアイテムが更新されたときだけ評価されるため、余計な実行やタイミングのずれも発生しません。

また、スクリプトは読むべきレジストリキーが異なるため、マシン全体用とユーザー単位用の2つに分けています。

区分読み取り対象キー備考
マシン全体 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKLM\SOFTWARE\Wow6432Node\...\Uninstall
SYSTEM権限でそのまま読めます
ユーザー単位 HKEY_USERS\<SID>\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall
HKEY_USERS\<SID>\SOFTWARE\Wow6432Node\...\Uninstall
全ユーザー分をSID単位で走査します

ここで注意したいのが、スクリプトがZabbixエージェントのサービスの権限で実行される点です。
今回の環境では、エージェントのサービスはLocalSystemアカウント(ローカルシステムアカウント)で動作していました。 Microsoftのドキュメントによると、LocalSystemアカウントで動作するサービスでは、HKEY_CURRENT_USERは現在のユーザーではなく既定のユーザーに関連付けられます。 そのため、スクリプトからHKCUを参照しても、ログオン中のユーザーの情報は読めません。 そこで今回は、HKEY_USERS配下を走査して、ユーザーごとの情報を読み取る方法にしました。



実際にやってみた

ここからは、ユーザー単位のアプリ一覧を取得する側を例に設定手順をご紹介します。

ステップ1:アプリ一覧を取得するスクリプトを用意

まずは、Zabbixにアプリ一覧をJSONで渡せるようにします。
ユーザー単位を取得する側の処理は、次の流れになります。

  1. ①ProfileListから、このPCにある全プロファイル(SIDとプロファイルのパス)を取得する
  2. ②HKEY_USERS配下にすでに読み込まれているSIDを確認する
  3. ③まだ読み込まれていないユーザーの分だけ、reg loadでNTUSER.DATを一時マウントする
  4. ④各SID配下のUninstallキーを読む
  5. ⑤自分でマウントしたものだけをreg unloadで戻す

すでにHKEY_USERSへ読み込まれているユーザーのハイブを重ねてロードしないよう、②で区別しています。 また、未ログオンのユーザーまで対象にしているのは、共用PCや、 退職者のプロファイルが残っている端末を想定したためです。

スクリプトの中身は本記事では割愛しますが、実行すると次のようなJSONが返ってきます。
以下は実際の出力から説明に必要な項目を抜粋し、コメントを追記したものです。

{
  "stat": {
	"mounted": 1,   // 今回一時マウントしたユーザープロファイル数
	"leftover": 0,  // 実行完了時点で残っている一時マウント数(監視対象)
	"profiles": 2,  // 走査対象としたユーザープロファイル数
	"apps": 1,      // 検出したアプリケーション数
	"elapsed_ms": 661  // スクリプト全体の実行時間(ミリ秒)
  },
  "apps": [
	{
	  // LLDでアイテム名やタグに使用するマクロ
	  "{#APPNAME}": "アプリ名",
	  "{#APPVER}": "1.2.3",
	  "{#APPPUB}": "発行元",
	  "{#APPSCOPE}": "user",
	  "{#APPUSER}": "DOMAIN\\user01",
	  "{#APPSID}": "S-1-12-1-...",
	  "{#APPINSTDATE}": "",
	  "{#APPUID}": "S-1-12-1-...|アプリ識別キー",
	  // アイテムプロトタイプの保存前処理で使用する通常キー
	  "uid": "S-1-12-1-...|アプリ識別キー",
	  "instdate": ""
	}
  ]
}

apps配列の中には、LLDマクロ形式のキー({#APPUID} など)と、同じ値を持つ通常のキー(uid など)を併記しています。
アイテムプロトタイプの保存前処理では、パラメータに書いたLLDマクロが、評価される前に実際の値へ置き換えられます。そのため、JSONPathで項目名として {#APPUID} を指定すると、置き換えられた後の値が項目名として扱われ、目的の項目を参照できません。そこで、保存前処理では通常のキーを参照し、LLDマクロは比較する値の側だけで使っています(ステップ4参照)。

このスクリプトをZabbixエージェントのUserParameterで呼び出せるようにしておきます。

# zabbix_agentd.conf
UserParameter=app.list.machine,powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\zabbix\scripts\zbx_apps_machine.ps1"
UserParameter=app.list.user,powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\zabbix\scripts\zbx_apps_user.ps1"

ステップ2:マスターアイテムを作成

監視対象ホストに、スクリプトを実行するアイテムを1つ作成します。これが「マスターアイテム」となり、スクリプトを実行して階層JSONを取得します。
データ取得はこのマスターアイテムでのみ行われます。UserParameterで設定したキーを利用することで、エージェント経由でスクリプトを実行します。
頻繁なデータ取得は今回の用途では不要であるため、監視間隔は「1d」としています。

項目設定値
タイプZabbixエージェント
キーUserParameterで設定したキー(例:app.list.user)
データ型テキスト
監視間隔1d
2

ステップ3:依存ディスカバリルールを作成

次に、アプリを検出するディスカバリルールを作成します。タイプは「依存アイテム」、マスターアイテムにステップ2で作成したアイテムを指定します。
保存前処理でJSONPath $.apps を指定することで、JSONからアプリ一覧の配列を取り出せます。apps配列の中にLLDマクロ形式のキーを持たせているため、LLDマクロタブの設定は不要です。

項目設定値
タイプ依存アイテム
キー任意(例:app.user.discovery)
マスターアイテムステップ2で作成したアイテム
保存前処理 名前:JSONPath
パラメータ:$.apps
3
4

ステップ4:アイテムプロトタイプを作成

アプリごとの監視アイテムを自動生成するための「アイテムプロトタイプ」を作成します。名前には{#APPNAME}(バージョン:{#APPVER})のようにLLDマクロを利用します。
保存前処理は2段構成で、1段目のJSONPathで該当アプリの値を絞り込み、2段目のJavaScriptで配列から先頭の要素を取り出します。
タグはapp_scope(machine / user)、app_pub(発行元)、app_user(ユーザー名)の3つを設定します。タグを自動設定することで、後述の一覧表示の際の絞り込みに活用できます。

項目設定値
名前{#APPNAME}(バージョン:{#APPVER})
タイプ依存アイテム
キー任意(例:app.user.item[{#APPUID}])
データ型文字列
マスターアイテムステップ2で作成したアイテム
保存前処理1名前:JSONPath
パラメータ:$.apps[?(@.uid=='{#APPUID}')].instdate
保存前処理2名前:JavaScript
パラメータ:下記コード
タグapp_pub:{#APPPUB}
app_scope:user
app_user:{#APPUSER}
var a = JSON.parse(value);
if (!a || a.length === 0) { return ''; }
return a[0];
5
6
7

ステップ5:レジストリハイブの残留を監視

最後に、スクリプト実行後に一時マウントしたレジストリハイブが残っていないかを監視するアイテムを追加します。
今回のスクリプトはユーザー単位のアプリを読むためにreg loadでレジストリハイブを一時的にマウントしており、読み終わればreg unloadで戻しますが、何らかの理由でアンロードに失敗すると、マウントされたまま残ります。
そこで、スクリプトの出力するJSONに、実行完了時点での残留数(leftover)を含めています。 値が0であれば残留はありません。

項目設定値
タイプ依存アイテム
キー任意(例:app.mount.leftover)
データ型数値(整数)
保存前処理JSONPath:$.stat.leftover
トリガー例last(/ホスト名/app.mount.leftover)>0
8
9

実行結果

ディスカバリの実行結果がこちらです。アプリケーションごとにアイテムが自動生成されているのがわかります。新しいアプリをインストールすれば、次のディスカバリ実行時にZabbixが自動で検出してくれます。
なお、下記にはマシン全体にインストールされたアプリケーションのアイテムも含まれています。作成手順はユーザー単位とほぼ同じです。

10

参照方法については、当初はカスタムモジュールでウィジェットを作ることを想定していました。ところが標準の「最新データ」画面でも、ホストフィルタ・名前の部分一致検索・タグフィルタが利用できます。
ホストで絞り込めばその端末のアプリ一覧が並びますし、アプリ名で部分一致検索すれば、それが入っている端末が並びます。冒頭に挙げた2つの目的は、標準機能でほぼ満たせそうです。
カスタムモジュールはフロントエンドの内部クラスに依存するため、バージョンアップのたびに追随改修が必要になる場合があります。作り込む前に標準機能を試してみる、という順序をおすすめします。

「最新の値」列にはレジストリのInstallDateが表示されます。レジストリにInstallDateの情報が無いアプリは値が入りません。

11

ハマったポイント

ここからは、スクリプトを作る過程で引っかかったところをご紹介します。同じようなスクリプトを作る場合の参考になればと思います。

ユーザー単位のアプリが0件になる

マシン全体は36件取得できたのに、ユーザー単位の結果が0件。しかもエラーは出ません。
原因は、SIDの絞り込み条件をS-1-5-21-*に限定していたことでした。検証端末はEntra ID参加で、ログオンユーザーのSIDはS-1-12-1-…という形式です。オンプレAD参加を前提とした条件に一致せず、対象ユーザーが丸ごと除外されていました。
対処として、ホワイトリスト方式からブラックリスト方式へ反転しました。.DEFAULT、S-1-5-18/19/20、S-1-5-80-*(サービス)、S-1-5-82-*(IIS AppPool)、S-1-5-90-*、S-1-5-96-*、*_Classesを除外し、残りはすべて対象とします。あわせて、プロファイルのパスがC:\Windows配下のものはサービス用として除外しています。

reg unloadが「アクセスが拒否されました」で失敗する

reg loadとレジストリの読み取りは成功するのに、reg unloadだけが失敗し、HKEY_USERSにマウントが残ってしまいました。
最初はタイミングの問題かと考え、ガベージコレクションを明示的に呼んでリトライを入れてみましたが、解決しませんでした。
検証を進めると、レジストリプロバイダ経由(Get-ChildItem Registry::HKEY_USERS\… など)で一時マウントしたハイブを読み取った場合に、reg unloadが失敗していることが分かりました。そこで、読み取りを.NETのMicrosoft.Win32.RegistryKeyに切り替え、try/finallyでClose()とDispose()を明示的に実行するようにしました。

$baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
	[Microsoft.Win32.RegistryHive]::Users,
	[Microsoft.Win32.RegistryView]::Default)
try {
	$unKey = $baseKey.OpenSubKey($fullSub)
	# ... 読み取り処理 ...
}
finally {
	if ($unKey) { $unKey.Close(); $unKey.Dispose() }
	$baseKey.Close(); $baseKey.Dispose()
}

これでreg loadからreg unloadまで、残留なく完走するようになりました。
この件は、アプリ一覧が取得できた時点で検証を終えていたら完全に見逃していたところです。監視の仕組みは何十台、何百台という単位で展開するものであり、1台では気づかない副作用も、展開した先では全台で積み上がっていきます。欲しいデータが取れたかどうかだけでなく、対象の端末に何も残していないかまで確認する。エージェントから外部スクリプトを実行する仕組みを作るときは、ここまで見ておきたいところです。

日本語のアプリ名が文字化けする

LLDで生成されたアイテム名とタグの日本語が文字化けしました。今回の環境では、PowerShellの標準出力がUTF-8になっておらず、それをZabbix側がUTF-8として処理していたことが原因でした。
Write-Outputを使わず、UTF-8(BOMなし)のStreamWriterで標準出力へ直接書き込む方式に変更しました。BOMを付けるとJSONの先頭に不可視文字が入り、JSONPathの前処理が失敗しました。

$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[Console]::OutputEncoding = $utf8NoBom
$stdout = [Console]::OpenStandardOutput()
$writer = New-Object System.IO.StreamWriter($stdout, $utf8NoBom)
$writer.Write($json)
$writer.Flush(); $writer.Close()

ややこしいのは、スクリプトファイル自体の保存形式は逆だという点です。BOMなしUTF-8で保存すると、Windows PowerShell 5.1がシステムのANSIコードページとして読み込むため、日本語のコメントが文字化けし、構文エラーになりました。スクリプトはBOM付きUTF-8で保存してください。
出力はBOMなし、スクリプトファイルはBOM付き。逆になるので、覚えておくと役に立つかもしれません。

こんな場面で活用できそう

場面確認できること
野良ソフトの検知許可していないアプリが入っている端末を洗い出す。
必須ソフトの導入確認ウイルス対策ソフトや業務アプリなど、全台に入っているべきものが入っていない端末を特定する。
バージョンのばらつき把握同じアプリが複数のバージョンで混在していないかを確認する。
EOLバージョンの洗い出しサポートが終了したバージョンが残っている端末を特定する。
障害調査の参考情報特定の端末でのみ起きる事象を調査する際に、他の端末との差分として確認する。

効果が出そうなのは、一覧を眺める使い方よりも、「入っていてはいけないものが入っていないか」「入っているべきものが消えていないか」という条件を持たせる使い方です。条件に合致したときに通知する、というのはZabbixがもともと得意としている領域でもあります。

注意事項

  1. ・全アプリをアイテムとして展開する今回の構成は、台数が増えるほどアイテム数が膨らみます。1台あたり100~300アプリとすると、100台で1~3万アイテムです。監視したい条件がはっきりしている場合は、対象を絞ってアイテム化したほうが構成は軽くなります。

  2. ・インストール日は必ずしも正確な情報ではありません。レジストリのInstallDateはすべてのアプリに登録されているとは限らず、値を取得できないものもあります。また、MSI形式ではWindows Installerが書き込みますが、Microsoftのドキュメント上の定義は「その製品が最後にサービスを受けた日時」であり、パッチ適用や修復のたびに上書きされます。厳密な初回インストール日ではない点にご注意ください。

  3. ・バージョン情報が取得できないアプリも存在します。レジストリにDisplayVersionが登録されておらず、バージョン情報を取得できないアプリもあります。

  4. ・多数のユーザー・ホストが存在する環境での動作は未確認です。今回は検証段階のため、多数のユーザーが存在する環境や、多数のホストへ設定した場合の動作は確認していません。

おわりに

いかがだったでしょうか。
ちなみに今回も、データ取得用のスクリプトは生成AIをベースに作成しました。ただ、SYSTEM権限での動作やEntra ID参加端末の挙動といった部分は、実機で動かしてみないと出てこない話です。AIの力を借りつつ、実機での確認は丁寧に。このあたりのバランスが大事だとあらためて感じました。

今回はZabbixエージェントからインストール済みアプリケーション一覧を取得し、LLDでアプリ単位のアイテムに展開するところまでのご紹介となります。専用の資産管理ツールの代わりになるわけではありませんが、導入必須アプリケーションの有無を確認したり、不要なアプリケーションが入っていないかを洗い出したりといった用途であれば、十分に実用的な選択肢になりそうです。
今後もZabbixの可能性を広げる使い方や検証結果をご紹介していきます。
なお、当社はZabbix認定パートナーとして、お客様のZabbix環境の構築や公式サポートを提供しております。ぜひこちらも一度ご検討いただければと思います。

関連リンク

ヴィンクスのZabbixソリューションはこちら

Zabbixソリューション



Zabbixについてお問い合わせ等ございましたら、こちらから

Zabbix関連のお問合せ