Powershell - looping through an array - powershell

I'm looking to search the C and E drives of all Windows servers in
Active Directory for any existing copies of putty.exe and their version.
The output needs to have the server name, full path to the executable,
and the file version. So far I have the following code (which right now is only using
two servers for testing:
$ComputerName = Get-ADComputer -filter "name -like 'computer01' -or name `
-like 'server01'" | select -ExpandProperty name
$OutputArr = #()
$findFiles = foreach($computer in $computername){
$file = Invoke-Command -computername $computer { Get-ChildItem -Path `
c:\, e:\ -Recurse | where-object{(!$_.psiscontainer -eq $true) -and `
($_.name -like "putty.exe")} | ForEach-Object -process {$_.fullname} }
$output = $OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $computer
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
Write-Verbose $OutputObj
}
$OutputArr | fl
The above code outputs the following array:
ComputerName : COMPUTER01
FilePath : {C:\Program Files\PuTTY\putty.exe, C:\Program Files (x86)\PuTTY\PUTTY.EXE}
ComputerName : SERVER01
FilePath : {C:\Program Files (x86)\putty\putty.exe, C:\Users\testuser\Desktop\Public Desktop\putty.exe}
This produces the correct data, but now I need to run another snippet of code against each
separate filepath under computername, but am not sure how to accomplish this, as it is
taking the full filepath with multiple entries.
Essentially, I need to separate each ComputerName in the array into multiple lines:
COMPUTER01,C:\Program Files\PuTTY\putty.exe
COMPUTER01,C:\Program Files (x86)\PuTTY\PUTTY.EXE
SERVER01,C:\Program Files (x86)\putty\putty.exe
Etc...
Is an array not the correct way to do it?

If you are working strictly with what you already have stored in $OutputArr, the following will work:
$out = foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
[pscustomobject][ordered]#{ComputerName = $line.ComputerName; FilePath = $fp}
}
}
else {
$line
}
}
$out | ConvertTo-Csv -NoTypeInformation
The foreach loop creates new objects with properties ComputerName and FilePath and stores them in $out as an array of objects.
If you do not care about properties and just want a comma-delimited list, you can use the following:
foreach ($line in $OutputArr) {
if ($line.filepath.count -gt 1) {
foreach ($fp in $line.FilePath) {
"{0},{1}" -f $line.ComputerName,$fp
}
}
else {
"{0},{1}" -f $line.ComputerName,$line.FilePath
}
}
This does the same looping as the first solution but instead uses the format operator (-f) to format the output. Piping to ConvertTo-Csv formats the output to be comma-delimited with the properties as headers.
You could move your desired functionality into your code before you even store anything in $OutputArr. I feel like doing all this after all of the other looping to create $OutputArr is just adding inefficiency.

PowerShell can get tricky when doing remote sessions. The below script should be a good starting point for you. Here are some other areas for improvement:
Doing Get-ChildItem -Recurse at the root of a drive will use an inordinate amount of memory and you could cause unintentional page file expansion or even make a server unresponsive due to 100% memory usage. In my snippet below I am using a list of well known paths. If you need to to identify if putty.exe is started on additional machines, your monitoring solution hopefully has process performance data and you can search for putty.exe there.
Speaking of memory management, remote shells have limitations of how much memory they can use. If you run winrm get winrm/config/winrs you will see the upper limit.
If you are going to authenticate to additional resources from within your remote script blocks, you will need to set up authentication that supports double hop scenarios (CredSSP or Kerberos)
$computerNames = #('computer1','computer2')
foreach($computer in $computerNames)
{
<#
First Script Block checks well known paths for putty.exe
#>
$puttyResults = Invoke-Command -ComputerName $computer -ScriptBlock {
$wellKnownPaths = #()
$wellKnownPaths += Join-Path $env:USERPROFILE -ChildPath "Desktop"
$wellKnownPaths += "D:\tools\"
$wellKnownPaths += $env:Path.Split(';')
$puttyPaths = #()
foreach($path in $wellKnownPaths)
{
$puttyPaths += Get-ChildItem $path -Filter "putty.exe" -Recurse
}
if($puttyPaths.Count -gt 0)
{
$resultsArray = #()
foreach($path in $puttyPaths)
{
$resultsArray += [PSCustomObject]#{
ComputerName = $env:COMPUTERNAME
PuttyPath = $path.FullName
}
}
return $resultsArray
}
return $null
}
if($puttyResults -ne $null)
{
foreach($result in $puttyResults)
{
<#
Second script block takes action against putty.exe
#>
$puttyExists = Invoke-Command -ComputerName $computer -ArgumentList #($result.PuttyPath) -ScriptBlock {
Param(
$PuttyPath
)
return (Test-Path $PuttyPath)
}
if($puttyExists)
{
$msg = "Putty exists on '{0}', at '{1}'" -f $result.ComputerName, $result.PuttyPath
Write-Host $msg -ForegroundColor:Yellow
}
}
}
}

I am not sure what exactly you are wanting to do, but this should work for iterating through your custom object. Your Invoke-Command can be simplified also.
$file = Invoke-Command -computername $computer { Get-ChildItem -Path "C:\", "E:\" -Recurse -File -Filter "putty.exe" | Select -Property VersionInfo }
$OutputObj = New-Object -TypeName PSobject
$OutputObj | Add-Member -MemberType NoteProperty -Name ComputerName -Value $env:COMPUTERNAME
$OutputObj | Add-Member -MemberType NoteProperty -Name FilePath -Value $file
$OutputArr += $OutputObj
foreach ($item in $OutputArr)
{
for ($i = 0; $i -lt $item.FilePath.Count; $i++)
{
Write-Output ([string]::Join(', ', $item.ComputerName, $item.FilePath[$i].VersionInfo.FileName, $item.FilePath[$i].VersionInfo.FileVersion))
}
}

Related

Function within a Function - Powershell

OK I am going to try to explain this as best as I can. What started out as a simple script has turned into a huge mess and now I cannot figure out how to get it working. I have been coming here for answers for some time so maybe you guys can help.
What I am trying to do is a import a list of systems and check to see if they are online. If they are online they go in one list and if not they go in another.
foreach ($server in $servers) {
if (Test-Connection $server -Count 1 -ea 0 -Quiet) {
Write-Host "$server Is Up" -ForegroundColor Green
$server | out-file -Append $liveSystems -ErrorAction SilentlyContinue
} else {
Write-Host "$server Is Down" -ForegroundColor Red
$server | out-file -Append $inactive -ErrorAction SilentlyContinue
}
}
From there I check to see if the application I need installed is on the systems. That is where things start to go off-track. When I run the function to process the $liveSystems file all I get is the last line of the file (or the same system over and over) and not each system as it should be.
function Is-Installed( $program ) {
$x86 = ((Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
$x64 = ((Get-ChildItem "HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall") |
Where-Object { $_.GetValue( "DisplayName" ) -like "*$program*" } ).Length -gt 0;
}
$program
function process-file1 {
param($filename)
Get-Content $filename -PipelineVariable line | ForEach-Object {
Is-Installed -program "My_Service"
if (Is-Installed -eq "True") {
Write-Host "$server has agent installed" -ForegroundColor Green
$server | Out-File $installed -ErrorAction SilentlyContinue
}
else
{
Write-Host "$server does not have agent installed" -ForegroundColor Red
$server | Out-File -Append $notInstalled -ErrorAction SilentlyContinue
}
}
}
process-file1 -filename $liveSystems
Once I can get the systems to process through the list of installed and not installed I am trying to take the list of installed systems and check which ones have the service running and which ones do not.
$array = #()
foreach($i in (gc $installed)) {
$svc = Get-Service my_service -ComputerName $i -ea "0"
$obj = New-Object psobject -Property #{
Name = $svc.name
Status = $svc.status
Computer = $i
}
$array += $obj
}
$array | Select Computer,Name,Status | Export-Csv -Path $resultsFile -
NoTypeInformation
Last but not least I run through that list of running and not running and attempt to start the service on systems that are not running.
function process-CSVfile2 {
param($filename)
Import-Csv $filename |
ForEach-Object -PipelineVariable object {
if($_.Status -eq "Running") {
Write-Host "Your Service is currently Running on" $_.Computer
}
if($_.Status -eq "Stopped") {
$serviceName = 'my_service'
$service = Get-CimInstance Win32_Service -ComputerName $_.Computer -Filter "Name=$serviceName"
$service.Start()
$service.WaitForStatus("Started",'00:00:30')
Start-Sleep 10
}
}
}
Several of these blocks run separately but when put together they will not run. I can't seem to get past the second block where it just looks at the same line over and over.
In addition there is a piece I have been trying to get working that would install the application on systems that do not have the service installed but that is not working either but I will save that for a different time.
If anyone can help me with this I would really appreciate it. After 3 days of trying to get it running I am at my wits end.
I'd create objects and properties instead of files with computers online etc...
Something like:
$Computers=New-Object -TypeName System.Collections.ArrayList
$Servers = #(Get-Content -path c:\servers.txt)
$Servers = $Servers | ? {$_} | select-object -uniqe |ForEach-Object {$_.TrimEnd()}
$Servers|ForEach-Object {
$tempobj=New-Object -TypeName PSObject
$tempobj | Add-Member -type NoteProperty -name Name -value $_
$tempobj | Add-Member -type NoteProperty -name isOnline -value $FALSE
$tempobj | Add-Member -type NoteProperty -name Installed -value $FALSE
$tempobj | Add-Member -type NoteProperty -name serviceRunning -value $FALSE
[void]$Computers.Add($tempobj)
then You could work on array (no need for additional files)
$Computers|Where-Object {$_.isOnline -eq $TRUE}
etc

Add-Member inside an outside foreach loop not exporting iterations to CSV

Input:
Servers: [1,2,3]
Services: [a,b,c]
The Server 1 has Service "a"
The Server 2 has Service "a", "b"
The Server 3 has Service "b", "c"
Expected output in CSV (gave correct output on console):
Server_Name ServerAvailability ServiceName[0] ServiceStatus[0] ServiceName[1] ServiceStatus[1]
1 Up a Running
2 Up a Running b Running
3 Up b Running c Running
Actual output in CSV:
Server_Name ServerAvailability ServiceName[0] ServiceStatus[0] ServiceName[1] ServiceStatus[1]
1 Up a Running
2 Up a Running
3 Up b Running
foreach ($s in $servers) {
foreach ($srv in $services) {
$Asrv = Get-Service -Name $srv -ComputerName $s
if ($Asrv -ne $null) {
$HashSrvs.Add($Asrv.Name, $Asrv.Status)
}
}
$infoObject = #()
$infoObject = New-Object PSObject
$infoObject | Add-Member -MemberType NoteProperty -Name "Server name" -Value $s
$infoObject | Add-Member -MemberType NoteProperty -Name "ServerAvailability" -Value $ConStatus
$i=0
foreach ($key in $HashSrvs.GetEnumerator()) {
$infoObject | Add-Member -MemberType NoteProperty -Name "ServiceName[$i]" -Value $key.Key -Force
$infoObject | Add-Member -MemberType NoteProperty -Name "ServiceStatus[$i]" -Value $key.Value -Force
$i++
}
$infoColl += $infoObject
}
$infoColl | Export-Csv -NoTypeInformation -Path .\Server_Inventory_$((Get-Date).ToString('MM-dd-yyyy')).csv -Encoding UTF8
The output is correct. No error shown. The error occurs at the CSV file. The highlighted foreach loop is not iterated. It stops at Service Name[0] and Service Status[0]. Please help me with this.
You're misunderstanding how Export-Csv works. The cmdlet takes a list of objects as input and determines the columns for the CSV from the first object in that list. For all subsequent objects missing properties are filled with null values and additional properties are omitted.
To get the result you want you'd need to know the maximum number of services and add that number of properties to each object. Alternatively you could create the CSV manually by building a list of comma-separated strings and writing that via Set-Content or Out-File. Both is doable, of course, but a much simpler, much more straightforward approach would be putting the list of services into a single column with a different delimiter:
$infoColl = foreach ($s in $servers) {
$svc = Get-Service -Name $services -Computer $s -ErrorAction SilentlyContinue |
ForEach-Object { '{0}={1}' -f ($_.Name, $_.Status) }
New-Object PSObject -Property #{
'Server name' = $s
'ServerAvailability' = $ConStatus
'Services' = $svc -join '|'
}
}

powershell: populate custom object from variable contents

I am in the process of writing a script that takes the output from the sysinternals command: psloggedon.exe and outputs the computer name and what users are signed in.
I currently have the script producing the following possibilities depending upon what output is given by each computer when queried.
DOMAIN\computer-name01
DOMAIN\user-name
DOMAIN\computer-name02
DOMAIN\user-name
DOMAIN\user-name
DOMAIN\computer-name03
Error connecting
DOMAIN\computer-name04
No user is signed in
Each chunk of data will always have a computer name followed by either the list of users signed in, an error, or a message saying that no one is signed in.
I would like to assign the data to a custom object so that I can output it to a CSV. As I understand this is the best way to generate a proper CSV.
Where I am getting confused with the custom object is how do I deal with a situation where the incoming data is changing in amount? So for instance, I may have a computer that has 4 people signed into it. I also don't understand how I can create the column headers. Most of the examples I have found the data that is being fed into the custom object already has it's own column headers or properties.
I am looking to output the data so that it looks something like below
DOMAIN\computer-name, DOMAIN\user-name, DOMAIN\user-name
DOMAIN\computer-name, Error Message
DOMAIN\computer-name, No one signed in
The column header over the computer name would be "Computer Name" and the header of the signed in user/error message/no one signed in would be "Status".
Microsoft has this pretty well documented it seems: https://technet.microsoft.com/en-us/library/ff730946.aspx
In this example I don't understand where "$objBatter.Name" comes from. How does the script know the property or column header from the input? Does the data being inputted already have that defined?
Sorry for being so confusing, I am having a hard time wrapping my brain around it so explaining it is hard.
Here is what I have tried. But basically the custom object just outputs the properties/column headers (sorry I am not really sure what this is called). The rest is blank.
$Computers = #(
,"computer-name01"
)
Foreach ( $Computer in $Computers)
{
$loggedon = $null
If ( Test-Connection -ComputerName $Computer -Quiet -Count 1 )
{
$loggedon = (PsLoggedon.exe -x -l \\$Computer | Where-Object { ($_ -like "*DOMAIN\*" -or $_ -like "*No one*" -or $_ -like "*$Computer\*" -or $_ -like "*Error opening*")}).trim()
if ( $loggedon -like "*Error Opening*" )
{
Write-Output "Error occurred while attempting to connect. This computer is online."
Continue
} else {
$loggedon = ,"$Computer" + $loggedon
$colComputerAndUser = #()
foreach ($item in $loggedon)
{
$ObjComputerAndUser = New-Object System.Object
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name Computer -Value $item.Computer
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name User -Value $item.User
$colComputerAndUser += $ObjComputerAndUser
}
$colComputerAndUser
}
}
}
UPDATE 01
I attempted to use the code you provided. It worked as intended, but I get a strange behavior if a computer returns more than just one person signed in.
Inside the custom object, the computer will be displayed multiple times.
$Computers = #(
,"computer01"
,"computer02"
)
$colComputerAndUser = #()
Foreach ( $Computer in $Computers)
{
$loggedon = $null
If ( Test-Connection -ComputerName $Computer -Quiet -Count 1 )
{
$loggedon = (PsLoggedon.exe -x -l \\$Computer | Where-Object { ($_ -like "*DOMAIN\*" -or $_ -like "*No one*" -or $_ -like "*$Computer\*" -or $_ -like "*Error opening*")}).trim()
$loggedon = [string]$loggedon
$ObjComputerAndUser = New-Object PSObject
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name Computer -Value $Computer
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name User -Value $loggedon
$colComputerAndUser += $ObjComputerAndUser
}
$colComputerAndUser
}
Computer User
-------- ----
computer01 DOMAIN\user01 DOMAIN\user02
computer01 DOMAIN\user01 DOMAIN\user02
computer02 DOMAIN\user03
Update 02
Sample 01 out from the PsLoggedon.exe - unedited
PsLoggedon v1.35 - See who's logged on
Copyright (C) 2000-2016 Mark Russinovich
Sysinternals - www.sysinternals.com
Users logged on locally:
DOMAIN\user.name01
DOMAIN\user.name02
Sample 02 out from the PsLoggedon.exe - unedited
PsLoggedon v1.35 - See who's logged on
Copyright (C) 2000-2016 Mark Russinovich
Sysinternals - www.sysinternals.com
Users logged on locally:
LOCALMACHINE\local.user01
LOCALMACHINE\local.user04
DOMAIN\user.name15
DOMAIN\user.name17
UPDATE 03
This code was actually working, but I had placed the out in the wrong part of the code.
$Computers = #(
,"computer01"
,"computer02"
,"computer03"
,"computer04"
,"computer05"
)
$colComputerAndUser = #()
Foreach ( $Computer in $Computers)
{
$loggedon = $null
If ( Test-Connection -ComputerName $Computer -Quiet -Count 1 )
{
$loggedon = (PsLoggedon.exe -x -l \\$Computer | Where-Object { ($_ -like "*DOMAIN\*" -or $_ -like "*No one*" -or $_ -like "*$Computer\*" -or $_ -like "*Error opening*")}).trim()
$loggedon = [string]$loggedon
$ObjComputerAndUser = New-Object PSObject
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name Computer -Value $Computer
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name User -Value $loggedon
$colComputerAndUser += $ObjComputerAndUser
}
}
$colComputerAndUser
Where this code is different is where the final $colComputerNameUser is placed. originally I had it contained within the foreach loop, which caused the output to duplicate. It now comes after the loop, and the output is working correctly.
Make an object with two properties: ComputerName, Users.
Concatenate all of the values after the ComputerName into a string and assign the string to the Users attribute.
$Computers = #(,"computername")
Foreach ($Computer in $Computers)
{
$loggedon = $null
if (Test-Connection -ComputerName $Computer -Quiet -Count 1){
$loggedon = (.\PsLoggedon.exe -x -l \\$Computer)
if ($loggedon -like "*Error Opening*"){
Write-Output "Error occurred while attempting to connect. This computer is online."
Continue
} else {
$loggedon = ,"$Computer" + $loggedon
$colComputerAndUser = #()
$UserString = ""
for($Cnt = 9;$Cnt -lt $loggedon.Count;$Cnt++)
{
$UserString += ($loggedon[$Cnt]).Trim() + "`n"
}
$ObjComputerAndUser = New-Object System.Object
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name Computer -Value $Computer
$ObjComputerAndUser | Add-Member -Type NoteProperty -Name User -Value $UserString
$colComputerAndUser += $ObjComputerAndUser
}
}
}
$colComputerAndUser | format-table -wrap
Not my most elegant work, but it works. I don't like hard-coding the start of the array count to 9. I tried to split on the colon but it just split on each `n in the output from PsLoggedon. I might try to refine this later. I also tried a global regex to match on [regex]'(?[^\])\(?.*)' but that didn't work out either. I got the results out, but couldn't enumerate the results.

Speeding powershell script with large csv file up

I am kinda new to powershell. Only be toying with it for a few days now and have written the below script to help search for multiple conditions in a csv file. I wrote something similar in VB and it takes 2 days to process the csv file. This powershell script takes about 6 hours to process 6500 machines and 9 policies. What i am trying to do is look in Policy.csv for a computer from computers.csv and a policy from a list and report if the computer has it or not.
Policy.csv has 6 fields in the table that need to be in the final report with an additional field added for status of the policy.
Computers.csv has 2 fields in the table that are the computer name and the OU it is in.
Packlist.txt is just the list of the applications(policies) that are being looked for.
Edit:
Samples of the csv files are as follows
Policy.csv
Device,Device DN,Group,Group DN,Policy Domain,Policy
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy1
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy2
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy3
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy1
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy2
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy3
Computer.csv
Device,Device DN
Comp1,OU=Here
Comp2,OU=There
Comp3,OU=AnyWhere
Packlist.txt
Policy1
Policy3
Result.csv
Device,Device DN,Group,Group DN,Policy Domain,Policy,Status
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy1,Entitled
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy1,Entitled
Comp3,OU=AnyWhere,,,,Policy1,Notentitled
Comp1,OU=Here,Domain_app,OU=Here,Ou=Apps,Server1,Policy3,Entitled
Comp2,OU=There,Domain_app,OU=Here,Ou=Apps,Server1,Policy3,Entitled
Comp3,OU=AnyWhere,,,,Policy3,Notentitled
The code is:
$data=import-csv -path c:\packagestatus\policy.csv
$computers=import-csv -path c:\packagestatus\computers.csv
$policylist= (Get-content -path c:\packagestatus\packlist.txt)
$policycount = $Policylist.count
$computercount = $computers.count
$Policycounter = 1
foreach ($policy in $policylist)
{
$Policy
$host.ui.RawUI.WindowTitle = "Processing $policyCounter of $policycount"
$Data_temp = $data|where-object{$_."Policy Instance" -eq $policy}
$computercounter = 1
foreach ($Computer in $computers)
{
$host.ui.RawUI.WindowTitle = "Processing Policy $policyCounter of $policycount and Computer $computercounter of $computercount"
if ($data_temp|Where-Object{$_.Device -eq $computer.device})
{
$result = $data_temp|where-object{$_.Device -eq $computer.device}|Where-Object{$_."Policy Instance" -eq $policy}
$result|Add-member -membertype Noteproperty -name Status -value Entitled
$result|export-csv -path c:\packagestatus\result1.csv -NoTypeInformation -append
}
Else
{
$result1 = New-Object -TypeName PSObject
$result1|add-member -membertype noteproperty -name "Device" -value $computer.device
$result1|add-member -membertype noteproperty -name "Device DN" -value $computer."Device DN"
$result1|add-member -membertype noteproperty -name "Group" -value $null
$result1|add-member -membertype noteproperty -name "Group DN" -value $null
$result1|add-member -membertype noteproperty -name "Policy Domain" -value $null
$result1|add-member -membertype noteproperty -name "Policy Instance" -value $Policy
$result1|add-member -membertype noteproperty -name "Status" -value NotEntitled
$result1|export-csv -path c:\packagestatus\result1.csv -force -NoTypeInformation -append
}
$computercounter++
}
$policycounter++
}
$host.ui.RawUI.WindowTitle = "Completed"
Ok, I think this should run faster for you...
I start by making a function to create objects for a given computer name, DN, and policy that it's missing. Then I load up the data from the files. Next I make a regex string to match against for all of the policies that are in the $Policylist. I do the same for the computer list. Then I filter down the $Data array for only entries that are in the policy list and also in the computer list.
This will, hopefully, limit the data that we're dealing with, and I think that will be faster in general. Next I group it by device, and for each grouping I look for any missing policies, and run that list against the function, and any matching policies I add the 'Status' property and output that entry. This is all collected in the $Results array.
Once we process all the computers we have records for, we look for the computers that weren't in the list, and create a NotEntitled object for all policies, and all all those to the $Results.
Lastly we sort and output $Results. It would be faster to not sort it I suppose, but probably harder to read as well. Here's the code, let me know how it works out for you:
Function NotEntitled{
[CmdletBinding()]
Param(
[String]$Device,
[String]$DeviceDN,
[String[]]$Pack
)
Process{
ForEach($Item in $Pack){
[PSCustomObject]#{
'Device' = $Device
'Device DN' = $DeviceDN
'Group' = $null
'Group DN' = $null
'Policy Domain' = $null
'Policy' = $Item
'Status' = 'NotEntitled'
}
}
}
}
$Data = import-csv -path c:\packagestatus\policy.csv
$Computers = import-csv -path c:\packagestatus\computers.csv
$Policylist = ,(Get-content -path c:\packagestatus\packlist.txt)
$PolicyReg = ($Policylist|%{[regex]::Escape($_)}) -join '|'
$ComputerReg = ($Computers.Device|%{[regex]::Escape($_)}) -join '|'
$FilteredData = $Data | Where{$_.Policy -match $PolicyReg -and $_.device -match $ComputerReg}
$Results = $FilteredData | Group Device | ForEach{
$Device = $_.group
$MissingPolicies = ,($Policylist | Where{$_ -notin $Device.Policy})
If(![string]::IsNullOrEmpty($MissingPolicies)){NotEntitled $Device[0].Device $Device[0].'Device DN' $MissingPolicies}
$Device | ForEach{Add-Member -InputObject $_ -NotePropertyName 'Status' -NotePropertyValue 'Entitled' -PassThru}
}
$CompList = $FilteredData | Select -ExpandProperty Device -Unique
$Results += $Computers | Where{$_.Device -notin $CompList} | ForEach{NotEntitled $_.Device $_.'Device DN' $Policylist}
$Results | Sort Device,Policy | Export-Csv c:\packagestatus\Result.csv -NoTypeInformation
I took your sample data, changed Comp1 Policy3 to Comp1 Policy4 (so that I could have a computer with only a partial policy set), and ran it and got these results output:
"Device","Device DN","Group","Group DN","Policy Domain","Policy","Status"
"Comp1","OU=Here","Domain_app","OU=Here,Ou=Apps","Server1","Policy1","Entitled"
"Comp1","OU=Here",,,,"Policy3","NotEntitled"
"Comp2","OU=There","Domain_app","OU=Here,Ou=Apps","Server1","Policy1","Entitled"
"Comp2","OU=There","Domain_app","OU=Here,Ou=Apps","Server1","Policy3","Entitled"
"Comp3","OU=AnyWhere",,,,"Policy1","NotEntitled"
"Comp3","OU=AnyWhere",,,,"Policy3","NotEntitled"

List installed applications on remote servers

I have pieced together the following script which lists all installed applications on a local system and writes it into a logfile, but I am unaware how to get this same output when using PSRemoteRegistry, the actual input list I need this against will be all remote targets.
Does anyone have experience with fitting this same code into the cmdlets available through PSRemoteRegistry? Specifically I need it to enumerate the displayname of every installed app found in the key HKLM:\Software\Microsoft\CurrentVersion\Uninstall
The piece I am needing help with getting into the PSRemoteRegistry cmdlets is:
Get-ChildItem 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall' | ForEach-Object {Log (Get-ItemProperty $_.pspath).DisplayName}
and this is the entire script:
clear
#$ErrorActionPreference = "silentlycontinue"
$Logfile = "C:\temp\installed_apps.log"
Function Log
{
param([string]$logstring)
Add-Content $Logfile -Value $logstring
}
$target_list = Get-Content -Path c:\temp\installed_apps_targets.txt
foreach ($system in $target_list){
if (test-connection $system -quiet)
{
Log "---------------Begin $system---------------"
Get-ChildItem 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall' | ForEach-Object {Log (Get-ItemProperty $_.pspath).DisplayName}
Log "---------------End $system---------------"
}
else
{
Log "**************$system was unreachable**************"
}
}
You can adapt something like this:
$Computer = "ComputerName"
$Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computer)
$RegKey= $Reg.OpenSubKey('Software\Microsoft\Windows\CurrentVersion\Uninstall')
$Keys = $RegKey.GetSubKeyNames()
$Keys | ForEach-Object {
$Subkey = $RegKey.OpenSubKey("$_")
Write-Host $Subkey.GetValue('DisplayName')
}
Have you met Invoke-Command?
$Logfile = "C:\temp\installed_apps.log"
Function Log() {
param([string]$logstring)
Add-Content $Logfile -Value $logstring
}
$scriptbock = {Get-ChildItem 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall' | ForEach-Object {Log (Get-ItemProperty $_.pspath).DisplayName}}
Get-Content -Path c:\temp\installed_apps_targets.txt | % {
if (test-connection $_ -quiet) {
Log "---------------Begin $system---------------"
Log $(Invoke-Command -ComputerName $_ -ScriptBlock $scriptblock)
Log "---------------End $system---------------"
}
else
{
Log "**************$system was unreachable**************"
}
}