<# .Synopsis Retrieve and populate a custom WMI class with information regarding users and groups inside local Groups .Description Likely run using a Configuration Item using Microsoft ConfigMgr (ECM), on a recurring schedule. The WMI information will be retrieved using a custom Inventory extension .Notes Original Author: Sherry Kissinger 2021-08-08 Modification: Sherry Kissinger 2021-12-07 Per Paolo Bragigni, https://docs.microsoft.com/en-us/answers/questions/495129/sccm-query-for-local-admin.html, when using other than en-US localization, the "local user enabled or disabled" would not correctly be detected. This modification is to change that behavior, to make it more language agnostic. Account = The Name of the user or group within the group (Name) Domain = Domain (if a Active Directory Domain group or user), the local computername if local account or group Category = User, Group, SystemAccount (aka, "Interactive"), or if a Microsoft Account Type = Local vs. Domain or unknown Name = The Name of the Group in which the Account was listed. Enabled = *if* it's a local username, is that local username enabled or disabled. Suggestions for customization: 1) change $LoggingEnabled from $True to $false, depending upon your company's logging standards. 2) Two possible suggestions for where to put that log file (if you set loggingEnabled to $true), either %temp% (usually %windir%\temp, if running via CM ConfigItem), or, the local client's CM log folder. #> #Function to Create a a empty class to populate Function New-WMIClassHC { if (Get-WMIObject -List -NameSpace "root\cimv2" | Where-Object {$_.Name -eq $Class}) { Write-Verbose "WMI Class $Class Exists" } else { Write-Verbose "Create WMI Class '$Class'" $NewClass = New-Object System.Management.ManagementClass ("root\cimv2", [String]::Empty, $Null); $NewClass['__CLASS'] = $Class $NewClass.Properties.Add("Account",[System.Management.CimType]::String, $false) $NewClass.Properties["Account"].Qualifiers.Add("Key", $true) $NewClass.Properties.Add("Domain",[System.Management.CimType]::String, $false) $NewClass.Properties["Domain"].Qualifiers.Add("Key", $true) $NewClass.Properties.Add("Category",[System.Management.CimType]::String, $false) $NewClass.Properties.Add("Type",[System.Management.CimType]::String, $false) $NewClass.Properties.Add("Name",[System.Management.CimType]::String, $false) $NewClass.Properties["Name"].Qualifiers.Add("Key", $true) $NewClass.Properties.Add("Enabled",[System.Management.CimType]::string, $false) $NewClass.Properties.Add("PasswordLastSet",[System.Management.CimType]::string, $false) $NewClass.Properties.Add("ScriptLastRan",[System.Management.CimType]::String, $false) $NewClass.Put() #| Out-Null } Write-Verbose "End of trying to create an empty $Class to populate later" } #Function for writing a log in cmtrace format function Write-log { [CmdletBinding()] Param( [parameter(Mandatory=$true)] [String]$Path, [parameter(Mandatory=$true)] [String]$Message, [parameter(Mandatory=$true)] [String]$Component, [Parameter(Mandatory=$true)] [ValidateSet("Info", "Warning", "Error")] [String]$Type ) switch ($Type) { "Info" { [int]$Type = 1 } "Warning" { [int]$Type = 2 } "Error" { [int]$Type = 3 } } if ($LogEnabled -eq $true) { # Create a log entry $Content = "" +` "" # Write the line to the log file Add-Content -Path $Path -Value $Content } } #Set up some variables [String]$ErrorActionPreference = "SilentlyContinue" [String]$VerbosePreference = "Continue" #Logging enabled at all ? $true / $false $LogEnabled = $true #Log Location, if run as SYSTEM, this will most likely be %windir%\temp $LogFilePath = $env:TEMP + "\CMLocalGroupMembers.log" #If you want to log into the CM Client log path, comment out the $env:temp one above, and uncomment this one instead: #$LogFilePath = (Get-ItemProperty -path HKLM:\Software\Microsoft\CCM\Logging\@Global).LogDirectory + "\CMLocalGroupMembers.log" Write-Verbose $LogFilePath #DeleteExisting Log File if one exists if (Test-Path $LogFilePath) {Remove-Item $LogFilePath -Force | Out-Null} #Define the custom class we'll empty and/or create $Class = "CM_LocalGroupMembers" $ScriptRanDate = Get-Date $ScriptRan = [System.Management.ManagementDateTimeConverter]::ToDmtfDateTime($ScriptRanDate) $LogText = "Checking for MSOnline module..." Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info $MSOnModule = Get-Module -Name "MSOnline" -ListAvailable #Check for MSOnline Module and install if not found if ($MSOnModule -eq $null) { $LogText = "MSOnline PowerShell module not found, attempting to install..." Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info try { Import-Module MSOnline $LogText = "Module exists" Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info } catch { $LogText = "Module does not exist. Script cannot continue" Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info Exit } } #If this is a Domain Controller, we don't even want to try--theoretically, there would be 'no local accounts' to find, # But... let's not take a chance. If this is a DC (domain role of 4 or 5 for primary or backup), just Bail and do nothing. if ( (Get-CimInstance -ClassName win32_computerSystem -Namespace 'root\cimv2').DomainRole -in (4,5)) { write-verbose "Domain Controller, do nothing" $LogText = "Domain Controller, BAIL BAIL BAIL!" Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info } else { #Not a Domain Controller, OK to continue #Get all the local groups according to the powershell CMDlet of Get-LocalGroup, we'll use this later. $Groups = Get-LocalGroup | select name #Stage the WMI Class in root\cimv2, we want it nice and clean and empty. Write-Verbose "Delete the '$Class' in root\cimv2 so we can make a new empty one." $LogText = "Delete the '$Class' in root\cimv2 so we can make a new empty one." Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info Remove-WMIObject -Namespace "root\cimv2" -class $Class -ErrorAction 'SilentlyContinue' Write-Verbose "Create a new empty '$Class' to populate later" $LogText = "Create a new empty '$Class' to populate later" Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info New-WMIClassHC #For each of the $Groups we found locally, let's enumerage any objects inside those groups, #and see what we can determine about them, and populate WMI, that class we just made, with info for later inventory. ForEach ($TheName in $Groups) { $Values1 = Get-LocalGroupMember -name $TheName.Name | select ObjectClass, Name, PrincipalSource ForEach ($ReturnedValues in $Values1) { write-verbose $TheName.Name $LogText = $TheName.Name Write-Log -Path $LogFilePath -Message ("Group: " + $LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info write-verbose $ReturnedValues $LogText = $ReturnedValues.Name.Split("\")[1] Write-Log -Path $LogFilePath -Message ("Account or nested group Inside: " + $LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info $LogText = $ReturnedValues.Name.Split("\")[0] Write-Log -Path $LogFilePath -Message ("Domain: " + $LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info $LogText = $ReturnedValues.ObjectClass Write-Log -Path $LogFilePath -Message ("Category: " + $LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info $LogText = $ReturnedValues.PrincipalSource Write-Log -Path $LogFilePath -Message ("Type: " + $LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info #Check if a Local user account is enabled or not. Make it $null to start with; just to be sure it's clean and empty. $Enabled = $null $Enabled = Get-LocalUser | where-object {$_.Name -eq $ReturnedValues.Name.Split("\")[1]} | Select name, enabled, PasswordLastSet if ($Enabled) { Write-Verbose "Let us try to determine if the local user account is enabled or disabled locally; There will only be something to say if it is a local account" $LogText = "Let us try to determine if the local user account is enabled or disabled locally; There will only be something to say if it is a local account" Write-Log -Path $LogFilePath -Message ($LogText | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info Write-Log -Path $LogFilePath -Message ($ReturnedValues.Name.Split("\")[1] + " is " + $Enabled.Enabled | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info } Write-Log -Path $LogFilePath -Message ("-----------------------" | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info #ok, we have everything we think we need, to populate that $class we made in root\cimv2, let's populate that, for this one value, in this group Set-WmiInstance -Namespace root\cimv2 -class $Class -arguments @{ Account = $ReturnedValues.Name.Split("\")[1] Domain = $ReturnedValues.Name.Split("\")[0] Category = $ReturnedValues.ObjectClass Type = $ReturnedValues.PrincipalSource Name = $TheName.Name ScriptLastRan=$ScriptRan Enabled = $Enabled.Enabled PasswordLastSet = $enabled.PasswordLastSet } | Out-Null } #End of Foreach for names/groups within a local group } #End of forEach for each local group } #End of checking if it's a Domain Controller #and... we're done here. Write-Log -Path $LogFilePath -Message ("Done" | Out-String) -Component $MyInvocation.MyCommand.Name -Type Info write-host "Done"