ADAppFinder.psm1
#Region '.\Private\Get-ADHostManageability.ps1' 0 function Get-ADHostManageability { [OutputType([System.Collections.Hashtable])] # $this[0] : Name = Computername; Value = [array]RemotableHostNames # $this[1] : Name = Computername; Value = [array]Non-RemotableHostNames param ( [int]$DaysInactive = 90, [switch]$Servers, [switch]$Workstations, [String]$OperatingSystemSearchString, [string]$SearchBase = $null ) if ($SearchBase) { $Time = (Get-Date).Adddays( - ($DaysInactive)) $AllComputers = Get-ADComputer -SearchBase $SearchBase -Filter { (LastLogonTimeStamp -gt $time) } -Properties Ipv4address $Comps = $allComputers.name $Params = @{} $Params.ComputerName = @() $NoRemoteAccess = @{} $NoRemoteAccess.NoRemoteAccess = @() foreach ($comp in $comps) { $testRemoting = Test-WSMan -ComputerName $comp -ErrorAction SilentlyContinue if ($null -ne $testRemoting ) { $params.ComputerName += $comp } else { $NoRemoteAccess.NoRemoteAccess += $comp } } } else { if ($Servers) { $Search = "*server*" } elseif ($Workstations) { $Search = "*windows 1*" } elseif ($OperatingSystemSearchString) { $Search = "*" + $OperatingSystemSearchString + "*" } $Time = (Get-Date).Adddays( - ($DaysInactive)) $AllComputers = Get-ADComputer -Filter { (LastLogonTimeStamp -gt $time) -and (OperatingSystem -like $search) } -Properties Ipv4address $Comps = $allComputers.name $Params = @{} $Params.ComputerName = @() $NoRemoteAccess = @{} $NoRemoteAccess.NoRemoteAccess = @() foreach ($comp in $comps) { $testRemoting = Test-WSMan -ComputerName $comp -ErrorAction SilentlyContinue if ($null -ne $testRemoting ) { $params.ComputerName += $comp } else { $NoRemoteAccess.NoRemoteAccess += $comp } } } return $Params, $NoRemoteAccess } #EndRegion '.\Private\Get-ADHostManageability.ps1' 59 #Region '.\Private\SearchForRegUninstallKey.ps1' 0 function SearchForRegUninstallKey { [OutputType([System.Management.Automation.PSCustomObject])] param( [Parameter( Mandatory = $true, Position = 0 )] [string[]]$SearchFor, [Parameter( Position = 1 )] [switch]$Wow6432Node ) $output = @() foreach ($item in $SearchFor) { $matched = @() $results = @() Get-ChildItem HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall | ` ForEach-Object { $obj = New-Object psobject Add-Member -InputObject $obj -MemberType NoteProperty -Name GUID -Value $_.pschildname Add-Member -InputObject $obj -MemberType NoteProperty -Name DisplayName -Value $_.GetValue("DisplayName") Add-Member -InputObject $obj -MemberType NoteProperty -Name DisplayVersion -Value $_.GetValue("DisplayVersion") if ($Wow6432Node) { Add-Member -InputObject $obj -MemberType NoteProperty -Name Wow6432Node? -Value "No" } $results += $obj #$output += $results }# End ForEach-Object if ($Wow6432Node) { Get-ChildItem HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall | ` ForEach-Object { $obj = New-Object psobject Add-Member -InputObject $obj -MemberType NoteProperty -Name GUID -Value $_.pschildname Add-Member -InputObject $obj -MemberType NoteProperty -Name DisplayName -Value $_.GetValue("DisplayName") Add-Member -InputObject $obj -MemberType NoteProperty -Name DisplayVersion -Value $_.GetValue("DisplayVersion") Add-Member -InputObject $obj -MemberType NoteProperty -Name Wow6432Node? -Value "Yes" $results += $obj } } $matched = $results | Sort-Object DisplayName | Where-Object { $_.DisplayName -match $item } if ($matched) { $output += $matched } else { $obj = New-Object psobject Add-Member -InputObject $obj -MemberType NoteProperty -Name GUID -Value "Missing: $item" Add-Member -InputObject $obj -MemberType NoteProperty -Name DisplayName -Value "Missing: $item" Add-Member -InputObject $obj -MemberType NoteProperty -Name DisplayVersion -Value "Missing: $item" Add-Member -InputObject $obj -MemberType NoteProperty -Name Wow6432Node? -Value "Missing: $item" $output += $obj } } # For Each return $output } # End Function #EndRegion '.\Private\SearchForRegUninstallKey.ps1' 56 #Region '.\Private\Test-IsAdmin.ps1' 0 function Test-IsAdmin { <# .SYNOPSIS Checks if the current user is an administrator on the machine. .DESCRIPTION This private function returns a Boolean value indicating whether the current user has administrator privileges on the machine. It does this by creating a new WindowsPrincipal object, passing in a WindowsIdentity object representing the current user, and then checking if that principal is in the Administrator role. .INPUTS None. .OUTPUTS Boolean. Returns True if the current user is an administrator, and False otherwise. .EXAMPLE PS C:\> Test-IsAdmin True #> # Create a new WindowsPrincipal object for the current user and check if it is in the Administrator role (New-Object Security.Principal.WindowsPrincipal ([Security.Principal.WindowsIdentity]::GetCurrent())).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } #EndRegion '.\Private\Test-IsAdmin.ps1' 23 #Region '.\Private\Write-AuditLog.ps1' 0 function Write-AuditLog { <# .SYNOPSIS Writes log messages to the console and updates the script-wide log variable. .DESCRIPTION The Write-AuditLog function writes log messages to the console based on the severity (Verbose, Warning, or Error) and updates the script-wide log variable ($script:LogString) with the log entry. You can use the Start, End, and EndFunction switches to manage the lifecycle of the logging. .INPUTS System.String You can pipe a string to the Write-AuditLog function as the Message parameter. You can also pipe an object with a Severity property as the Severity parameter. .OUTPUTS None The Write-AuditLog function doesn't output any objects to the pipeline. It writes messages to the console and updates the script-wide log variable ($script:LogString). .PARAMETER BeginFunction Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable. .PARAMETER Message The message string to log. .PARAMETER Severity The severity of the log message. Accepted values are 'Information', 'Warning', and 'Error'. Defaults to 'Information'. .PARAMETER Start Initializes the script-wide log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. .PARAMETER End Sets the message to "End Log" and exports the log to a CSV file if the OutputPath parameter is provided. .PARAMETER EndFunction Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable. .PARAMETER OutputPath The file path for exporting the log to a CSV file when using the End switch. .EXAMPLE Write-AuditLog -Message "This is a test message." Writes a test message with the default severity (Information) to the console and adds it to the log variable. .EXAMPLE Write-AuditLog -Message "This is a warning message." -Severity "Warning" Writes a warning message to the console and adds it to the log variable. .EXAMPLE Write-AuditLog -Start Initializes the log variable and sets the message to "Begin [FunctionName] Log.", where FunctionName is the name of the calling function. .EXAMPLE Write-AuditLog -BeginFunction Sets the message to "Begin [FunctionName] function log.", where FunctionName is the name of the calling function, and adds it to the log variable. .EXAMPLE Write-AuditLog -EndFunction Sets the message to "End [FunctionName] log.", where FunctionName is the name of the calling function, and adds it to the log variable. .EXAMPLE Write-AuditLog -End -OutputPath "C:\Logs\auditlog.csv" Sets the message to "End Log", adds it to the log variable, and exports the log to a CSV file. .NOTES Author: DrIOSx #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( ### [Parameter( Mandatory = $false, HelpMessage = 'Input a Message string.', Position = 0, ParameterSetName = 'Default', ValueFromPipeline = $true )] [ValidateNotNullOrEmpty()] [string]$Message, ### [Parameter( Mandatory = $false, HelpMessage = 'Information, Warning or Error.', Position = 1, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true )] [ValidateNotNullOrEmpty()] [ValidateSet('Information', 'Warning', 'Error')] [string]$Severity = 'Information', ### [Parameter( Mandatory = $false, ParameterSetName = 'End' )] [switch]$End, ### [Parameter( Mandatory = $false, ParameterSetName = 'BeginFunction' )] [switch]$BeginFunction, [Parameter( Mandatory = $false, ParameterSetName = 'EndFunction' )] [switch]$EndFunction, ### [Parameter( Mandatory = $false, ParameterSetName = 'Start' )] [switch]$Start, ### [Parameter( Mandatory = $false, ParameterSetName = 'End' )] [string]$OutputPath ) begin { $ErrorActionPreference = "SilentlyContinue" # Define variables to hold information about the command that was invoked. $ModuleName = $Script:MyInvocation.MyCommand.Name -replace '\..*' $FuncName = (Get-PSCallStack)[1].Command $ModuleVer = $MyInvocation.MyCommand.Version.ToString() # Set the error action preference to continue. $ErrorActionPreference = "Continue" } process { try { $Function = $($FuncName + '.v' + $ModuleVer) if ($Start) { $script:LogString = @() $Message = '+++ Begin Log | ' + $Function + ' |' } elseif ($BeginFunction) { $Message = '>>> Begin Function Log | ' + $Function + ' |' } $logEntry = [pscustomobject]@{ Time = ((Get-Date).ToString('yyyy-MM-dd hh:mmTss')) Module = $ModuleName PSVersion = ($PSVersionTable.PSVersion).ToString() PSEdition = ($PSVersionTable.PSEdition).ToString() IsAdmin = $(Test-IsAdmin) User = "$Env:USERDOMAIN\$Env:USERNAME" HostName = $Env:COMPUTERNAME InvokedBy = $Function Severity = $Severity Message = $Message RunID = -1 } if ($BeginFunction) { $maxRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Measure-Object -Property RunID -Maximum).Maximum if ($null -eq $maxRunID) { $maxRunID = -1 } $logEntry.RunID = $maxRunID + 1 } else { $lastRunID = ($script:LogString | Where-Object { $_.InvokedBy -eq $Function } | Select-Object -Last 1).RunID if ($null -eq $lastRunID) { $lastRunID = 0 } $logEntry.RunID = $lastRunID } if ($EndFunction) { $FunctionStart = "$((($script:LogString | Where-Object {$_.InvokedBy -eq $Function -and $_.RunId -eq $lastRunID } | Sort-Object Time)[0]).Time)" $startTime = ([DateTime]::ParseExact("$FunctionStart", 'yyyy-MM-dd hh:mmTss', $null)) $endTime = Get-Date $timeTaken = $endTime - $startTime $Message = '<<< End Function Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" $logEntry.Message = $Message } elseif ($End) { $startTime = ([DateTime]::ParseExact($($script:LogString[0].Time), 'yyyy-MM-dd hh:mmTss', $null)) $endTime = Get-Date $timeTaken = $endTime - $startTime $Message = '--- End Log | ' + $Function + ' | Runtime: ' + "$($timeTaken.Minutes) min $($timeTaken.Seconds) sec" $logEntry.Message = $Message } $script:LogString += $logEntry switch ($Severity) { 'Warning' { Write-Warning ('[WARNING] ! ' + $Message) $UserInput = Read-Host "Warning encountered! Do you want to continue? (Y/N)" if ($UserInput -eq 'N') { Write-Output "Script execution stopped by user!" exit } } 'Error' { Write-Error ('[ERROR] X - ' + $FuncName + ' ' + $Message) -ErrorAction Continue } 'Verbose' { Write-Verbose ('[VERBOSE] ~ ' + $Message) } Default { Write-Information ('[INFORMATION] * ' + $Message) -InformationAction Continue} } } catch { throw "Write-AuditLog encountered an error (process block): $($_.Exception.Message)" } } end { try { if ($End) { if (-not [string]::IsNullOrEmpty($OutputPath)) { $script:LogString | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding utf8 Write-Verbose "LogPath: $(Split-Path -Path $OutputPath -Parent)" } else { throw "OutputPath is not specified for End action." } } } catch { throw "Error in Write-AuditLog (end block): $($_.Exception.Message)" } } } #EndRegion '.\Private\Write-AuditLog.ps1' 205 #Region '.\Public\Find-ADHostApp.ps1' 0 function Find-ADHostApp { <# .SYNOPSIS Searches AD Computers uninstall registry nodes for input Strings for installed app finding. .DESCRIPTION Searches computers using Invoke-Command passing functions to remote hosts. It will search the registry for strings matching a provided array of app names. By default, the local computer on which the script is running will not be scanned unless the 'Local' switch is used. If the 'Local' switch is used, ONLY the local computer will be scanned. .PARAMETER AppNames Array of one or more strings to search for apps: "Spiceworks","Microsoft","Adobe". .PARAMETER DaystoConsiderAHostInactive How many days back to consider an AD Computer last sign in as active. .PARAMETER SearchServers Enter one or more filenames. .PARAMETER SearchWorkstations Search Windows 10 and 11 workstations. .PARAMETER SearchOSString Search using custom OS Search String. .PARAMETER ComputerNames Search using specific hosts assumed to be online. .PARAMETER SearchBase Search a specific Organizational Unit: "OU=Infrastructure,OU=CorpComputers,DC=ad,DC=fabuloso,DC=com". .PARAMETER Filter Use a standard Filter: "Name -like "*PDC*"". Defaults to Wildcard. .PARAMETER Report Enable this switch to output a CSV Report. .PARAMETER DirPath Enter the working directory you wish the report to save to. Default creates C:\temp. .PARAMETER IncludeWow6432Node Also Search Wow6432Node. .PARAMETER Local Include the local machine in the scan. If set, ONLY the local computer will be scanned. .EXAMPLE Find-ADHostApp -AppNames "Adobe","Microsoft","Carbon" -ComputerNames "pdc-00","pdc-ha-00" -IncludeWow6432Node Output: GUID : Missing: Adobe DisplayName : Missing: Adobe DisplayVersion : Missing: Adobe Wow6432Node? : Missing: Adobe PSComputerName : pdc-00 RunspaceId : 47d370fb-f095-4bcf-a036-40997cb5af12 GUID : {F1BECD79-0887-4630-957B-108C894264AD} DisplayName : Microsoft Azure AD Connect Health agent for AD DS DisplayVersion : 3.1.77.0 Wow6432Node? : No PSComputerName : pdc-00 RunspaceId : 47d370fb-f095-4bcf-a036-40997cb5af12 .NOTES The function defaults to Searching all remotable host in a domain found to have a login within the last 90 days. It does not take into account the state of the product if a service is involved. This would require a manual check on the specific service. .LINK https://criticalsolutionsnetwork.github.io/ADAppFinder #> [CmdletBinding(DefaultParameterSetName = 'Default')] [OutputType([System.Management.Automation.PSCustomObject])] param ( [Parameter( Mandatory = $true, HelpMessage = 'Array of one or more strings to search for apps: "Spiceworks","Microsoft","Adobe"', Position = 0 )] [string[]]$AppNames, [Parameter( HelpMessage = 'How many days back to consider an AD Computer last sign in as active', Position = 1 )] [int]$DaystoConsiderAHostInactive = 90, [Parameter( Mandatory = $true, ParameterSetName = 'Default', HelpMessage = 'Search Servers', Position = 2 )] [switch]$SearchServers, [Parameter( Mandatory = $true, ParameterSetName = 'SearchBase', HelpMessage = 'Search a specific Organizational Unit', Position = 2 )] [string]$SearchBase, [Parameter( Mandatory = $true, ParameterSetName = 'SearchBase', HelpMessage = 'Use a standard Filter: "Name -like "*PDC*"". Defaults to Wildcard', Position = 3 )] [string]$Filter = "*", [Parameter( Mandatory = $true, ParameterSetName = 'SearchOSString', HelpMessage = 'Search using custom OS Search String', Position = 2 )] [string]$SearchOSString, [Parameter( Mandatory = $true, ParameterSetName = 'SearchWorkstations', HelpMessage = 'Search Windows 10 and 11 workstations', Position = 2 )] [switch]$SearchWorkstations, [Parameter( Mandatory = $true, ParameterSetName = 'SearchComputers', HelpMessage = 'Search using specific hosts assumed to be online.', Position = 2 )] [string[]]$ComputerNames, [Parameter( Mandatory = $true, HelpMessage = 'Include the local machine in the scan. If set, ONLY the local computer will be scanned.', ParameterSetName = 'Local', Position = 2 )] [switch]$Local, [Parameter( HelpMessage = 'Enable this switch to output a CSV Report.' )] [switch]$Report, [Parameter( HelpMessage = 'Enter the working directory you wish the report to save to. Default creates C:\temp' )] [string]$DirPath = 'C:\Temp\', [Parameter( HelpMessage = 'Also Search Wow6432Node.' )] [switch]$IncludeWow6432Node ) begin { if (!($script:LogString)) { Write-AuditLog -Start } else { Write-AuditLog -BeginFunction } Write-AuditLog -Message "Starting Find-ADHostApp" if ($Report) { # Create temp directory if [bool]$DirPathCheck = Test-Path -Path $DirPath If (!($DirPathCheck)) { Try { #If not present then create the dir New-Item -ItemType Directory $DirPath -Force } Catch { Write-Output "The Directory $DirPath was not created and does not exist. Evalate your permissions or modify the `$DirPath variable to suit." break } } } # Logging message about local machine scanning if ($Local) { if (!(Test-IsAdmin)) { Write-AuditLog -Message "The user must be running as administrator to scan the local machine. Exiting." -Severity Error break } Write-AuditLog -Message "Local switch was used. Only the local computer will be scanned." } else { Write-AuditLog -Message "Unless using the 'local' switch, the local machine the script is running on will not be scanned." } if ($Local) { $computers = $env:COMPUTERNAME } elseif ($ComputerNames) { $computers = $ComputerNames | Where-Object { $_ -ne $env:COMPUTERNAME } } elseif (($SearchServers) -or ($SearchWorkstations) -or ($SearchOSString)) { $Params, $NoRemoteAccess = Get-ADHostManageability -DaysInactive $DaystoConsiderAHostInactive -Servers:$SearchServers -Workstations:$SearchWorkstations -OperatingSystemSearchString $SearchOSString if ($params.ComputerName) { $computers = $Params.ComputerName | Where-Object { $_ -ne $env:COMPUTERNAME } $NonReachable = $NoRemoteAccess.NoRemoteAcces } else { Write-AuditLog "No computers were found to be online. Exiting." break } } elseif ($SearchBase) { $Params, $NoRemoteAccess = Get-ADHostManageability -DaysInactive $DaystoConsiderAHostInactive -SearchBase $SearchBase if ($params.ComputerName) { $computers = $Params.ComputerName | Where-Object { $_ -ne $env:COMPUTERNAME } $NonReachable = $NoRemoteAccess.NoRemoteAcces } else { Write-AuditLog "No computers were found to be online. Exiting." break } } } # End Begin Block process { try { Write-AuditLog -Message "Invoking command on remote computers." -Severity "Information" Write-AuditLog -Message "The computers are: `n$(($Computers -join ", "))" -Severity "Information" $results = Invoke-Command -ComputerName $computers -ScriptBlock ${Function:SearchForRegUninstallKey} -ArgumentList $AppNames, $IncludeWow6432Node } catch { Write-AuditLog -Message "An error occurred while invoking the command on remote computers: $_" -Severity "Error" } } # End Process Block end { try { if ($Report) { if ($ComputerNames) { $results | Export-Csv "$DirPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($env:USERDNSDOMAIN)\ADAppInstallStatus.csv" -NoTypeInformation } # End If $ComputerNames else { $computers | Out-File "$DirPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($env:USERDNSDOMAIN)\ReachableHosts.txt" -NoTypeInformation $NonReachable | Out-File "$DirPath\$((Get-Date).ToString('yyyy-MM_dd_hh.mm.ss'))_$($env:USERDNSDOMAIN)\NonReachableHosts.txt" -NoTypeInformation $results | Export-Csv "$DirPath\$((Get-Date).ToString('yyyy-MM-dd_hh.mm.ss'))_$($env:USERDNSDOMAIN)\ADAppInstallStatus.csv" -NoTypeInformation } } # End If $Report } catch { Write-AuditLog -Message "An error occurred during the end block of Find-ADHostApp: $_" -Severity "Error" } Write-AuditLog -EndFunction return $results } #End Block } #EndRegion '.\Public\Find-ADHostApp.ps1' 240 |