2012年9月28日金曜日

◆イベントログを抽出する FilterXPath

以前にもイベントログの抽出はやっている。
PowerShell: ◆イベントログを取得(抽出)する(Get-WinEvent)

ただし、こいつは標準的なプロパティで抽出しているので、イベント固有の項目で抽出となると簡単にはいかない。

多分、下記の赤枠部分が共通的なプロパティで青枠が個別のプロパティといった感じだろう。(イベントビューワの画面)
image

では、この個別部分で抽出するにはどうするのか。
ヘルプを見るとFilterXPathパラメータにXPath形式で条件を記述してやるようだ。

Get-WinEvent -logname security  -FilterXPath $filter

ちなみに、Get-WinEventを引数なしで実行するとWIndows8ではなぜか以下のようなエラーの嵐に見舞われる。(2012ではOK)
image

シンタックスでエラーになっているわけではなく、取得したデータをうまく表示できない感じだ。
こういう非互換はちょっと困りもの。


XPath形式でのフィルターの書き方はヘルプを見るとこんな感じ。
 image

XMLの階層を[]で区切って書くようだ。
なんかちょっと面倒くさそう。
XPathはほとんど使ったことが無いのでちょっと苦労したが、とりあえず以下のような感じでいけそうだ。

$filter = @"
  Event/System/EventID='4624' and
  Event/EventData[
    Data[@Name='LogonType']='2' or
    Data[@Name='LogonType']='3' or
    Data[@Name='LogonType']='10' or
    Data[@Name='LogonType']='11'
  ]
"@

これは、イベントIDが’4624’(ログオン)で、かつログオンタイプが順に、対話、ネットワーク、リモート、キャッシュされた対話ログオン。

キャッシュされた対話ログオンというのはドメインコントローラと通信ができないときとかに使われるのかと思っていたが、結果を見ると頻繁に使われている。(というかほとんどがキャッシュログオンだ)

取得できたオブジェクトを見てみると、
image

となっていて、イベント固有の情報は「Message」というプロパティに文字列で入っている。
そこで、こいつを文字列解析して項目を取り出そうとしたのだが、改行付の文字列なのでシングルラインモードで正規表現を使ってかなりの力技チックになってしまった。
っで、取得してから気づいたのだがメンバー一覧をよく見ると「ToXml」メソッドがちゃんと用意されている。

気を取り直して、XMLに変換してから取り出してみる。
ここら辺は、それこそPowerShell: ◆イベントログを取得(抽出)する(Get-WinEvent)でやっているので問題なし、と思ったのだが、どうにもこうにもうまくいかない。(クエリしても何も返ってこない)

以前サンプルでやったのとどこが違うのか少しずつ試していくと、ルートノードについているネームスペースが気に入らないらしい。

<Event xmlns='http://schemas.microsoft.com/win/2004/08/events/event'>

このネームスペースを消すとうまく取得できた。
都度消してやろうかとも思ったが、さすがに本質的解決じゃないので色々調べたところ、どうやらネームスペース付きのXMLの場合はXPathの方でもネームスペースを指定する必要があるっぽい。
メソッドの定義を見てみるとXPathに加えて「XmlNamespaceManager」なるものを指定するオーバーロードがあるのでこれを指定すればよいのだろう。
image

対象のXML自体にNameTableというプロパティがあり、それをコンストラクタ引数に指定すると「XmlNamespaceManager」は作れる。

出来た「XmlNamespaceManager」にAddNamespaceメソッドで名前空間とそのプリフィックスを追加。

$secLog | %{
  $secLogXML = [xml]$_.ToXML()
  $ns = New-Object Xml.XmlNamespaceManager $secLogXML.NameTable
  $ns.AddNamespace(
    "e", http://schemas.microsoft.com/win/2004/08/events/event)
  $prop = $secLogXML.selectSinglenode($filterStr ,$ns).Value

あとは、XPathのそれぞれのノードに名前空間プリフィックス(上記の場合はe:)をつけてあげる。

$filterStr  = "e:Event/e:EventData/e:Data[@Name='TargetUserName']/text()"

ちょっと面倒?
どこかでデフォルトの名前空間を指定して、プリフィックスがつかなければデフォルトの名前空間を使う、ってな方法がないのかしらん?と思うが・・・。

とりあえず、以下の印をつけた項目を取得する。
image

/Event/Systemと、/Event/EventData用にそれぞれ以下のようなXPathを用意して、

$systemFiltertmpl = "e:Event/e:System/e:{0}/text()"
$eventDataFiltertmpl = "e:Event/e:EventData/e:Data[@Name='{0}']/text()"

可変の要素(または属性)部分を以下のような配列で作っておき、。

$systemProps = @('Computer')
$eventDataProps = @('TargetUserName','LogonType','TargetDomainName')

入れ替えながら使用すれば良い。


最後に、面倒くさそうだったので後回しにしていたが、ここでクエリ(XPath)に日付の抽出条件を付ける。
イベントログのクエリに使う時間はUTC時刻を使うのだとか。
UTC時刻はConvertTimeToUtcメソッドで変換できる。

[System.TimeZoneInfo]::ConvertTimeToUtc()

最終的にクエリに指定するには文字列変換が必要で、ToString(“u”)でUTCフォーマットになるらしいのだが、結果を見ると微妙に期待したものと違っている。

PS>[System.TimeZoneInfo]::ConvertTimeToUtc((Get-Date)).ToString("u")
2012-09-28 15:38:44Z

クエリーで使う形式は日付と時刻の間に”T”が入っている。

2012-09-28T15:38:44Z

どうしてこう微妙に違うのか良く判らないが、仕方がないので直接指定で変換。

HogeHoge.ToString("yyyy-MM-ddTHH:mm:ssZ")

 

以上の結果を纏めたのが以下のスクリプト。
20日前から20日間のログを対象にしている。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062

$fromDay = -20    #前日:-1
$days = 20      #開始からの日数
$startTime =
 
 
[DateTime](Get-Date).AddDays($fromDay).ToString("yyyy/MM/dd 00:00:00")
$startUtcTime =
 
 
[System.TimeZoneInfo]::
ConvertTimeToUtc(
   
$startTime).ToString("yyyy-MM-ddTHH:mm:ssZ")
$endUtcTime =
 
 
[System.TimeZoneInfo]::
ConvertTimeToUtc(
   
$startTime.AddDays($days)).ToString("yyyy-MM-ddTHH:mm:ssZ"
)
   

#表示対象のプロパティ
$systemProps = @('Computer')
$eventDataProps = @('TargetUserName','LogonType','TargetDomainName')
#表示対象のプロパティ用のXPath
$systemFiltertmpl = "e:Event/e:System/e:{0}/text()"
$eventDataFiltertmpl = "e:Event/e:EventData/e:Data[@Name='{0}']/text()"

$filter = 
@"
  Event/System/EventID='4624' and
  Event/EventData[
    Data[@Name='LogonType']='2' or
    Data[@Name='LogonType']='3' or
    Data[@Name='LogonType']='10' or
    Data[@Name='LogonType']='11'
  ] and
  Event/System/TimeCreated[@SystemTime>='$startUtcTime'] and
  Event/System/TimeCreated[@SystemTime<'$endUtcTime']
"@
 

$secLog = 
$null
$secLog
 = Get-WinEvent -logname security  -FilterXPath 
$filter

$addProp
 =
 {
 
param($name,$shortFormat
)
 
$prop = $secLogXML.selectSinglenode($filterStr ,$ns).
Value
 
if($shortFormat -and $name -eq "Computer"){$prop = ($prop -Split '\.')[0]
}
 
Add-Member -Name $name -type NoteProperty -Value $prop -Input $eventObj
 
}


$secLog | %
{
 
$secLogXML = [xml]$_.
ToXML()
 
$ns = New-Object Xml.XmlNamespaceManager $secLogXML.
NameTable
 
$ns.
AddNamespace(
   
"e", "http://schemas.microsoft.com/win/2004/08/events/event"
)
 
$eventObj = $_
 
   
 
$systemProps | %
{
   
$filterStr = $systemFiltertmpl -f $_
    & $addProp $_ $true
  }

 
$eventDataProps | %
{
   
$filterStr = $eventDataFiltertmpl -f $_
    & $addProp $_
  }
 
return $eventObj
} | ft -Property ($systemProps+$eventDataProps) -AutoSize

#ft @{name="pcName";expression={($_.Computer -split '\.')[0]}},
# TargetDomainName,TargetUserName,LogonType,
# TimeCreated,SubjectUserName -AutoSize

結果

image

あとは、こいつを他のマシンからも取得できるようにすれば完成だが、Get-WinEventにComputerNameパラメータを指定して、適切なCredentialを指定するだけなので省略。

管理されるサーバー側でファイアウォールの例外設定が必要になるが、Powershellとは特に関係ないのでこれも省略。

2012年9月27日木曜日

◆データベースへアクセスする関数

データベースアクセスはこれまで何度もやっているが、以下で関数が提供されていたので転載しておく。
Windows PowerShell: Windows PowerShell からデータベースにアクセスする

Powershellの第1人者が提供する関数なので使って損はないだろうと思い、プロファイルに入れておくことにした。

抽出用と更新用の関数2つ。

接続文字列のサンプルだけをちょっと追加している。
Get-DatabaseDataU –? で簡易的に接続文字列サンプルを表示させている。

また、この記事で紹介されている接続文字列のサンプルサイトはリンクしておくと役に立ちそうだ。

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063

$comment=@'
  接続文字列サンプル
  (1) "Data Source=myServerAddress;Initial Catalog=myDataBase;User Id=myUsername;Password=myPassword;"
  (2) "Data Source=myServerAddress;Initial Catalog=myDataBase;Integrated Security=SSPI;"
'@


function Get-DatabaseDataU
 {
   
param
 (
       
[string]$connectionString,
        [string]$query,
        [switch]$isSQLServer,
        [switch]${??}
    )
 
if(${??}) {$comment;return
}

   
if ($isSQLServer
) {
       
Write-Verbose 'in SQL Server mode'
        $connection = New-Object -TypeName System.Data.SqlClient.SqlConnection
    } else
 {
       
Write-Verbose 'in OleDB mode'
        $connection = New-Object -TypeName System.Data.OleDb.OleDbConnection
    }
   
$connection.ConnectionString = $connectionString
    $command = $connection.
CreateCommand()
   
$command.CommandText = $query
    if ($isSQLServer
) {
       
$adapter = New-Object -TypeName System.Data.SqlClient.SqlDataAdapter $command
    } else
 {
       
$adapter = New-Object -TypeName System.Data.OleDb.OleDbDataAdapter $command
    }
   
$dataset = New-Object -TypeName System.Data.DataSet
    $adapter.Fill($dataset
)
   
$dataset.Tables[0]
}

function Invoke-DatabaseQueryU
 {
   
param
 (
       
[string]$connectionString,
        [string]$query,
        [switch]$isSQLServer,
        [switch]${??}

    )
 
if(${??}) {$comment;return
}

   
if ($isSQLServer
) {
       
Write-Verbose 'in SQL Server mode'
        $connection = New-Object -TypeName System.Data.SqlClient.SqlConnection
    } else
 {
       
Write-Verbose 'in OleDB mode'
        $connection = New-Object -TypeName System.Data.OleDb.OleDbConnection
    }
   
$connection.ConnectionString = $connectionString
    $command = $connection.
CreateCommand()
   
$command.CommandText = $query
    $connection.
Open()
   
$command.
ExecuteNonQuery()
   
$connection.close()
}

2012年9月26日水曜日

◆スリープ

Windows7の時はキーボード操作で簡単にスリープできたのだが、Windows8はちょっとキー操作が多い。
「Win + D」「Alt + F4」「↑」「Enter」
とかって感じ?(↑の回数が常に一定なのか?)

複合キーが多く頻繁に使うにはちょっと面倒。

ということで、コマンドを作ってショートカットに割り当てておくことにした。

こんな感じでできそうだ。

Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Application]::SetSuspendState(
    [System.Windows.Forms.PowerState]::Suspend,$false,$false)

後はショートカットを作り、以下のようなコマンドを指定。

C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -file "D:\Documents\Luncher\Sleep.ps1" -WindowStyle Hidden

クイックランチ(ツールバー)に突っ込んでおけばワンクリックでスリープできるようになる。
image

2012年9月21日金曜日

◆ActiveDirectory グループに所属しているか判定する

あるユーザーが特定のグループに所属しているか判定したい。

どうするんでしょうね・・・。

単純にユーザーが所属しているグループなら取れそうな気もするけど、グループが入れ子になっていると再帰して親まで辿らないといけない。

ちょっと面倒な感じ。

逆にグループに所属しているメンバーは「Get-ADGroupMember」に「Recursive」パラメータがあってすべてのリーフを取得できる。

こいつを保持して置いて「-contain」で判定するのが簡単かな。

もっとダイレクトな方法があるのかしらん・・・。

--

whoami /groups /fo csv | ConvertFrom-Csv

こんな感じでグループ名を取ってくれば良いのだろうか。