Powershell Software Audit Output -csv format separated columns - powershell

My current Powershell script spits out a .txt doc of the computer name, and software that I would like to be able to import into a csv file, with each computer name being a new column.
currently the output looks like this:
PC1=
productname
SoftwareA
SoftwareB
PC2=
productname
SoftwareA
SoftwareB
how can I script this to appropriately sort this data? a straight import to csv will have all of this info in a single column. Is there something I can throw on the foreach loop to have it write to the next column? Or could I have each loop write to it's own .txt, and then grab each .csv and have them import into a new sheet
here's the source code:
$ComputerNames = get-content ".\Computers.txt"
foreach ($Computer in $ComputerNames)
{$arryStandardSoftware = get-content -path ".\StandardSoftware.txt"| Foreach-Object{$_.trim()}
$AuditResult = (Get-WMIObject -namespace "root\cimv2\sms" -class sms_installedsoftware -computername "$computer"|
Select-Object productname|Where-Object{$arryStandardSoftware -notcontains "$($_.productname)"})
echo "$Computer ="$AuditResult | out-file ".\SoftwareAudit.txt" -append}

What you want is an array of arrays.
ComputerA ComputerB ComputerC
SoftwareX SoftwareX SoftwareY
SoftwareY SoftwareZ SoftwareZ
SoftwareZ SoftwareA
SoftwareB
To get this result, you need to compile each array as you loop through the WMI results. Find the length of the longest array and then write out each row.
Here is a brute force approach to doing that:
$ComputerNames = get-content ".\Computers.txt"
$ComputerIndex = 0
$MasterArray = New-Object object[] $ComputerNames.Count
#collect the list in an array of arrays
foreach ($Computer in $ComputerNames) {
$arryStandardSoftware = get-content -path ".\StandardSoftware.txt"| Foreach-Object{$_.trim()}
$AuditResult = (Get-WMIObject -namespace "root\cimv2\sms" -class sms_installedsoftware -computername "$computer"|
Select-Object productname|Where-Object{$arryStandardSoftware -notcontains "$($_.productname)"})
$SoftwareArray = #()
$SoftwareArray += $Computer
$AuditResult | % { $SoftwareArray += $_.productname }
$MasterArray[$ComputerIndex] = $SoftwareArray
$ComputerIndex += 1
}
In the previous loop, an array is built for each computer. The first element is the computer name and the rest of the array is the list of software.
Now find out which of the arrays is the longest.
$longest = 0
for ($i=0;$i -lt $MasterArray.Count; $i++) {
if ($MasterArray[$i].Count -gt $longest){
$longest = $MasterArray[$i].Count
}
}
Once we know the maximum column length, we can iterate through all the arrays, building the rows which will be output to the CSV file.
$MyOutput = $null
for ($i=0;$i -lt $longest; $i++) {
$row = ""
for ($j=0;$j -lt $MasterArray.Count; $j++) {
if ($i -lt $MasterArray[$j].Count){
$row += $MasterArray[$j][$i]
}
if ($j -lt ($MasterArray.Count - 1) ){
$row += "`t"
}
}
$MyOutput += ($row + "`r`n")
}
$MyOutput > 'My.csv'
Like I said, this is a brute force approach, but the requirement to have each computer's software list as a column restricts the output options available.

The power of powershell is objects (PsCustomObject). In order to output each computer as a column, you can construct custom object and add new property to it, using computer name as property name (so long the computer name does not contain spaces or special characters).
The following script should output something like this:
ProductName, PC1, PC2
SoftwareA, true, false
SoftwareB, false, true
Haven't tested it, but you should get the basic idea.
$ComputerNames = get-content ".\Computers.txt"
$arryStandardSoftware = get-content -path ".\StandardSoftware.txt"| Foreach-Object{$_.trim()}
$reports = $arryStandardSoftware | select #{N="ProductName";E={$_}}
foreach ($Computer in $ComputerNames)
{
$installed = Get-WMIObject -namespace "root\cimv2\sms" -class sms_installedsoftware -computername "$computer" | select ProductName
foreach ($r in $reports)
{
Add-Member -InputObject $r -MemberType NoteProperty -Name $Computer -Value ($installed -contains $r.ProductName)
}
}
$reports | export-csv .\SoftwareAudit.txt -NoTypeInformation

Related

Looping an array for servers and drive names

I'm trying to find old files on my servers and having a little trouble with the drive path for the Get-NeglectedFiles function. My serverpath keeps showing up as \server\ .
Function Get-NeglectedFiles
{
Param([string[]]$path,
[int]$numberDays)
$cutOffDate = (Get-Date).AddDays(-$numberDays)
Get-ChildItem -Path $path -Recurse |
Where-Object {$_.LastAccessTime -le $cutOffDate}
}
$Endresults = #()
$serverlist = get-content "C:\temp\serverlist.txt"
foreach($server in $serverlist) {
$results = Get-WmiObject -ComputerName $Server -Class Win32_Share | select name
$Endresults += New-Object psobject -Property #{
Servername = $server
Result = $results
}
}
foreach($drive in $server){
$drives = $results | Where-Object { $_ -ne $null}
$serverpath = "\\" + $server + "\" + $drives + "\"
}
{Get-NeglectedFiles -path $serverpath -numberDays 90 | select name, lastaccesstime
}
You're probably looking to do something like this (I've simplified it a bit but you can extend on it):
$serverlist = Get-Content 'C:\temp\serverlist.txt';
foreach ($server in $serverlist) {
$drives = Get-WmiObject -ComputerName $Server -Class Win32_Share;
foreach ($drive in $drives.Name) {
$serverpath = "\\$server\$drive\";
$serverpath;
Get-NeglectedFiles -path $serverpath -numberDays 90 | select Name, LastAccessTime;
};
};
Explanation:
Get list of server names from file serverlist.txt
For each server in that list:
Retrieve the list of share names on that server
For each share on that server generate a serverpath and run Get-NeglectedFiles
Side note:
You also should probably inspect what is being returned by:
Get-WmiObject -ComputerName $Server -Class Win32_Share
And make sure that all of the shares returned are ones you want to use. For example, when I run it, I get shares like IPC$, print$, ADMIN$, as well as the default drive shares, and all other custom shares that have been created on the server. You probably aren't going to be cleaning files out of those.
Another side note:
You might want to consider using the -File parameter inside of your Get-NeglectedFiles command so that you are only targeting files and not directories.

Comparing Arrays within Powershell

I'm looking to do the Following Steps:
Reading in a CSV File
Add The Contents from the CSV File to An Array
Compare the Contents from the CSV with the Contents from another Array
If the Items in the Array aren't members of the CSV Array
Send Email
I First Tried Running the Script with One Host Missing from the Reports2 CSV File, This Missing Host was displayed to the Console and Written to the Reports2 File, but Still when i Re-Run the Code it still displays the last Element (Host that was Missing From Reports2.CSV):
This is the script I'm currently working On:
EDIT: I have now edited the code snippet to reflect the working solution
$user = ''
$pswd = ''
$vCenter_Servers = ""
$now = Get-Date
$start = $now.AddDays(-15)
$esxiHosts = Import-CSV C:\Scripts\Report1.csv #Reading CSV File
$MaitanceMode = #()
$Ticket = #()
foreach($ESXI in $esxiHosts){
$Ticket += $ESXI | Select-Object -ExpandProperty Name
}
foreach($vCenter in $vCenter_Servers) {
$srv = Connect-VIServer -Server $vCenter -User $user -Password $pswd
Get-VMHost -PipelineVariable esx -Server $srv | Where-Object {$_.ConnectionState -eq 'Maintenance'} |
ForEach-Object -Process {
$maintEntered = Get-VIEvent -Entity $esx -Start $start -MaxSamples ([int]::MaxValue) -Server $srv |
Where-Object{$_ -is [VMware.Vim.EnteredMaintenanceModeEvent]}
if($maintEntered){
#Skipping
}
else {
$MaitanceMode += $esx | Select-Object -ExpandProperty Name
}
}
} #Ending ForEach Loop
$NoTicket = $MaitanceMode | Where {$Ticket -Contains $_}
$NoTicket
You should instantiate your array containing the results as an empty array, probably before ForEach-Object -Process {... with $MaitanceMode = #() and when you want to add elements to it, replace this line:
$MaitanceMode = $esx | select name
by
$MaitanceMode += $esx | select name
Edit:
Further replace this line:
$esxiHosts = Import-CSV C:\Scripts\Report2.csv
by this line:
$esxiHosts = Import-CSV C:\Scripts\Report2.csv | Select-Object -ExpandProperty Name
and this line:
$MaitanceMode += $esx | select name
by this line:
$MaitanceMode += $esx | Select-Object -ExpandProperty Name
And don't forget to instantiate $MaitanceMode as an empty array. This is now mandatory. Otherwise it will become a string and not an array.
Despite the accepted answer from #Thomas, it is not correct to use the increase assignment operator (+=) to create a collection in PowerShell. For one thing, it is a very expensive syntax.
see: Why should I avoid using the increase assignment operator (+=) to create a collection.
To build a collection of objects in PowerShell, you should use the PowerShell pipeline by removing the <variable> += of the concerned commands (this will leave the objects on the pipeline) and catch the whole collection by adding <variable> = in front of the iterator (e.g. Foreach-Object). By using this PowerShell syntax, there is no need to initiate the arrays (<variable> = #()).
Taking your script as an example:
$user = ''
$pswd = ''
$vCenter_Servers = ""
$now = Get-Date
$start = $now.AddDays(-15)
$esxiHosts = Import-CSV C:\Scripts\Report1.csv #Reading CSV File
$Ticket = foreach($ESXI in $esxiHosts){
$ESXI | Select-Object -ExpandProperty Name
}
foreach($vCenter in $vCenter_Servers) {
$srv = Connect-VIServer -Server $vCenter -User $user -Password $pswd
Get-VMHost -PipelineVariable esx -Server $srv | Where-Object {$_.ConnectionState -eq 'Maintenance'} |
$MaitanceMode = ForEach-Object -Process {
$maintEntered = Get-VIEvent -Entity $esx -Start $start -MaxSamples ([int]::MaxValue) -Server $srv |
Where-Object{$_ -is [VMware.Vim.EnteredMaintenanceModeEvent]}
if($maintEntered){
#Skipping
}
else {
$esx | Select-Object -ExpandProperty Name
}
}
} #Ending ForEach Loop
$NoTicket = $MaitanceMode | Where {$Ticket -Contains $_}
$NoTicket

Trying to add PSCustomObjects to an ArrayList on a remote machine

I have some experience with PowerShell, and usually Google or searching forums like these yields the answers when I have questions - but not this time.
I'm trying to collect the number of .log files in a directory on a remote server, then I'd like to store the location (drive letter and folder path) and the count in an array list for later. So far everything is working as I'd expect, but I'm running into trouble adding my PSCustomObjects to the array list. I'm not sure if it's because I'm executing on a remote server or if something else is causing the problem. Here is my code:
$server = Read-Host -Prompt 'Please enter the server name'
[System.Collections.ArrayList]$returnObj = #()
Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
Write-Host $obj
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
}
}
}
$returnObj
In this format I get a syntax error on the line
$returnObj.Add($obj | Select-Object DatabaseFolder,LogFileCount)
If I change the above line to $returnObj.Add($obj) I avoid the syntax error, but instead I get an error saying I cannot call a method on a null valued expression.
I've tried creating the ArrayList inside the Invoke-Command and I've tried using New-Object instead of PSCustomObject to no avail.
I think your mixing stuff a bit up, this will do:
$returnObj = Invoke-Command -ComputerName $server {
$drives = Get-PSDrive -PSProvider FileSystem |
Where-Object {$_.Description -like "ExVol*"} |
Select-Object Root
foreach ($d in $drives) {
Set-Location -Path $d.Root
$folders = Get-ChildItem -Path $d.Root |
Where-Object {$_.Name -like "*.log"} |
Select-Object Name
foreach ($f in $folders) {
$count = (Get-ChildItem -Path $f.Name).Count
[PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
}
}
}
$returnObj
The problem is this line:
[System.Collections.ArrayList]$returnObj = #()
is declared outside of the Invoke-Command -ScriptBlock. This means it's not available within the session on the remote machine, and as such can not be used there.
On a side note, you cannot fill an array like you fill a Hashtable with data.
Arrays are filled like $MyArray = #(); $MyArray += 'MyValue'
Hashtables like $MyHash=#{}; $MyHash.SomeKey = 'SomeValue' or as you indicated $MyHash.Add('SomeKey', 'SomeValue')
ArrayLists are filled like [System.Collections.ArrayList]$MyArrayList = #(); $MyArrayList.Add('SomeValue')
I hope this makes it a bit more clear. The return values can always be catched before the Invoke-Command or even before a simple foreach (). For example $result = 0..3 | ForEach-Object {$_} is perfectly valid too.
You need to actually return your object from the remote system to your local system since you cannot use your $returnObj within the remote session.
As an example:
$returnValue = Invoke-Command -ComputerName $server {
$obj = [PSCustomObject]#{
LogFolder = $d.Root.Trim() + $f.Name
LogFileCount = $count
}
#return the object via the pipline
$obj
}
$returnObj.Add($returnValue | Select-Object DatabaseFolder,LogFileCount)
The above example is lacking of proper error handling, therefore you would get an error if the remote system is not reachable but it's a start.

Check Multiple Computers if Path Exists and Export to CSV

I want to make a script that will check whether or not a directory exists on each computer in "computers.csv".
This is what I've come up with:
$results = #()
$computers = Get-Content "C:\Users\me\Desktop\Notes\Computers.csv"
foreach ($computer in $computers) {
$path = Test-Path "\\$computer\c$\Program Files\Folder\"
if ($path -eq $true)
$Output = "True"
else
$Output = "False"
}
$details = #{
Computer_Name = $computer
Output = $Output
}
$results += New-Object PSObject -Property $details
$results |
Select-Object -Property Computer_Name,Output |
Export-Csv c:\results.csv -NoTypeInformation
Script is failing and I'm not entirely sure why. I need the script to export to a CSV due to how many computers are being queried.
You've got several syntax errors. You're missing brackets with if and else, and your foreach closing bracket is in the wrong place. Try this:
$results = #()
$computers = Get-Content "C:\Users\me\Desktop\Notes\Computers.csv"
foreach ($computer in $computers) {
$path = Test-Path "\\$computer\c$\Program Files\Folder\"
If ($path -eq $true) {
$Output = "True"
}
Else {
$Output = "False"
}
$details = #{
Computer_Name = $computer
Output = $Output
}
$results += New-Object PSObject -Property $details
}
$results | select-object -property Computer_Name, Output | Export-csv c:\results.csv -NoTypeInformation
That said, this pattern is one that should be avoided:
$results = #()
foreach ($item in $set) {
$results += $item
}
$results
The problem is that $results += $item copies the entire array into a new array and then adds the new item. It's a huge overhead as the size of the array increases.
Try something like this instead:
Get-Content "C:\Users\me\Desktop\Notes\Computers.csv" | ForEach-Object {
[PSCustomObject]#{
Computer_Name = $_
Output = Test-Path "\\$_\c$\Program Files\Folder\"
}
} | Export-Csv -Path C:\results.csv -NoTypeInformation

ForEach-Object piped to CSV is only showing last element

I'm trying to export to csv the scheduled tasks for multiple remote machines. I'm using a modified version of this script. I'm trying to export one csv per machine that lists all of the scheduled tasks. Currently my code just exports the last task for each machine.
foreach ($computerName in $computerNames) {
$Schedule.connect($computerName)
$AllFolders = Get-AllTaskSubFolders
foreach ($Folder in $AllFolders) {
if ($Tasks = $Folder.GetTasks(0)) {
$TASKS | % {[array]$results += $_}
$Tasks | Foreach-Object {
New-Object -TypeName PSCustomObject -Property #{
'Name' = $_.name
'Path' = $_.path
'Server' = $computername
} | Export-Csv $("C:\Users\MyName\Desktop\" + $computername + ".csv")
}
}
}
}
I've tried putting the Export-Csv at the end of each of the curly braces and none output what I want. Any ideas what I'm doing wrong?
EDIT
I'm not sure this fully addresses the issue, as some parts of the code seem weired to me such as $tasks = $folder.GetTasks. However OP title clearly says that the CSV is showing the last element meaning to me that his code mostly works except for the Export-CSV part.
Export-Csv expects an array as input, so the code below uses the elements in $task to generate the array of objects. Select-Object is there to decide in which order the properties are stored in the CSV.
Try this:
foreach ($computerName in $computerNames) {
$Schedule.connect($computerName)
$AllFolders = Get-AllTaskSubFolders
$result = #()
foreach ($Folder in $AllFolders) {
if ($Tasks = $Folder.GetTasks(0)) {
$TASKS | % {[array]$results += $_}
$Tasks | Foreach-Object {
$result += New-Object -TypeName PSCustomObject -Property #{
Name = $_.name;
Path = $_.path;
Server = $computername
}
}
}
}
$result | select Name, Path, Server | Export-Csv $($exportPath + $computername + ".csv")
}
This new version saves results on a per computer basis.
You are not changing either $exportPath or $computername in your loop, so each time through your outer loop, the csv file is being overwritten. Also, this
if ($Tasks = $Folder.GetTasks(0)) {
may be wrong. If you're trying to determine if $Tasks is equal to $Folder.GetTasks(0), you'd need this:
if ($Tasks -eq $Folder.GetTasks(0)) {