ADグループのユーザーメンバシップに関するレポートを作成します。Powershellスクリプトの作成における4つの問題



Scripting GuruのBillITwartは、 WindowsITProに関する彼の記事で 、Active Directoryグループのユーザーメンバーシップを表示するPowershellスクリプトを作成する際に直面した問題について説明しています 。 すべてが正常に機能するように、4つの改善を行う必要がありました。 Billがグループメンバーシップの結論をどのように実装したかを知ることができ、Powershellスクリプト自体をダウンロードしてダウンロードできます。



スクリプトの最終バージョンへのリンク。

www.windowsitpro.com/content/content/141463/141463.zip



私は数を失い、フォーラムで「ADドメインのすべてのユーザーとそのグループメンバーシップに関する情報を取得する方法を誰もが知っていますか?」という質問に何度も会いました。 監査員と情報セキュリティコンサルタントも、組織内のActive Directoryインフラストラクチャ(環境)を評価するときに同様の質問をします。 この問題は非常に緊急であるため、このタスクを簡素化するPowerShellスクリプトを作成することにしました。

最初は、似たようなスクリプトを書くのは些細なことだと思っていましたが、4つの障害が私のやり方で出くわし、仕事が複雑になりました。 これらの問題については後ほど説明しますが、最初に、ADを検索するときにPowershellでMicrosoft.NETを使用する基本についてお話したいと思います。



AD検索に.NETを使用する





.NETを使用してADを検索すると、PowerShellのタイプアクセラレータを使用してオブジェクトを検索できます。 (タイプアクセラレータは.NETクラスの短縮名です)。 たとえば、このドメインのすべてのユーザーをリストするには、次のコマンドを入力します。



PS C:\> $searcher = "(&(objectCategory=user)(objectClass=user))" PS C:\> $searcher.FindAll()
      
      







[ADSISearcher]は、.NET System.DirectoryServices.DirectorySearcher .NETオブジェクトの型アクセラレータです。 このタイプのアクセラレータに続く行は、このオブジェクトのSearchFilterプロパティを設定してすべてのユーザーオブジェクトを検索し、 FindAllメソッドが検索を開始します。 出力で、 System.DirectoryServices.SearchResultオブジェクトのリストを取得します。

次に、ユーザーが属しているグループを判別します。 調べるには、 SearchResultオブジェクトのPropertiesコレクションを使用して、memberofなどのオブジェクト属性を取得します。 前の例の$ searcher変数を使用して、 FindAllメソッドの代わりにFindOneメソッドを使用して、単一の結果を抽出し、グループのユーザーメンバーシップを推測できます。



 PS C:\> $result = $searcher.FindOne() PS C:\> $result.Properties["memberof"] | sort-object
      
      







最初のコマンドは検索フィルターに一致する最初のユーザーを検索し、2番目のコマンドはユーザーが所属するグループをリストします。

ただし、このリストをよく見ると、重要な詳細が欠落していることがわかります。ユーザーのプライマリグループはmemberof属性に含まれていません。 グループ(メイングループを含む)の完全なリストを取得したいので、最初の問題につながります。



問題#1:メインユーザーグループを見つける方法





memberof属性からメイングループを除外する回避策があります。 この記事では、 support.microsoft.com / kb / 321360で説明されています。次の手順実行します。

  1. (LDAPプロバイダーの代わりに)WinNTプロバイダーを使用してユーザーオブジェクトに接続(接続)します。
  2. ユーザー属性primaryGroupIDを取得します。
  3. メイングループを含むWinNTプロバイダーを使用してユーザーグループ名を取得します。
  4. sAMAccountName属性を使用してこれらのグループのADを検索します。
  5. primaryGroupToken属性がユーザー属性primaryGroupIDと一致するグループを見つけます。


この「回避策」の問題は、ユーザーオブジェクトに接続するためにWinNTプロバイダースクリプトが必要なことです。 つまり、スクリプトは識別されたユーザー名(CN = Ken Myer、OU = Marketing、DC = fabrikam、DC = com)をWinNTプロバイダーが使用できる形式(WinNT:// FABRIKAM / kenmyerなど)に変換する必要がありますユーザー)。



問題#2:ある名前形式から別の形式への変換





NameTranslateオブジェクトは、ADオブジェクトの名前を変数(代替)形式に変換するIADsNameTranslateインターフェイスを使用するCOM(ActiveX)オブジェクトです。 NameTranslateオブジェクトを使用するには、オブジェクトを作成し、そのInitメソッドを呼び出して初期化します。 たとえば、リスト1は、 NameTranslateが作成および初期化するスクリプトのVBScriptコードを示しています。



リスト1:VBScriptでNameTranslateを作成および初期化する



 Const ADS_NAME_INITTYPE_GC = 3 Dim NameTranslate Set NameTranslate = CreateObject("NameTranslate") NameTranslate.Init ADS_NAME_INITTYPE_GC, vbNull
      
      







ただし、図1に示すように、PowerShellではNameTranslateオブジェクト期待どおりに機能しません。





図1:PowerShellでのNameTranslateの予期しない動作



問題は、 NameTranslateオブジェクトに 、.NET(およびPowerShell)がCOMオブジェクトへの簡単なアクセスを提供するために使用するタイプライブラリないことです。 しかし、幸いなことに、この問題は回避できます。.NETInvokeMemberメソッドを使用すると、PowerShellでプロパティを取得または設定したり、タイプライブラリにないCOMオブジェクトからメソッドを呼び出したりできます。 リスト2は、表1のVBScriptコードに相当するPowershellを示しています



リスト2:PowerShellでNameTranslateオブジェクトを作成および初期化する



 $ADS_NAME_INITTYPE_GC = 3 $NameTranslate = new-object -comobject NameTranslate [Void] $NameTranslate.GetType().InvokeMember("Init", "InvokeMethod", $NULL, $NameTranslate, ($ADS_NAME_INITTYPE_GC, $NULL))
      
      







名前に関連する別の問題をスクリプトで解決したかったのです。 ユーザーADのmemberof属性には、ユーザーがメンバーである識別名のリストが含まれていますが、代わりに各グループのsamaccountname属性を取得したかったです。 このスクリプトは、 NameTranslateオブジェクトを使用してこの問題に対処します。



問題#3:特殊文字の処理





識別名に関するMicrosoftのドキュメントでは、個々の文字を正しく解釈するには(たとえば、「\」の接頭辞を付けて)省略しなければならないことが記載されています( この記事の詳細)。 幸いなことに、Pathname COMオブジェクトはこの機能を提供します。 スクリプトはPathnameオブジェクトを使用して、特殊文字を含むこれらの識別名をスキップします。 NameTranslateオブジェクトと同様に、このオブジェクトにはタイプライブラリがないため、 Pathnameオブジェクトには.NET InvokeMemberメソッドも必要です。



問題4:生産性の向上





問題#1(メインユーザーグループの検索方法)を見ると、回避策にはユーザーグループの検索が必要であることがわかります。 複数のアカウントに対してこの手順を実行すると、どのように最適ではないかを理解できます。 問題#2(名前の形式から別の形式への変換)を検討しながら言及したmemberof属性内の各グループのsamaccountname属性を取得することも、最適ではなく、時間がかかります。 この問題を解決するために、スクリプトは結果をハッシュしてパフォーマンスを向上させる2つのグローバルハッシュテーブルを使用します。



Get-UsersAndGroups.ps1





Get-UsersAndGroups.ps1は、ユーザーとそのグループメンバーシップのリストを表示する既製のPowershellスクリプトです。 スクリプトのコマンドライン構文は次のとおりです。

 Get-UsersAndGroups [[-SearchLocation] <String[]>] [-SearchScope <String>]
      
      







-SearchLocationパラメーターは、ユーザーアカウントの1つ以上の識別名です。 識別名にはカンマ(、)が含まれているため、PowerShellが配列として解釈しないように、識別名ごとに括弧(単一または二重)で囲む必要があります。 パラメーター名-SearchLocationはオプションです。 スクリプトはパイプライン入力も受け入れます。 パイプラインの各値は、検索する識別名である必要があります。

-SearchScopeは、AD検索の可能なスケールを示します。 この値は次の3つのうちの1つである必要があります。 ベース -検索はベースオブジェクトに制限され、使用されません。 OneLevel-ベースオブジェクトとSubtreeの最も近い子オブジェクトの検索-バックライトによる検索。 この値が指定されていない場合、デフォルトでサブツリーが使用されます。 特定の組織単位(OU)が必要であるが、その中にネストされているOUがない場合は、 -SearchScope OneLevelを使用します。 スクリプトは、表1にリストされているプロパティを含むオブジェクトを表示します。







4つの課題を克服する





スクリプトは上記の問題を解決します。





グループとユーザーの監査を簡素化する



Get-UsersAndGroups.ps1スクリプトの記述は、一見すると単純ではないように思えましたが、簡単ではありませんでした。 最も単純なスクリプトアプリケーションは次のコマンドです。



 PS C:\> Get-UsersAndGroups | Export-CSV Report.csv -NoTypeInformation
      
      







特定のドメインのユーザーとグループの完全なリストを含む.csvファイルを作成します。 武器の名前はそのようなスクリプトです。グループやユーザーごとにレポートをすばやく簡単に作成できます。



もう一度、リンクをスクリプトの最終バージョンに複製します。

www.windowsitpro.com/content/content/141463/141463.zip



スクリプト自体:



 # Get-UsersAndGroups.ps1 # Written by Bill Stewart (bstewart@iname.com) #requires -version 2 <# .SYNOPSIS Retreves users, and group membership for each user, from Active Directory. .DESCRIPTION Retreves users, and group membership for each user, from Active Directory. Note that each user's primary group is included in the output, and caching is used to improve performance. .PARAMETER SearchLocation Distinnguished name (DN) of where to begin searching for user accounts; eg "OU=Information Technology,DC=fabrikam,DC=com". If you omit this parameter, the default is the current domain (eg, "DC=fabrikam,DC=com"). .PARAMETER SearchScope Specifies the scope for the Active Directory search. Must be one of the following values: Base (Limit the search to the base object, not used), OneLevel (Searches the immediate child objects of the base object), or Subtree (Searches the whole subtree, including the base object and all its child objects). The default value is Subtree. To search only a location but not its children, specify OneLevel. .OUTPUTS PSObjects containing the following properties: DN The user's distinguished name CN The user's common name UserName The user's logon name Disabled True if the user is disabled; false otherwise Group The groups the user is a member of (one object per group) #> [CmdletBinding()] param( [parameter(Position=0,ValueFromPipeline=$TRUE)] [String[]] $SearchLocation="", [String][ValidateSet("Base","OneLevel","Subtree")] $SearchScope="Subtree" ) begin { $ADS_NAME_INITTYPE_GC = 3 $ADS_SETTYPE_DN = 4 $ADS_NAME_TYPE_1779 = 1 $ADS_NAME_TYPE_NT4 = 3 $ADS_UF_ACCOUNTDISABLE = 2 # Assume pipeline input if SearchLocation is unbound and doesn't exist. $PIPELINEINPUT = (-not $PSBOUNDPARAMETERS.ContainsKey("SearchLocation")) -and (-not $SearchLocation) # If -SearchLocation is a single-element array containing an emty string # (ie, -SearchLocation not specified and no pipeline), then populate with # distinguished name of current domain. In this case, input is not coming # from the pipeline. if (($SearchLocation.Count -eq 1) -and ($SearchLocation[0] -eq "")) { try { $SearchLocation[0] = ([ADSI] "").distinguishedname[0] } catch [System.Management.Automation.RuntimeException] { throw "Unable to retrieve the distinguished name for the current domain." } $PIPELINEINPUT = $FALSE } # These hash tables cache primary groups and group names for performance. $PrimaryGroups = @{} $Groups = @{} # Create and initialize a NameTranslate object. If it fails, throw an error. $NameTranslate = new-object -comobject "NameTranslate" try { [Void] $NameTranslate.GetType().InvokeMember("Init", "InvokeMethod", $NULL, $NameTranslate, ($ADS_NAME_INITTYPE_GC, $NULL)) } catch [System.Management.Automation.MethodInvocationException] { throw $_ } # Create a Pathname object. $Pathname = new-object -comobject "Pathname" # Returns the last two elements of the DN using the Pathname object. function get-rootname([String] $dn) { [Void] $Pathname.GetType().InvokeMember("Set", "InvokeMethod", $NULL, $Pathname, ($dn, $ADS_SETTYPE_DN)) $numElements = $Pathname.GetType().InvokeMember("GetNumElements", "InvokeMethod", $NULL, $Pathname, $NULL) $rootName = "" ($numElements - 2)..($numElements - 1) | foreach-object { $element = $Pathname.GetType().InvokeMember("GetElement", "InvokeMethod", $NULL, $Pathname, $_) if ($rootName -eq "") { $rootName = $element } else { $rootName += ",$element" } } $rootName } # Returns an "escaped" copy of the specified DN using the Pathname object. function get-escaped([String] $dn) { [Void] $Pathname.GetType().InvokeMember("Set", "InvokeMethod", $NULL, $Pathname, ($dn, $ADS_SETTYPE_DN)) $numElements = $Pathname.GetType().InvokeMember("GetNumElements", "InvokeMethod", $NULL, $Pathname, $NULL) $escapedDN = "" for ($n = 0; $n -lt $numElements; $n++) { $element = $Pathname.GetType().InvokeMember("GetElement", "InvokeMethod", $NULL, $Pathname, $n) $escapedElement = $Pathname.GetType().InvokeMember("GetEscapedElement", "InvokeMethod", $NULL, $Pathname, (0, $element)) if ($escapedDN -eq "") { $escapedDN = $escapedElement } else { $escapedDN += ",$escapedElement" } } $escapedDN } # Return the primary group name for a user. Algorithm taken from # http://support.microsoft.com/kb/321360 function get-primarygroupname([String] $dn) { # Pass DN of user to NameTranslate object. [Void] $NameTranslate.GetType().InvokeMember("Set", "InvokeMethod", $NULL, $NameTranslate, ($ADS_NAME_TYPE_1779, $dn)) # Get NT4-style name of user from NameTranslate object. $nt4Name = $NameTranslate.GetType().InvokeMember("Get", "InvokeMethod", $NULL, $NameTranslate, $ADS_NAME_TYPE_NT4) # Bind to user using ADSI's WinNT provider and get primary group ID. $user = [ADSI] "WinNT://$($nt4Name.Replace('\', '/')),User" $primaryGroupID = $user.primaryGroupID[0] # Retrieve user's groups (primary group is included using WinNT). $groupNames = $user.Groups() | foreach-object { $_.GetType().InvokeMember("Name", "GetProperty", $NULL, $_, $NULL) } # Query string is sAMAccountName attribute for each group. $queryFilter = "(|" $groupNames | foreach-object { $queryFilter += "(sAMAccountName=$($_))" } $queryFilter += ")" # Build a DirectorySearcher object. $searchRootDN = get-escaped (get-rootname $dn) $searcher = [ADSISearcher] $queryFilter $searcher.SearchRoot = [ADSI] "LDAP://$searchRootDN" $searcher.PageSize = 128 $searcher.SearchScope = "Subtree" [Void] $searcher.PropertiesToLoad.Add("samaccountname") [Void] $searcher.PropertiesToLoad.Add("primarygrouptoken") # Find the group whose primaryGroupToken attribute matches user's # primaryGroupID attribute. foreach ($searchResult in $searcher.FindAll()) { $properties = $searchResult.Properties if ($properties["primarygrouptoken"][0] -eq $primaryGroupID) { $groupName = $properties["samaccountname"][0] return $groupName } } } # Return a DN's sAMAccount name based on the distinguished name. function get-samaccountname([String] $dn) { # Pass DN of group to NameTranslate object. [Void] $NameTranslate.GetType().InvokeMember("Set", "InvokeMethod", $NULL, $NameTranslate, ($ADS_NAME_TYPE_1779, $dn)) # Return the NT4-style name of the group without the domain name. $nt4Name = $NameTranslate.GetType().InvokeMember("Get", "InvokeMethod", $NULL, $NameTranslate, $ADS_NAME_TYPE_NT4) $nt4Name.Substring($nt4Name.IndexOf("\") + 1) } function get-usersandgroups2($location) { # Finds user objects. $searcher = [ADSISearcher] "(&(objectCategory=User)(objectClass=User))" $searcher.SearchRoot = [ADSI] "LDAP://$(get-escaped $location)" # Setting the PageSize property prevents limiting of search results. $searcher.PageSize = 128 $searcher.SearchScope = $SearchScope # Specify which attributes to retrieve ([Void] prevents output). [Void] $searcher.PropertiesToLoad.Add("distinguishedname") [Void] $searcher.PropertiesToLoad.Add("cn") [Void] $searcher.PropertiesToLoad.Add("samaccountname") [Void] $searcher.PropertiesToLoad.Add("useraccountcontrol") [Void] $searcher.PropertiesToLoad.Add("primarygroupid") [Void] $searcher.PropertiesToLoad.Add("memberof") # Sort results by CN attribute. $searcher.Sort = new-object System.DirectoryServices.SortOption $searcher.Sort.PropertyName = "cn" foreach ($searchResult in $searcher.FindAll()) { $properties = $searchResult.Properties $dn = $properties["distinguishedname"][0] write-progress "Get-UsersAndGroups" "Searching $location" -currentoperation $dn $cn = $properties["cn"][0] $userName = $properties["samaccountname"][0] $disabled = ($properties["useraccountcontrol"][0] -band $ADS_UF_ACCOUNTDISABLE) -ne 0 # Create an ArrayList containing user's group memberships. $memberOf = new-object System.Collections.ArrayList $primaryGroupID = $properties["primarygroupid"][0] # If primary group is already cached, add the name to the array; # otherwise, find out the primary group name and cache it. if ($PrimaryGroups.ContainsKey($primaryGroupID)) { [Void] $memberOf.Add($PrimaryGroups[$primaryGroupID]) } else { $primaryGroupName = get-primarygroupname $dn $PrimaryGroups.Add($primaryGroupID, $primaryGroupName) [Void] $memberOf.Add($primaryGroupName) } # If the user's memberOf attribute is defined, find the group names. if ($properties["memberof"]) { foreach ($groupDN in $properties["memberof"]) { # If the group name is aleady cached, add it to the array; # otherwise, find out the group name and cache it. if ($Groups.ContainsKey($groupDN)) { [Void] $memberOf.Add($Groups[$groupDN]) } else { $groupName = get-samaccountname $groupDN $Groups.Add($groupDN, $groupName) [Void] $memberOf.Add($groupName) } } } # Sort the ArrayList and output one object per group. $memberOf.Sort() foreach ($groupName in $memberOf) { $output = new-object PSObject $output | add-member NoteProperty "DN" $dn $output | add-member NoteProperty "CN" $cn $output | add-member NoteProperty "UserName" $userName $output | add-member NoteProperty "Disabled" $disabled $output | add-member NoteProperty "Group" $groupName $output } } } } process { if ($PIPELINEINPUT) { get-usersandgroups2 $_ } else { $SearchLocation | foreach-object { get-usersandgroups2 $_ } } }
      
      







WindowsITPro経由



PS NetWrix AD Change Reporterを使用して、ADの構造と変更に関するさまざまなレポートを取得できます。 このプログラムを使用すると、ADの変更に遅れずについていくことができ、同時にログを使用した面倒な作業やスクリプトによる手動の自動化が不要になります。 NetWrix Webサイトでプログラムの詳細を確認できます



All Articles