Adding objects to array to create multiple columns - powershell

I'm trying to get the DisplayName of all Software listed in AddRemovePrograms for each computer, add it to an array under the name of the computer, then export. Here's what I have:
$Computers = gc "C:\Get Software.txt"
$CSV = "C:\Get Software.csv"
$Results = #()
If (Test-Path $CSV) {
Remove-Item $CSV
}
Foreach($Computer in $Computers){
#Get DisplayName of Software Installed on Asset
$Software = Get-WmiObject Win32Reg_AddRemovePrograms -ComputerName $Computer | Select-Object -ExpandProperty DisplayName
$counter = 0
While ($counter -lt $Software.count){
#Create a PSObject. Loops through all software and adds to $Results.
$Obj = New-Object PSOBJECT
Add-member –inputobject $Obj –membertype Noteproperty -Name $Computer -Value $Software[$counter]
$counter++
$Results+=$Obj
}
}
$Results | Export-Csv $CSV -NoTypeInformation
Unfortunately, the output only lists the first Computer in the CSV. I've tried stepping through this to understand it, I just don't understand why I can't add the $Obj variable to $Results with a different Name Property (In this case I'm looping and making a new name based on the computer name in my .txt file). It seems to only take the first input and won't put a new column header then spit out the software. Below is an example of what I'm getting and what I'd like to get instead.
OUTPUT
A01234
Program1
Program2
Program3
EXPECTED OUTPUT
A01234 B05678 C09123
Program1 Program97 Program30
Program2 Program98 Program31
Program3 Program99 Program32
Hopefully this makes some sense. Any assistance would be appreciated, I'm clearly doing something wrong with the objects and I'm not seeing it. Thanks!

This is do-able, it just isn't quite as simple as you would think. What I would suggest is setting things up as a hashtable where the keys are your computer names and your values are an array of software titles for each computer. Then you find out how many titles the computer with the most software has (that's how many rows you'll have, so it's how many times you'll have to loop), and create a loop to make that many objects. Each object will have no properties to start, and then we add a property for each computer in the hashtable, and the value for that property will be the Nth item listed in the hashtable for that computer. Here's the code, it might make this all make more sense:
$Computers = 'A01234','B05678','C09123'
$SftwrPerCmp = [ordered]#{}
ForEach($Computer in $Computers){
$SftwrPerCmp.add($Computer,#())
#Randomly generate 7-15 strings as 'Programs' for current computer
$Software = 1..$(get-random -Maximum 15 -Minimum 7)|%{(Get-Random -inputobject $("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" -split ''|?{$_}) -count 6) -join ''}
ForEach($Title in $Software){
$SftwrPerCmp["$Computer"] += $Title
}
}
$MaxTitleCount = $SftwrPerCmp.Values|%{$_.count}|sort|select -last 1
$Results = #()
For($i=0;$i -lt $MaxTitleCount;$i++){
$Record = New-Object PSObject
$SftwrPerCmp.Keys | ForEach{Add-Member -InputObject $Record -NotePropertyName $_ -NotePropertyValue $SftwrPerCmp["$_"][$i]}
$Results += $Record
Clear-Variable Record
}
$Results|Format-Table -AutoSize
Now obviously you will not randomly generate software titles, you'll use your existing $Software = Get-WmiObject line that's in your existing code instead since that part was at least working for you. This should result in the listings that you were looking for. My code resulted in this:
A01234 B05678 C09123
------ ------ ------
CU7K5E 6GJWOB 97H1TY
7VCZ5T CIPWVK 760NKU
CHKPY0 J4B7D0 1QOSD3
2YEFR4 2VY6DM O68SKU
VI7ZQG WLJQN9 Q5VJAZ
ZQOKNV R9KZG1 H2XZK4
S8IZC4 GRSMPU BIZXKA
LAVNI0 TKBOUC K9DEFU
3U7KVO JZ3X4H
A6GVUK 18AC5H
NMI32Q H14GPJ
50KSZ6 XU0FWC
PAN5TC 9WXR5U
531M04

Related

Unable to show export-csv in PoweSshell

I have been researching the web to see what am I missing and can't find out, I run the command it goes thru the list of computers but the export doc is always empty.
Here is the code
foreach ($computer in Get-Content "\\NETWORK PATH\user-computers.txt") {
Write-host $computer
$colDrives = Get-WmiObject Win32_MappedLogicalDisk -ComputerName $computer
$Report = #()
# Set our filename based on the execution time
$filenamestring = "$computer-$(get-date -UFormat "%y-%b-%a-%H%M").csv"
foreach ($objDrive in $colDrives) {
# For each mapped drive – build a hash containing information
$hash = #{
ComputerName = $computer
MappedLocation = $objDrive.ProviderName
DriveLetter = $objDrive.DeviceId
}
# Add the hash to a new object
$objDriveInfo = new-object PSObject -Property $hash
# Store our new object within the report array
$Report += $objDriveInfo
}}
# Export our report array to CSV and store as our dynamic file name
$Report | Export-Csv -LiteralPath "\\NETWORK PATH\Drive-Maps.csv" -NoTypeInformation
I want to know what each computer currently got mapped network drives, thanks for all your help and guidance.
I'm not sure why you're not getting output. I've rewritten your script for a few reasons I'd like to point out. First, your variable naming is not very clear. I'm guessing you come from a VBScripting background. Next, you're creating an array and then adding to it - this is simply not needed. You can capture the output of any loop/scriptblock/etc directly by assigning like tihs.
$Report = foreach($thing in $manythings){Do lots of stuff and everything in stdout will be captured}
If you write your script in a way that takes advantage of the pipeline, you can do even more. Next, creating the object with New-Object is slow compared to using the [PSCustomObject] type accelerator introduced in V3. Finally, it seems you create a custom csv for each computer but in the end you just export everything to one file. I'm going to assume you are wanting to collect all this info and put in one CSV.
My recommendation for you to help troubleshoot, run this against your machines and confirm the output on the screen. Whatever you see on the screen should be captured in the report variable. (Except write-host, it's special and just goes to the console)
$computerList = "\\NETWORK PATH\user-computers.txt"
$reportFile = "\\NETWORK PATH\Drive-Maps.csv"
Get-Content $computerList | ForEach-Object {
Write-host $_
$mappedDrives = Get-WmiObject Win32_MappedLogicalDisk -ComputerName $_
foreach ($drive in $mappedDrives)
{
# For each mapped drive – build a hash containing information
[PSCustomObject]#{
ComputerName = $_
MappedLocation = $drive.ProviderName
DriveLetter = $drive.DeviceId
}
}
} -OutVariable Report
Once you know you have all the correct info, run this to export it.
$Report | Export-Csv -LiteralPath $reportFile -NoTypeInformation

Powershell add null content and not null content to one CSV

I need to include 'Installed' and 'Not Installed' data in one CSV
I think I need to incorporate an -OR logical operator to include TRUE/FALSE
output in one CSV. Idk how to do that yet.
There's a folder with many *ServerData files that contain a list of KBs with
possible duplicate KBs.
There is a *ServerData file for each server, with possible duplicate files.
I want to test whether any of them contain KB2151757 and KB4556403.
Then output the results to a csv with a status of either Installed or Not
Installed.
Currently it only returns a list of computers with the KB installed.
If the $patch is not found, it currently returns nothing (null).
For each $computer searched, it needs to return the specified fields for the
[PSCustomObject]
I'm thinking that maybe I just need to take a function to find 'installed' and a function to find 'not installed' and add the results together to export. Idk how to do that. I feel like there must be an easier way.
Click to view a sample of the CSV
$computers = Get-Item -path F:\*ServerData | Select-Object -ExpandProperty basename
$patch = gc -path F:\*ServerData | Sort-Object -Unique | Select-String KB2151757, KB4556403 #'KB(\d+)'
$output = ForEach ($computer in $computers) {
ForEach ($kb in $patch) {
if ($null -eq $patch){
[PSCustomObject]#{
Status = 'Not Installed'
Server = $computer
KB = $kb
}
} else{
[PSCustomObject]#{
Status = 'Installed'
Server = $computer
KB = $kb
}
}
}
}
$output | Export-csv C:\KB-Report.txt -notypeinformation -delimiter ',' -encoding utf8
If you start by grouping the files by the associated computer name, then the procedure becomes straightforward (pseudocode):
for each Computer
for each ExpectedPatch
if ServerData for Computer contains ExpectedPatch
Output object with 'Installed' status for ExpectedPatch on Computer
else
Output object with 'NotInstalled' status for ExpectedPatch on Computer
So let's give that a try:
# Define the articles we're looking for
$ExpectedPatches = 'KB2151757', 'KB4556403'
# Enumerate and group data files by computer name, output as hashtable
# The resulting hashtable will have the computer name is Name and the associated files as its value
$ServerDataPerComputer = Get-Item -Path F:\*ServerData |Group BaseName -AsHashtable
foreach($Computer in $ServerDataPerComputer.GetEnumerator())
{
foreach($Patch in $ExpectedPatches)
{
# Pipe all the file references to Select-String, look for the KB ID, return after the first match if any
$Status = if($Computer.Value |Select-String "\b$Patch\b" |Select-Object -First 1){
'Installed'
}
else {
# Select-String didn't find the KB ID in any of the files
'NotInstalled'
}
[pscustomobject]#{
Status = $Status
Server = $Computer.Name
KB = $Patch
}
}
}

Power shell For Loop not Looping

So the output works fine but I'm having an issue with it only outputing the last line it runs. Is there anyway to check for loops to test in the future?
but i have a list of ip address and im trying to check if the firewall in windows is enabled or disabled.
They are on one LARGE (300+ workgroup). Any help in getting this to loop properly would be appreciated. Security and other things are not a concern cause i have other scripts that run fine. And i dont get any errors. just the single output.
ive already tried moving the array and that didn't help. im thinking it could be the PSCustomObject part as i'm just starting to learn these. Or could it be my input and output formats are different and that's causing issues??
clear
$ComputerList = get-content C:\Users\Administrator\Desktop\DavidsScripts\TurnOffFirewall\input.txt
$Status = #(
foreach ($Computer in $ComputerList) {
netsh -r $Computer advfirewall show currentprofile state})[3] -replace 'State' -replace '\s'
$Object = [PSCustomObject]#{
Computer = $Computer
Firewall = $Status
}
Write-Output $Object
$Object | Export-Csv -Path "C:\FirewallStatus.csv" -Append -NoTypeInformation
Your previous code was not escaping the loop and was only adding the last computer in the loop to the object.
The best way I have found, is to make a temp object and add it to an array list then export that. Much nicer.
$ComputerList = get-content C:\Users\Administrator\Desktop\DavidsScripts\TurnOffFirewall\input.txt
$collectionVariable = New-Object System.Collections.ArrayList
ForEach ($Computer in $ComputerList) {
# Create temp object
$temp = New-Object System.Object
# Add members to temp object
$temp | Add-Member -MemberType NoteProperty -Name "Computer" -Value $Computer
$temp | Add-Member -MemberType NoteProperty -Name "Firewall" -Value $((netsh -r $Computer advfirewall show currentprofile state)[3] -replace 'State' -replace '\s')
# Add the temp object to ArrayList
$collectionVariable.Add($temp)
}
Write-Output $collectionVariable
$collectionVariable | Export-Csv -Path "C:\FirewallStatus.csv" -Append -NoTypeInformation
Here's a streamlined, functional version of your code, using a single pipeline:
Get-Content C:\Users\Administrator\Desktop\DavidsScripts\TurnOffFirewall\input.txt |
ForEach-Object {
[pscustomobject] #{
Computer = $_
Firewall = (-split ((netsh -r $_ advfirewall show currentprofile state) -match '^State'))[-1]
}
} | Export-Csv -Path C:\FirewallStatus.csv -NoTypeInformation
Note:
No intermediate variables are needed; each computer name read from the input file is processed one by one, and each custom object constructed based on it is sent to the output CSV file.
The command for extracting the firewall status from netsh's output was made more robust in order to extract the state information based on the line content (regex ^State, i.e., a line starting with State) rather than a line index ([3]); the unary form of -split splits the line of interest into tokens by whitespace, and index [-1] extracts the last token, which is the state value.
As for what you tried:
Your foreach loop ended before $Object was constructed, so you ended up constructing just 1 object to send to the output file with Export-Csv.
If you had formatted your code properly, that fact would have been more obvious; try using Visual Studio Code with the PowerShell extension, which offers automatic formatting via the >Format Document (Shift+Alt+F) command.

Trouble executing powershell script on multiple remote machines

I need to generate a list of all users on our network who are members of their workstation's local administrators group. I found a script here https://gallery.technet.microsoft.com/scriptcenter/List-local-group-members-762b48c5#content which was written to list local group members by executing a WMI query through Powershell. I've tested this script and it works well, but I've been trying to modify it to take in a list of computers to check and that's where I've run into trouble. Here's what I've done:
function LocalAdmins
{
param([string]$GroupName = "Administrators")
begin
{
# Get all workstations listed in this text file
$WorkStations = Get-Content -Path C:\useful_lists\testLocal.txt
# Initialize an array to hold the results of the query
$arr = #()
# hash table for storing computer name, member pairings
$hash = #();
}
process
{
foreach ($machine in $WorkStations)
{
$wmi = Get-WmiObject -ComputerName $machine -Query `
"SELECT * FROM Win32_GroupUser WHERE GroupComponent=`"Win32_Group.Domain='$machine',Name='$GroupName'`""
# Parse out the username from each result and append it to the array
if ($wmi -ne $null)
{
foreach($item in $wmi)
{
$arr += ($item.PartComponent.Substring($item.PartComponent.IndexOf(',') + 1).Replace('Name=', '').Replace("`"", ''))
}
}
# Return a hash table comprised of two columns: Computer Name & Members
$hash += #{ComputerName=$machine;Members=$arr}
}
}
end
{
return $hash
}
}
When I ran the unmodified script here's what I got as output:
PS > (Get-LocalGroupMembers -ComputerName "<COMPUTER NAME>" -GroupName "Administrators").Members
ACCOUNTNAME
ACCOUNTNAME
ACCOUNTNAME
PS >
However, when I run the version of this script that I modified I get this:
PS > (LocalAdmins -GroupName "Administrators").Members
PS >
I'm fairly certain that the issue lies either in how I've setup the first foreach loop to run the wmi query or how the results of that query are being stored in the hash table. I'm not sure what I could do differently to fix the issue.
Thanks in advance to anyone who can help!
UPDATE
Per mortenya's suggestion, I edited my test text file to only include one computer in it. Doing so, along with taking out the foreach ($machine in $computers) loop worked as expected producing the following result:
>> LocalAdmins -GroupName "Administrators"
Name Value
---- ----
ComputerName {computerName.domain}
Members {account, account, account, account}
>>
However, going back and trying to get this to work when incorporating multiple machines using the code above (I've updated it since my initial post), I get the following:
>> LocalAdmins -GroupName "Administrators"
Name Value
---- -----
ComputerName computerName1.domain
Members {}
ComputerName computerName2.domain
Members {}
>>
Why is it that with one machine in the list I can get the members of the Administrator group, but adding a second computer to the list makes it so I can not retrieve members from that group on either machine?
So, if you're going to use Begin{}, Process{}, and End{}, use them for what they're meant for, in the Begin{} block, initialize all your arrays and constant varaibles.
Begin {
# Get all workstations listed in this text file
$WorkStations = Get-Content -Path C:\useful_lists\testLocal.txt
# Store the contents of that list in an array
$computers = #()
$hash = #()
}
Outside of that, I did this same thing a few months ago, it's a little messy, but it spit out a list of computers and who was in the Local Administrators group. It was partially to practice some different methods.
$output = 'c:\psresults\ListOfLocalAdministratorsGroup.txt'
$results = New-Object System.Collections.ArrayList
$objSID = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")
$objgroup = $objSID.Translate( [System.Security.Principal.NTAccount])
$objgroupname = ($objgroup.Value).Split("\")[1]
foreach($server in (Get-ADComputer -Filter *).name)
{
$admins = New-Object System.Collections.ArrayList
$group =[ADSI]"WinNT://$server/$objgroupname"
$members = #($group.psbase.Invoke("Members"))
$members | foreach {
$obj = new-object psobject -Property #{
Server = $Server
Admin = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
}
#$obj
$admins.Add($obj)
}
$results.Add($admins)
}
$results | Out-File $Output
I found the meat of that somewhere and then modified it a bit.
EDIT: I just put this into ISE and it seems to work fine
$machine = "testsrv"
$groupname = "Administrators"
$wmi = Get-WmiObject -ComputerName $machine -Query `
"SELECT * FROM Win32_GroupUser WHERE GroupComponent=`"Win32_Group.Domain='$machine',Name='$GroupName'`""
if ($wmi -ne $null)
{
foreach ($item in $wmi)
{
$arr += ($item.PartComponent.Substring($item.PartComponent.IndexOf(',') + 1).Replace('Name=', '').Replace("`"", ''))
}
}
$hash = #{ComputerName=$machine;Members=$arr}
return $hash
Get it working on one machine, then start trying to add the loops back in.
EDIT 2.0:
I made a .txt file with only computer names in it, not the FQDN, that works fine for me. I can run it and get results using your script with minor modification.
Despite what I'd said about the Begin{} block, the $arr variable will need to be initialized inside the foreach ($machine in $WorkStations) loop. The reason for this is that when the loop runs, it will create the $arr array, add the data we want, insert that data into a global variable, and then clean up the $arr variable. If we make this global, it won't be cleaned up until the function is done, and we will just keep adding to it, which isn't what we actually want in this case.
The problem you're having with getting multiple machines to work is likely how you're building your results table.

Powershell - Create new line for multiple array objects using Export-csv

I have an odd one that I haven't seen much writing on. Anyway, here goes.
I'm trying to build an array and export it to CSV. The problem is, if there is more than one result returned, I can't figure out how to add it to the CSV as a new line. I am currently using a -join to throw all the results into the same cell, but that's not optimal. What I'd really like to do is add a new row and throw the extra results underneath it in the same column. Does that make sense? Here's what I have now:
# Grab all VMs, put into variable
$vms = Get-VM
# Use to build report
foreach ($vm in $vms){
New-Object PSObject -Property #{
VMName = $vm.Name
UsedSpaceGB = $vm.UsedSpaceGB
StorageAllocatedGB = ($vm.HardDisks.capacitygb | Measure-Object -Sum).Sum
NumberOfCPUs = $cm.NumCpu
MemoryGB = $vm.MemoryGB
Datastores = Get-Datastore -VM $vm
Application = ($vm | Get-Annotation -CustomAttribute Applications -ErrorAction SilentlyContinue).Value | select
} | select VMName,#{label="Application";expression={$_.Application -join ","}},UsedSpaceGB,StorageAllocatedGB,NumberOfCPUs,MemoryGB,#{l="Datastores";e={$_.Datastores -join ","}} | Export-Csv -Path C:\script\VMCapacityUsedByApp.csv -NoClobber -Append -NoTypeInformation
}
By the way, this is using VMware's PowerCLI snapin. Any help is greatly appreciated.
Ok, looks like datastores and applications are the only fields that are going to return arrays according to your previous code. Assuming that the $cm.NumCpu was supposed to be $vm.NumCpu the following code should do what you want. It will figure out if you have more datastores or applications, and then loop through expanding the arrays for those fields creating new records for the same VM listing additional datastores and applications until it runs out of records. I set it to only list all details of a VM on the first record, but I'm sure you can figure out how to alter that if needed. Try this code and see how it looks to you:
# Grab all VMs, put into variable
$vms = Get-VM
# Use to build report
foreach ($vm in $vms){
$TempVM = New-Object PSObject -Property #{
VMName = $vm.Name
UsedSpaceGB = $vm.UsedSpaceGB
StorageAllocatedGB = ($vm.HardDisks.capacitygb | Measure-Object -Sum).Sum
NumberOfCPUs = $cm.NumCpu
MemoryGB = $vm.MemoryGB
Datastores = Get-Datastore -VM $vm
Application = ($vm | Get-Annotation -CustomAttribute Applications -ErrorAction SilentlyContinue).Value
}
$Records = if($TempVM.Application.count -gt $TempVM.Datastores.Count){$TempVM.Application.Count}else{$TempVM.Datastores.Count}
$ExpandedVM = #()
$ExpandedVM += $TempVM|select Name,UsedSpaceGB,StorageAllocatedGB,NumberOfCPUs,MemoryGB,#{l="Datastores";e={$TempVM.Datastores[0]}},#{l="Application";e={$TempVM.Application[0]}}
for($i=1;$i -lt $Records;$i++){$ExpandedVM += $TempVM|select Name,#{l="Datastores";e={$TempVM.Datastores[$i]}},#{l="Application";e={$TempVM.Application[$i]}}}
$ExpandedVM | Export-Csv -Path C:\script\VMCapacityUsedByApp.csv -NoClobber -Append -NoTypeInformation
}
There may be a more elegant way to do it, but that should be functional for you at the very least. I don't have VM machines to test against, or the plugin you use, so I made up data that should be in line with what you're feeding it (strings for all fields except datastores and application both of which have their own array of strings) and ended up with output like this:
Name UsedSpaceGB StorageAllo NumberOfCP MemoryGB Datastores Applicatio
catedGB Us n
---- ----------- ----------- ---------- -------- ---------- ----------
TestVM 250 500 4 16 Store1 Word
TestVM Store2 Excel
TestVM Store3 Access
TestVM Outlook
TestVM2 487 500 4 32 StoreA WoW
TestVM2 StoreB SC2
TestVM2 StoreC D3
TestVM2 StoreD
TestVM2 StoreE
That is what you were looking for I think.