Create  Edit  Diff  FrontPage  Index  Search  Changes  Login

The Backyard - WindowsManagementInstrumentation Diff

  • Added parts are displayed like this.
  • Deleted parts are displayed like this.

!Windows Management Instrumentation

""このテキストはアスキーから出版されたWindows 2000 Magazineに寄稿したものの初稿を復元したものです。

HTMLアプリケーションを利用して、RubyでWindows2000の簡易管理ツールを作成してみよう。

Windows2000には、Windows Management Instrumentation(以下単にWMIと記す)というシステム管理サービスのオブジェクトが搭載されている(本誌1号137ページ、本誌3号140ページ参照: 注「本誌」とはWindows2000 Magazineを指す)。今回は、実際に前回紹介したGUIシェルのHTMLアプリケーションの応用例として、WMIを使用したWindows2000用簡易管理ツールを作成していこう。

WMI自体は、Distributed Management Task Force(http://www.dmtf.org/)によって規格化されたCIM(Common Information Model)という管理情報モデルのWin32実装へのアクセス手段を提供するサービスだ。このCIMというオブジェクトモデルは、WBEM(Web-Based Enterprize Management)というシステム管理情報に関するイニシアティブに基づいたものだ。

こう書くと、なんのことやら非常にわかりにくいが、この関連を例えば、XML(マークアップ言語の規格)に対するDOM(実際の文書をオブジェクトとしてプログラムから扱う場合のモデル)に対するMSXML(XMLをパーシングして、DOMオブジェクトに変換するCOMのコンポーネント)というように、より身近なものに置き換えると多少は理解しやすいかも知れない。

ここで注意しておくべきなのは、あくまでもWMIを通して得られる情報は、この規格に則って提供されたものだけだということだ。後のほうでインストールされたアプリケーション情報の取得スクリプトが出てくるが、動かして見れば一目瞭然、MSI(Microsoft Installer)を使用してインストールされたものについてしか、情報は取得できない。最近のものについてはわからないが、少なくてもWindowsNT4.0時代のインストールシールドを使用したアプリケーションのインストール情報は得ることができないのだ。

従って、現在のように、まだWindows9xや、WindowsNTをインストールしたPCが残っていたり、その時代のアプリケーションを使用しているような環境では、WMIですべてが管理できるわけではない。実際、Windows2000内部でも、コントロールパネルの「アプリケーションの追加と削除」は、旧来の情報(レジストリのインストール情報)にアクセスしているはずだ。

だからといって、「まだ使えない」と切り捨てる必要はない。Windows2000が出荷されて1年が経過した現在では、むしろこれからのシステム管理の主機能として「そろそろ始めよう」と考えるのが良いのではないだろうか。

!!共通項目

以降のHTAで共通の部分について説明をしておこう。

<html>
<head>                          ←スクリプトはHEAD内に記述する
<script language="RubyScript">  ←スクリプト言語としてRubyScript
                                   (ActiveScriptRuby)を指定する
def resize                      ←ウィンドウのサイズを調整するためのメソッド
   resizeTo 720, 400               WindowオブジェクトのresizeToメソッドの引数は
                                   幅と高さである
end                              このメソッドはロード完了時(BODY要素の
                                  onloadイベント)に呼び出される
def connectWMI(wql, host, uid, pwd)  ←WMIオブジェクトを取得するための
                                        共通メソッド
   locator = WIN32OLE.new("WbemScripting.SWbemLocator.1")  ←WMIをスクリプトから
                              使用する場合、SWbemLocatorオブジェクトを作成する。
   if host.empty?                ←ホスト名を指定していない場合の処理
                                 (Stringオブジェクトのempty?メソッドは
                                   空文字列ならば真となる)
     service = locator.ConnectServer  ←ローカルホストへ接続する場合、引数は不要
                                       (ローカルホストのWMIへの接続時には
                                         ユーザー/パスワードの指定はできない
                                         ため、通常、引数を指定しない)
   else
     service = locator.ConnectServer host, "root/cimv2", uid, pwd
                               ←指定されたホストへ指定ユーザーとして
                               ログオンする。
                               デフォルトでは、管理者権限を持つユーザー
                               以外はリモートにはログオンできない
                               (アクセス拒否を受ける 図:rb4-2.tif)
                               なお、Win32OLEでは途中の引数を省略できないため、
                               WMIのネームスペースとして"root/cimv2"(規定値)を
                               指定する
   end
   service.ExecQuery(wql)        ←オブジェクトクェリーを発行する。
                                   Rubyは最後に評価した値がメソッドの戻り値と
                                   なるので、明示的なreturnの記述は不要
end

def showinformation
   tbl = Window.tbl              ←tblという変数にテーブルオブジェクトを代入
   while tbl.rows.length > 1     ←テーブルの先頭行以外を削除する
                                   すなわち、行数(rows.length)が1より大きい
                                   場合は次行の処理を行う
     tbl.deleteRow 1             ←0行目(THによるヘッダ行)を残すため、
                                   削除する行番号として1を指定する
   end
   set = connectWMI 検索文字列,
     Window.host.value, Window.uid.value, Window.pwd.value
                                 ←WMIのサーバオブジェクトのExecQueryに渡す
                                   検索文字列は、WQLと呼ばれるSQLのサブセット。
                                   返送される値は、クェリーの結果のオブジェクト
                                   の集合オブジェクトとなる。
   set.each do |obj|            ←イテレータを使用して各オブジェクト毎に処理を
                                 行う
     tr = tbl.insertRow -1      ←テーブルに1行追加する
                                 追加した結果はTRオブジェクトとなる
                                 引数の-1は最後を意味する特別な値
     td = tr.insertCell -1      ←TRにセルを追加する
                                 引数の-1は最後を意味する特別な値
     td.innerText = obj.property  ←TDオブジェクトのinnerTextプロパティに
                                 設定した文字列はそのまま表示される
     td.innerHTML = obj.property  ←TDオブジェクトのinnerHTMLプロパティに
                                 設定した文字列はHTMLとして解釈される
   end
end

def clear                      ←フォームを使用していないため、
                                  <INPUT TYPE="RESET">を使用した消去ができない
                                 ので自力でリセット処理を行う
   Window.host.value = ""      ←空文字列をTEXTのvalue要素へ代入して消去する
   Window.uid.value = ""
   Window.pwd.value = ""
end
</script>
</head>
<body onload="resize" language="RubyScript">
コンピュータ:<input type="text" id="host">  ←WMIを使用して情報を取得する
                                   コンピュータを指定する
                                   ローカルホストの情報を取得する場合は
                                   入力を行ってはならない
ユーザー名:<input type="text" id="uid">
パスワード:<input type="password", id="pwd">
<input type="button" id="go" onclick="showinformation"
       value="実行" language="RubyScript">  ←実行ボタンを押すとshowfixメソッド
                                             が呼び出される
<input type="button" id="clr" onclick="clear"
       value="クリア" language="RubyScript">  ←クリアボタンを押すとclear
                                             メソッドが呼び出される
<hr>
<table id="tbl" border="2">                ←情報表示用のテーブル
  <tr>
   <th>タイトル1</th><th>タイトル2</th>    
  </tr>
</table>
</body></html>

----
!!!WMIからの情報の取得方法

#プログラムIDに"WbemScripting.SWbemLocator.1"を指定してロケータオブジェクトを取得する。
#ロケータオブジェクトのConnectServerメソッドを使用してWMIサーバオブジェクトを取得する
#WMIサーバオブジェクトのExecQueryメソッドを使用して、目的とするオブジェクトのセットを取得する
#セットにイテレータを適用して、個々のオブジェクトから情報を取得する。

●ExecQueryの引数は、WQLと呼ばれるSQLのサブセットである。
:WQLの構文: select プロパティ名 from オブジェクト名 [where 条件]

複数のプロパティ名を指定する場合は、「,」で区切る。「*」を指定するとすべてのプロパティの取得の意味になる。

なお、WMIについての詳細な情報は、http://msdn.microsoft.com/Downloads/sdks/wmi/からSDKをダウンロードすることによって入手可能である。
----

!!ホットフィックス

ホットフィックスの適用は、管理上の必須項目だ。2月初旬にももフィックスを行っていなかったIISをターゲットにした大規模なクラッキングが発生したが、Webサーバに限らず、ホットフィックスはこまめに適用したい。

ところが、エクスプローラのバージョン情報を見ればすぐに確認できるサービスパックと異なり、ホットフィックスの適用情報はよくわからないというのが実情だ。

次のHTAは、現在適用されているホットフィックスIDと説明文を表示する。またホットフィックスIDの表示にはアンカータグを使用して、相当するKB(ナレッジベース)へのリンクとした。クリックすることによってどのような問題がフィックスされたのかをすぐに確認できるわけだ(実行例:rb4-1.tif)。

!!!hfix.hta

<html><head>
<script language="RubyScript">
def resize
   resizeTo 720, 400
end

def connectWMI(wql, host, uid, pwd)
   locator = WIN32OLE.new("WbemScripting.SWbemLocator.1")
   if host.empty?
     service = locator.ConnectServer
   else
     service = locator.ConnectServer host, "root/cimv2", uid, pwd
   end
   service.ExecQuery(wql)
end

def showfix
   tbl = Window.tbl
   while tbl.rows.length > 1
     tbl.deleteRow 1
   end
   set = connectWMI "select * from Win32_QuickFixEngineering",
     Window.host.value, Window.uid.value, Window.pwd.value
   set.each do |fix|
     tr = tbl.insertRow -1
     td = tr.insertCell -1
     qid = fix.HotFixID
     td.innerHTML =
      "<a href='http://support.microsoft.com/kb/#{qid}/ja'>#{qid}</a>"
     td = tr.insertCell -1
     td.innerText = fix.Description
   end
end

def clear
   Window.host.value = ""
   Window.uid.value = ""
   Window.pwd.value = ""
end
</script>
</head>
<body onload="resize" language="RubyScript">
<center><h1>ホットフィックス情報</h1></center>
コンピュータ:<input type="text" id="host">
ユーザー名:<input type="text" id="uid">
パスワード:<input type="password", id="pwd">
<input type="button" id="go" onclick="showfix"
       value="実行" language="RubyScript">
<input type="button" id="clr" onclick="clear"
       value="クリア" language="RubyScript">
<hr>
<table id="tbl" border="2">
  <tr>
   <th>ホットフィックスID</th><th>説明</th>
  </tr>
</table>
</body></html>

----
!!!WMIをRubyで使用する場合の注意点

RubyでWMIを使用する場合、重大な注意事項がある。それは、MSWin版(例えば、ActiveScriptRubyパッケージ)Rubyで直接スクリプトとして使用すると、終了時にCOMのクリーンアップ処理とオブジェクトの解放の順序の問題でプログラムがクラッシュするということだ。

ただし、ここで示したように、ActiveScriptRubyとして、HTAや、Windowsスクリプトホスト(CScript、WScript)から使用する場合には、この問題は発生しない。またCygwin版Rubyでも筆者の経験では問題は発生していない。

従って、コンソールベースのツールとしてRubyでWMIを使用するスクリプトを実行する場合は、ActiveScriptRubyでCScriptを使用して実行するか、Cygwin版Rubyを使用して頂きたい.
----

!!アプリケーションインストール情報

WMIのWin32_Productオブジェクトを使用すると、ローカルホストのみならずリモートホストについても、インストールされたアプリケーションの情報の取得や、再インストール、アンインストールを行うことができる。

これは非常に強力な機能であるが、最初に触れたように、適用できるのがMSIを使用してインストールされたアプリケーションだけなのは残念だ。もっとも、最近のマイクロソフト製品であれば必ずMSIを使用しているし、サードパーティ製品であってもWindows2000をターゲットとしたものについては(例えばトレンドマイクロのウィルスバスター2001など)MSIを使用しているので、今後はWMIを使用してすべてのインストール情報を管理できるようになることが期待できる。

また、当然ではあるが、リモートホストへログインしたユーザー(デフォルト状態では管理者権限を持つユーザーに限定される)がインストールしたアプリケーションと、すべてのユーザー用にインストールしたアプリケーション以外の情報の取得はできない。従って管理しやすさを優先する場合、管理者権限を持たないユーザーによるアプリケーションのインストールはあらかじめできないようにポリシーを設定しておかなければならない。

なおここで紹介するHTAでは、インストールされているアプリケーション情報の表示のほかに、アンインストールも行うことができるようにしてある(実行例:rb4-3.tif)。

!!!inst.hta

<html><head>
<script language="RubyScript">
@obj = Array.new  # 再インストール/アンインストールで使用するWin32_Productオブジェクトを
                   # 保持しておくための配列を作成している。
                   # このように、ホストアプリケーション(この場合はHTA)と変数の生存期間が一致する場合、
                   # Rubyでは@を先頭につけて特殊な変数だということを明示する。
                   # なお、HEADのscriptブロックでメソッド宣言の外側に配置したコードは、
                   # 初期化処理時に1度だけ実行される。

def resize
   resizeTo 900, 400
end

def connectWMI(wql, host, uid, pwd)
   locator = WIN32OLE.new("WbemScripting.SWbemLocator.1")
   if host.empty?
     service = locator.ConnectServer
   else
     service = locator.ConnectServer host, "root/cimv2", uid, pwd
   end
   service.ExecQuery(wql)
end

def showinst
   tbl = Window.tbl
   while tbl.rows.length > 1
     tbl.deleteRow 1
   end
   set = connectWMI "select * from Win32_Product",
         Window.host.value, Window.uid.value, Window.pwd.value
   idx = 0
   @obj.clear              # clearメソッドを使用して配列を空にする
   set.each do |ins|
     @obj << ins           # 配列に要素を追加するときは<<を使用する。
     tr = tbl.insertRow -1
     td = tr.insertCell -1
     td.innerText = ins.description
     td = tr.insertCell -1
     dat = ins.installdate
     td.innerText = "#{dat[0,4]}/#{dat[4,2]}/#{dat[6,2]}"
     td = tr.insertCell -1
     case ins.installstate  #値によって処理を分岐させたい場合、case…when…endを使用する
     when 5
       state = "Installed"  # ins.installstateが5の場合、ここに処理が移る
                            # この処理実行後は、endの次の行にジャンプする
     when 2
       state = "Absent"
     when 1
       state = "Advertised"
     when -1
       state = "Unknown Package"
     when -2
       state = "Invalid Argument"
     else
       state = "Bad Configuration"
     end
     td.innerText = state
     td = tr.insertCell -1
     td.innerText = ins.vendor
     td = tr.insertCell -1
     td.innerText = ins.version
     td = tr.insertCell -1
     td.innerHTML = "<input type='button' value='削除'>"
     td = tr.insertCell -1
     td.style.display = "none"   # 非表示要素を作成
     td.innerHTML = idx.to_s     # オブジェクトのインデックスを保存する
     idx += 1
   end
end

def tblclick                      # テーブル内の要素をクリックすると呼び出される
   elem = Window.event.srcElement  # イベント発生元を取り出す
   return unless elem.tagName == "INPUT" # INPUT(この場合ボタン)でなければ戻る
   td = elem.parentElement               # INPUTの親にあたるTD要素を取り出す
   td = td.nextSibling                   # インデックスを保存したTD要素を取り出す
                                   # nextSiblingは次の兄弟要素を取り出すメソッドで、
                                   # TDに使用すると同一TR内の次のTDを意味する。
   o = @obj[td.innerText.to_i]     # インデックスを使用して、該当するWin32_Productオブジェクトを取り出す
   o.uninstall                     # uninstallメソッドで、アンインストールが行われる
   showinst                        # 結果を反映させる
end

def clear
   Window.host.value = ""
   Window.uid.value = ""
   Window.pwd.value = ""
end
</script>
</head>
<body onload="resize" language="RubyScript">
<center><h1>インストール情報</h1></center>
コンピュータ:<input type="text" id="host">
ユーザー名:<input type="text" id="uid">
パスワード:<input type="password", id="pwd">
<input type="button" id="go" onclick="showinst"
       value="実行" language="RubyScript">
<input type="button" id="clr" onclick="clear"
       value="クリア" language="RubyScript">
<hr>
<table id="tbl" border="2" onclick="tblclick">
  <tr>
   <th>製品名</th><th>インストール日付</th><th>状態</th><th>ベンダー</th><th>バージョン</th>
  </tr>
</table>
</body></html>

!!アプリケーションインストール

これまでの例は、WQLによって既存オブジェクトを取得して、保持する情報を表示する処理であった。今度は、情報を持たない空のオブジェクトを使って処理を行う例を紹介しよう。

前のパートで使用したWin32_Productを使うことにより、ローカルホストやリモートホストにアプリケーションをインストールすることができる。この場合は、まだインストールが行われていないため、既存情報を持ったWin32_Productオブジェクトの必要はない。このような処理では、WMIサーバオブジェクトのGetメソッドを使用する。

----
!!!既存情報から独立なWMIオブジェクトの取得方法

#プログラムIDに"WbemScripting.SWbemLocator.1"を指定してロケータオブジェクトを取得する。
#ロケータオブジェクトのConnectServerメソッドを使用してWMIサーバオブジェクトを取得する
#WMIサーバオブジェクトのGetメソッドを使用して、目的とするオブジェクトを取得する
----------------------------------------------------------------

インストールに使用するメソッドは、installメソッドだ。このメソッドは引数を3個取り、書式は以下のようになる。

WIN32_Product.install ファイル名, コマンド行オプション, AllUser

ファイル名には、MSIに則ったインストーラファイルを指定する。注意が必要なのは、リモートホストに対するインストール時には該当するホストからアクセスできるパス名を与える必要があるということだ。HTAの「参照」ボタンを押して得られるローカルファイルのパスを与えると、リモートホストに同一パス名が存在していなければ、当然失敗する。

コマンド行オプションは、ここでは空文字列を指定しているが、プロパティ=設定値という形式を取る。実際にどのようなプロパティがあるかはインストールするパッケージに依存する。

AllUserで示した第3引数は、trueまたはfalseのブール値で、現在のログインユーザー用にインストールする場合falseを、全ユーザー用にインストールする場合trueを指定する。ここでは、リモートホストに対しては必ずAdministratorログインとなるため、全ユーザー用インストールを指定している。

installメソッドの主な戻り値は以下のとおりである。

:0:正常終了
:3:ファイルが見つからない
:123:構文エラー(空白が入ったパス名を渡すときに""を付けて渡したりするとこのエラーとなる。メソッドに対する引数なのでパスを""で囲んだりする必要はない)
:1619:インストールパッケージのオープンエラー
:1620:インストールパッケージのオープンエラー

現時点では、1619および1620の相違点はSDKからではわからないが、3と異なり実際にファイルが存在する場合にこれらのエラーとなる。ファイルの読み取り制御権の問題や、1度インストールした時のパッケージについての情報との矛盾がある場合(会話型で実行した時にパスの再入力を要求されるような場合)、MSIを使用していないパッケージを指定した場合に返されるようだ。

実行例(rb4-4.tif)は、ファイル名にActiveScriptRubyのURL(http://arton.hp.infoseek.co.jp/ActiveRuby18.msi)を指定してインストールを行ったところである。ActiveScriptRubyは拡張子をbinとして配布しているため、通常であれば、MSIを起動するために拡張子を1度MSIに変更しなければならないのだが、この例のようにWIN32_Productオブジェクトに直接ファイルを渡す場合、フォーマットがMSIに則っているため、正常にインストールが可能だ。

!!!instnew.hta

<html>
<head>
<script language="RubyScript">
def resize
   resizeTo 700, 200
end

def connectWMI(objname, host, uid, pwd)
   locator = WIN32OLE.new("WbemScripting.SWbemLocator.1")
   if host.empty?
     service = locator.ConnectServer
   else
     service = locator.ConnectServer host, "root/cimv2", uid, pwd
   end
   service.Get objname
end

def doinst
   file = Window.fl.value
   if file.empty?
     alert "ファイルが指定されていません"
     return
   end
   inst = connectWMI "Win32_Product",
          Window.host.value, Window.uid.value, Window.pwd.value
   n = inst.install file, "", true
   Window.result.innerText = "結果:#{n}"
end

def clear
   Window.host.value = ""
   Window.uid.value = ""
   Window.pwd.value = ""
   Window.fl.value = ""
   Window.result.innerText = ""
end
</script>
</head>
<body onload="resize" language="RubyScript">
<center><h1>アプリケーションインストール</h1></center>
コンピュータ:<input type="text" id="host">
ユーザー名:<input type="text" id="uid">
パスワード:<input type="password", id="pwd">
<br>
<input type="file" id="fl" size="80">
<input type="button" id="go" onclick="doinst"
       value="実行" language="RubyScript">
<input type="button" id="clr" onclick="clear"
       value="クリア" language="RubyScript">
<hr>
<span id="result"></span>
</body>
</html>

!!イベントログ

今度は、WMIのイベント機能を使用して、イベントログに対する書き込みを監視するHTAを作成してみよう。

----
!!!WMIイベントオブジェクトの取得方法

#プログラムIDに"WbemScripting.SWbemLocator.1"を指定してロケータオブジェクトを取得する。
#ロケータオブジェクトのConnectServerメソッドを使用してWMIサーバオブジェクトを取得する
#WMIサーバオブジェクトのExecNotificationQueryメソッドを使用して、目的とするオブジェクトを取得する

:イベントを取得する場合のWQL:select * from イベントオブジェクト within 監視間隔 where 条件文

イベントオブジェクトには、条件文で特定したオブジェクトの、生成(ここで使用している__InstanceCreationEventオブジェクト)や削除(__InstanceDeletionEvent)といった特定のイベントに応じたものが用意されている。

監視間隔は、WMIが指定したオブジェクトのチェックを行う間隔を秒単位で指定する。

条件文では、イベント監視の対象となるオブジェクトを示すtargetinstanceというキーワードを使用することができる。
----------------------------------------------------------------

WQLの記述で使用している<<-WQL……WQLという書き方は、Rubyではヒアドキュメントといい、「<<-」に空白を入れずに続けて記述したシンボル(この場合、WQL)から独立した行に再度そのシンボルが出現するまでを文字列として扱うものだ。

ここでは、セレクトタグから指定されたイベントログに対するレコード(イベントログの各レコードは、WIN32_NTLogEventというWMIオブジェクトである)の追加(WMIのイベントモデルでは、インスタンスの作成と解釈されるため、__InstanceCreationEventオブジェクトによって示される)を検出して、HTA上のテーブルタグに行を追加していくように作成してみた(実行例:rb4-5.tif)。

なお、このHTAにはセキュリティログとシステムログを見るために権限を付与している個所があるので、明示的にログインするリモートホストの監視の場合はともかく、ローカルホストで実行するためには管理者権限でのログインが必要である。

!!!evlog.hta

<html>
<head>
<script language="RubyScript">
@timer = nil

def resize
   resizeTo 700, 500
end

def connectWMI(wql, host, uid, pwd)
   locator = WIN32OLE.new("WbemScripting.SWbemLocator.1")
   sec = locator.security_    # セキュリティおよびシステムログの監査に必要な権限を付与
   sec.privileges.add 7       # アプリケーションログを監視するだけならこの2行は不要
   if host.empty?
     service = locator.ConnectServer
   else
    service = locator.ConnectServer host, "root/cimv2", uid, pwd
  end
  service.ExecNotificationQuery wql
end

def docheck
   wql = <<-WQL
     select * from __InstanceCreationEvent
       within 3
     where targetinstance isa 'WIN32_NTLogEvent'
       and targetinstance.logfile = '#{Window.logfile.value}'
   WQL
   @event = connectWMI wql,
         Window.host.value, Window.uid.value, Window.pwd.value
    @timer = setTimeout "checkloop", 1000, "RubyScript"
   Window.statdisp.innerText = "監視中"
end

def checkloop
   tbl = Window.tbl
   while true   # 滞留しているイベントをすべて受信するために、ループを使用
     begin
       evt = @event.nextevent 500 # 500ミリの間、イベントオブジェクトの取得を試みる
     rescue     # イベントオブジェクトの取得に失敗すると例外が発生するため、例外をキャッチする
       @timer = setTimeout "checkloop", 1000, "RubyScript" # 再度、タイマーを発生させる
       return
     end  
       log = evt.targetInstance # ターゲットとなるオブジェクトは、
                                # nexteventメソッドが返したオブジェクトのtargetInstanceプロパティで
                                # 取得する
     tr = tbl.insertRow -1
     td = tr.insertCell -1
     td.innerText = log['type'] # RubyのObjectクラスのメソッド名と重複するプロパティ名を呼び出す場合の書式
     dt = log.timeGenerated     # date型はWin32OLEでは'yyyymmddhhmmss'という文字列で返る
     td = tr.insertCell -1
     td.innerText = "#{dt[0,4]}/#{dt[4,2]}/#{dt[6,2]}"
     td = tr.insertCell -1
     td.innerText = "#{dt[8,2]}:#{dt[10,2]}:#{dt[12,2]}"
     td = tr.insertCell -1
     td.innerText = log.message
     td = tr.insertCell -1
     td.innerText = log.sourceName
   end
end

def clear
   if @timer.nil? == false
     clearTimeout @timer
     @timer = nil
   end
   Window.statdisp.innerText = "停止中"
   Window.host.value = ""
   Window.uid.value = ""
   Window.pwd.value = ""
   tbl = Window.tbl
   while tbl.rows.length > 1
     tbl.deleteRow 1
   end
end
</script>
</head>
<body onload="resize" language="RubyScript">
<center><h1>イベントログ監視</h1></center>
コンピュータ:<input type="text" id="host">
ユーザー名:<input type="text" id="uid">
パスワード:<input type="password", id="pwd">
<br>
<center>
<select id="logfile">
  <option selected value="Application">アプリケーション
  <option value="Security">セキュリティ
  <option value="System">システム
</select>
<input type="button" id="go" onclick="docheck"
       value="実行" language="RubyScript">
<input type="button" id="clr" onclick="clear"
       value="クリア" language="RubyScript">
</center>
<hr>
<center><h2 id="statdisp">停止中</h2></center>
<table id="tbl" border="2">
  <tr>
   <th>タイプ</th><th>日付</th><th>時刻</th><th>メッセージ</th><th>ソース</th>
  </tr>
</table>
</body>
</html>

これまでのHTAと異なり、イベントログへの書き込みというのは、ただ待っていてもなかなか出会うことはできない。読者の便宜のため、本当に動いているかの確認用にアプリケーションイベントログへの書き込みを行うスクリプトを付けておこう(logwrite.hta)。ボタンを押すと、それぞれのタイプに応じたイベントの書き込みを行うHTAだ。

ただしHTA(IE)自体のイベント処理の関係で、素早くボタンをクリックしても必ずしもHTA内でonclickイベントが処理されるとは限らないから、押した数だけevlog.hta [[help writing|http://writing-help.org/]] evlog.htaがイベントログを表示しないと悩む必要はない(ボタンを押した数だけイベントログに書き込みが行われるわけではない)。従ってevlog.htaの精度についてはイベントビューアと見比べると良い。

!!!logwrite.hta

<html><head>
   <HTA:APPLICATION ID="logWrite" scroll="no" />
<script language="RubyScript">
@shell = WIN32OLE.new("WScript.Shell")

def resize
   resizeTo 400, 100
end

def logwrite(tp, msg)
   @shell.logEvent tp, msg
end
</script>
</head>
<body onload="resize" language="RubyScript">
<input type="button" value="エラー" id="e"
  onclick="logwrite 1, 'これはエラーです'"
  langugage="RubyScript">
<input type="button" value="警告" id="w"
  onclick="logwrite 2, 'これは警告です'"
  langugage="RubyScript">
<input type="button" value="情報" id="i"
  onclick="logwrite 4, 'これは情報です'"
  langugage="RubyScript">
<input type="button" value="監査の成功" id="s"
  onclick="logwrite 8, 'これは監査の成功です'"
  langugage="RubyScript">
<input type="button" value="監査の失敗" id="f"
  onclick="logwrite 16, 'これは監査の失敗です'"
  langugage="RubyScript">
</body></html>

このイベントログに対する書き込みイベントの検出機能は、(インストール情報などと異なり)現時点ですぐに使用できるという意味で、非常に実用的だ。

そこで、WIN32_NTLogEventオブジェクトの主なプロパティを列挙しておこう(表1)。evlog.htaでは、抽出条件として、logfileプロパティのみを使用したが、プロパティを知っていれば、より必要に応じたWQLの記述が可能だからだ。

例えば、監査の失敗のログだけを見張る場合、evlog.htaで使用するWQLの条件部を

where targetinstance isa 'WIN32_NTLogEvent'
  and targetinstance.logfile = 'Security' and targetinstance.type = '監査失敗'

のように書き換えることで対応できる。

また、Rubyの能力を利用して、messageプロパティに含まれる文字列を正規表現でマッチングさせて、スクリプト内でさらに絞りこむことも可能だ。


||プロパティ名||意味
||Category||ソース依存の分類番号
||CategoryString||ソース依存の分類文字列
||Computername||イベントが発生したホスト名
||EventCode||イベント番号(イベントビューアの「イベント」に相当)
||EventIdentifier||ソース内部でのイベント識別子(下位16ビットはeventcode)
||Logfile||イベントログの種類(アプリケーション、セキュリティ、システム)
||Message||ログメッセージ(※)
||RecordNumber||イベントログ上のレコード番号
||SourceName||ソース名
||TimeGenerated||イベントログの発生日付
||TimeWritten||イベントログへの書き込み日付
||Type||イベント種(※)
||User||イベントがログオンユーザーと関連している場合、ユーザー名(通常はnil)

※ このプロパティは、取り出し方法によって、error、warning、infroamtion、audit success、audit failureという文字列が取れる場合と、エラー、警告、情報、監査成功、監査失敗という文字列が取れる場合とがある。筆者の経験では、前者は直接WIN32_NTLogEventを取得した場合に、後者はイベント経由で取得した場合に、それぞれ返される。実行環境のロケールが影響していると想像できるのだが、Win32OLEで特にロケール情報を渡しているのではないため、現時点では詳細は不明だ。

表2 特殊なプロパティ

||Data||バイナリデータ
||InsertionStrings||挿入文字列

表2のプロパティは、特殊なため、別枠で示した。

Dataはイベントビューアの「イベントのプロパティ」で表示されるウィンドウの下部ペインのバイナリデータに相当するプロパティ(配列)であるが、現在のWin32OLEの実装と型の互換性がないため、先頭1バイトしか取得できない。

InsertionStringsは、Messageプロパティを構成する文字列を配列として返すプロパティである。従ってMessageプロパティを参照すれば良いため、使用する必要はない。

!!イベントログのメールによる通知

最後に、非常に現実的な例として、監査関係とエラーのイベントを検出すると、管理者宛てに電子メールを送信するスクリプトを示そう。なお、余談になるが、セキュリティログのチェックを行う場合は、監査の失敗だけではなく、必ず監査の成功もチェックしなければならない。ある意味では失敗より成功のほうが危険な状態を意味している可能性があるからだ。

Windows2000でCOMのオブジェクトを使用して電子メールを送信するには幾つか方法があるが、ここではもっとも標準的な手法であるIISのSMTP機能を利用している(Outlookがインストールされている場合は、Outlookを使用するという方法もある)。実際に試すにはあらかじめIISをインストールしておいて頂きたい(本誌1号135ページ参照)。

!!!logmail.hta

<html>
<head>
<script language="RubyScript">
@timer = nil

def resize
   resizeTo 200, 100
end

def connectWMI(wql)
   locator = WIN32OLE.new("WbemScripting.SWbemLocator.1")
   sec = locator.security_
   sec.privileges.add 7
   service = locator.ConnectServer
   service.ExecNotificationQuery wql
end

def docheck
   wql = <<-WQL
     select * from __InstanceCreationEvent
       within 3
     where targetinstance isa 'WIN32_NTLogEvent'
           and (targetinstance.type = '監査失敗'
                or targetinstance.type = '監査成功'
                or targetinstance.type = 'エラー')
   WQL
   @event = connectWMI wql
   @timer = setTimeout "checkloop", 1000, "RubyScript"
end

def checkloop
   while true
     begin
       evt = @event.nextevent 500
     rescue
       @timer = setTimeout "checkloop", 1000, "RubyScript"
       return
     end
     log = evt.targetInstance
     from = "xxx@xxxx.co.jp"           #実際のアドレスに置換
     to = "xxx@xxx.co.jp"              #実際のアドレスに置換
     subject = "#{log.computername}:#{log.logfile}"
     dt = log.timeGenerated
     body = <<-BODY
       発生日付:"#{dt[0,4]}/#{dt[4,2]}/#{dt[6,2]}"
       発生時刻:"#{dt[8,2]}:#{dt[10,2]}:#{dt[12,2]}"
       ソース:#{log.sourceName}
       #{log.message}
     BODY
     smtp = WIN32OLE.new "CDONTS.NewMail"
     smtp.invoke "send", from, to, subject, body, 2  #Rubyで定義済みのメソッドと名前が衝突する場合は、
                                        #invokeメソッドを使用し、第1引数に文字列としてメソッド名を渡す
   end
end

def start
   resize
   docheck
end
</script>
</head>
<body onload="start" language="RubyScript">
<center><h1>ログ監視中</h1></center>
<br>
</body>
</html>

送信方法の書式が本誌1号137ページのリストと異なっているが、これは、sendというメソッドがRubyのオブジェクトにあらかじめ組み込まれているため、直接NewMailオブジェクトのsendメソッドの呼び出しができないこと(Ruby自身が持つsendメソッドが呼び出されるためエラーとなる)によるものだ。引数の最後の「2」は、メールの高プライオリティを示す値で、他に低プライオリティを示す「0」、通常を示す「1」が指定できる。

最後に、実際にこういったスクリプトでシステムを運用する場合にあたっての注意事項を書いておこう。

logmail.htaのようなスクリプトを監視対象のサーバで実行するためには、イベントログ読み込み権限付与のためAdministrator(あるいは管理者権限を持ったユーザー)としてログインしたままコンピュータを放置しなければならないことになる。管理者がログインした状態でコンピュータを放置するということはセキュリティという面では実に危険極まりない。もちろん、サーバが設置されている部屋そのものにセキュリティが施されており、管理者以外コンソールに触ることができないのであれば問題はないが、そうでないのなら、evlog.htaで示したリモートホストへのログインと組み合わせて、監視用コンピュータから実行するというような考慮が必須である(この時、監視対象のイベントログはリモートホストのものになるが、実際のスクリプト処理はローカルホスト側で実行されるのであるから、IISはローカルホスト側で実行しておかなければならない)。また、いちいち入力するのが面倒だからとHTA内に直接管理者のユーザ名とパスワードを記述したりすることはもってのほかだ。面倒であっても必ず手で入力するように作成すべきである。

!!まとめ

今回は、Ruby、HTA、WMIを使用した、典型的な簡易管理ツールの実例を紹介した。いずれも100行に満たない小さなものであるが、これらのサンプルが実作業への手がかりになれば筆者としては幸いである。