Powershell Output - formatting - powershell

I have the below powershell script and I want the format to show the whole string, as though I was using Format-Table with -Wrap and -Autosize. I've tried using those, but it doesn't show all the needed properties, not sure if I wasn't using it correctly or what was the case.
$threshold = 30 #Number of days to look for expiring certificates
$deadline = (Get-Date).AddDays($threshold) #Set deadline date
Invoke-Command -ComputerName { Dir Cert:\LocalMachine\My } | foreach {
If ($_.NotAfter -le $deadline) {
$_ | Select Issuer, Subject, NotAfter, #{Label="Expires In (Days)";
Expression={($_.NotAfter - (Get-Date)).Days}}
}
}
This is what my output currently looks like:
Issuer Subject NotAfter Expires In (Days)
CN=MASKEDMASKEDMASKED ... CN=masked.customer.masked.com, OU=... 2/21/2014 5:59:59 PM -17
CN=MASKEDMASKEDMASKED ... CN=masked.customer.masked.com, OU=... 2/21/2014 5:59:59 PM -17
Can anyone provide some assistance, please? I basically just need to expand those columns to see all the details.

If you don't plan to use the data after these commands, you can pipe them to Format-* as you mentioned. Select will get you the data, but not the friendly display. Is there a reason you need it displayed?
Here's a quick example where you get the best of both worlds:
#Assign output to a variable
$result = Invoke-Command -ComputerName { Dir Cert:\LocalMachine\My } | foreach {
If ($_.NotAfter -le $deadline) { $_ | Select Issuer, Subject, NotAfter, #{Label="Expires In (Days)";Expression={($_.NotAfter - (Get-Date)).Days}} }
}
#Write the results to the verbose stream. Use format table and out-string for appearances
#Change to write-host, debug, error, warning, etc. as needed
Write-Verbose "Results:`n$($result | Format-Table -AutoSize -Wrap | Out-String)"
#$result is still a usable object at this point
$result
*edit: I don't have the reputation to comment on Cole9350's suggestion - don't use ExpandProperty. That is for expanding a single property...

Use the -ExpandProperty Parameter of your Select-Object cmdlet

Related

Why Powershell outputting this table?

I'm a powershell noob. How come the following code is also outputing the table at the end after the "File to Delete" loop?
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
# use partial hashes for files larger than 100KB:
# see documentation at: https://powershell.one/tricks/filesystem/finding-duplicate-files#finding-duplicate-files-fast
$result = Find-PSOneDuplicateFileFast -Path '\\READYNAS\Pictures\2020\10' #-Debug -Verbose
$stopwatch.Stop()
# output duplicates
$allFilesToDelete = #(foreach($key in $result.Keys)
{
#filters out the LAST item in the array of duplicates, because a file name of xxxx (0) comes before one without the (0)
$filesToDelete = $result[$key][0..($result[$key].count - 2)]
#add each remaining duplicate file to table
foreach($file in $filesToDelete)
{
$file |
Add-Member -MemberType NoteProperty -Name Hash -Value $key -PassThru |
Select-Object Hash, Length, FullName
}
}
)
$allFilesToDelete | Format-Table -GroupBy Hash -Property FullName | Out-String | Write-Host
$allFilesToDelete | Sort-Object -Property FullName -OutVariable allFilesToDelete
$allFilesToDelete | Format-Table -Property FullName | Out-String | Write-Host
$confirmation = Read-Host "Are you Sure You Want To Delete $($allFilesToDelete.count) files? (y/n)"
if ($confirmation -eq 'y') {
$i = 0
foreach($fileToDelete in $allFilesToDelete)
{
$i++
Write-Host "$i File to Delete: $($fileToDelete.FullName)"
#Remove-Item $file.FullName -Force -Verbose 4>&1 | % { $x = $_; Write-Host "Deleted file ($i) $x" }
}
} else {
Write-Host "User chose NOT to delete files!"
}
$allFilesToDelete | Sort-Object -Property FullName -OutVariable allFilesToDelete produces output (the input objects in the requested sort order), and since you're not capturing or redirecting it, it prints to the host (display, terminal) by default.
It seems your intent is to sort the objects stored in $allFilesToDelete, which your command does, but it also produces output (the common -OutVariable parameter does not affect a cmdlet's output behavior, it simply also stores the output objects in the given variable); you could simply assign the output back to the original variable, which wouldn't produce any output:
$allFilesToDelete = $allFilesToDelete | Sort-Object -Property FullName
In cases where actively suppressing (discarding) output is needed, $null = ... is the simplest solution:
See this answer for details and alternatives.
Also see this blog post, which you found yourself.
Because the output resulted in implicitly Format-Table-formatted display representations (for custom objects that have no predefined formatting data), the subsequent Read-Host and Write-Host statements - surprisingly - printed first.
The reason is that this implicit use of Format-Table results in asynchronous behavior: output objects are collected for 300 msecs. in an effort to determine suitable column widths, and during that period output to other output streams may print.
The - suboptimal - workaround is to force pipeline output to print synchronously to the host (display), using Out-Host.
See this answer for details.

PowerShell Export-CSV - Missing Columns [duplicate]

This question already has an answer here:
Not all properties displayed
(1 answer)
Closed 1 year ago.
This is a follow-up question from PowerShell | EVTX | Compare Message with Array (Like)
I changed the tactic slightly, now I am collecting all the services installed,
$7045 = Get-WinEvent -FilterHashtable #{ Path="1system.evtx"; Id = 7045 } | select
#{N=’Timestamp’; E={$_.TimeCreated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')}},
Id,
#{N=’Machine Name’; E={$_.MachineName}},
#{N=’Service Name’; E={$_.Properties[0].Value}},#{N=’Image Path’;E=$_.Properties[1].Value}},
#{N=’RunAsUser’; E={$_.Properties[4].Value}},#{N=’Installed By’; E={$_.UserId}}
Now I match each object for any suspicious traits and if found, I add a column 'Suspicious' with the value 'Yes'. This is because I want to leave the decision upto the analyst and pretty sure the bad guys might use something we've not seen before.
foreach ($Evt in $7045)
{
if ($Evt.'Image Path' -match $sus)
{
$Evt | Add-Member -MemberType NoteProperty -Name 'Suspicious' -Value 'Yes'
}
}
Now, I'm unable to get PowerShell to display all columns unless I specifically Select them
$7045 | Format-Table
Same goes for CSV Export. The first two don't include the Suspicious Column but the third one does but that's because I'm explicitly asking it to.
$7045 | select * | Export-Csv -Path test.csv -NoTypeInformation
$7045 | Export-Csv -Path test.csv -NoTypeInformation
$7045 | Select-Object Timestamp, Id, 'Machine Name', 'Service Name', 'Image Path', 'RunAsUser', 'Installed By', Suspicious | Export-Csv -Path test.csv -NoTypeInformation
I read the Export-CSV documentation on MS. Searched StackOverFlow for some tips, I think it has something to do with PS checking the first Row and then compares if the property exists for the second row and so on.
Thank you
The issue you're experiencing is partially because of how objects are displayed to the console, the first object's Properties determines the displayed Properties (Columns) to the console.
The bigger problem though, is that Export-Csv will not export those properties that do not match with first object's properties unless they're explicitly added to the remaining objects or the objects are reconstructed, for this one easy way is to use Select-Object as you have pointed out in the question.
Given the following example:
$test = #(
[pscustomobject]#{
A = 'ValA'
}
[pscustomobject]#{
A = 'ValA'
B = 'ValB'
}
[pscustomobject]#{
C = 'ValC'
D = 'ValD'
E = 'ValE'
}
)
Format-Table will not display the properties B to E:
$test | Format-Table
A
-
ValA
ValA
Format-List can display the objects properly, this is because each property with it's corresponding value has it's own console line in the display:
PS /> $test | Format-List
A : ValA
A : ValA
B : ValB
C : ValC
D : ValD
E : ValE
Export-Csv and ConvertTo-Csv will also miss properties B to E:
$test | ConvertTo-Csv
"A"
"ValA"
"ValA"
You have different options as a workaround for this, you could either add the Suspicious property to all objects and for those events that are not suspicious you could add $null as Value.
Another workaround is to use Select-Object explicitly calling the Suspicious property (this works because you know the property is there and you know it's Name).
If you did not know how many properties your objects had, a dynamic way to solve this would be to discover their properties using the PSObject intrinsic member.
using namespace System.Collections.Generic
function ConvertTo-NormalizedObject {
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline, Mandatory)]
[object[]] $InputObject
)
begin {
$list = [List[object]]::new()
$props = [HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase)
}
process {
foreach($object in $InputObject) {
$list.Add($object)
foreach($property in $object.PSObject.Properties) {
$null = $props.Add($property.Name)
}
}
}
end {
$list | Select-Object ([object[]] $props)
}
}
Usage:
# From Pipeline
$test | ConvertTo-NormalizedObject | Format-Table
# From Positional / Named parameter binding
ConvertTo-NormalizedObject $test | Format-Table
Lastly, a pretty easy way of doing it thanks to Select-Object -Unique:
$prop = $test.ForEach{ $_.PSObject.Properties.Name } | Select-Object -Unique
$test | Select-Object $prop
Using $test for this example, the result would become:
A B C D E
- - - - -
ValA
ValA ValB
ValC ValD ValE
Continuing from my previous answer, you can add a column Suspicious straight away if you take out the Where-Object filter and simply add another calculated property to the Select-Object cmdlet:
# create a regex for the suspicious executables:
$sus = '(powershell|cmd|psexesvc)\.exe'
# alternatively you can join the array items like this:
# $sus = ('powershell.exe','cmd.exe','psexesvc.exe' | ForEach-Object {[regex]::Escape($_)}) -join '|'
$7045 = Get-WinEvent -FilterHashtable #{ LogName = 'System';Id = 7045 } |
Select-Object Id,
#{N='Timestamp';E={$_.TimeCreated.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')}},
#{N='Machine Name';E={$_.MachineName}},
#{N='Service Name'; E={$_.Properties[0].Value}},
#{N='Image Path'; E={$_.Properties[1].Value}},
#{N='RunAsUser'; E={$_.Properties[4].Value}},
#{N='Installed By'; E={$_.UserId}},
#{N='Suspicious'; E={
if ($_.Properties[1].Value -match $sus) { 'Yes' } else {'No'}
}}
$7045 | Export-Csv -Path 'X:\Services.csv' -UseCulture -NoTypeInformation
Because you have many columns, this will not fit the console width anymore if you do $7045 | Format-Table, but the CSV file will hold all columns you wanted.
I added switch -UseCulture to the Export-Csv cmdlet, which makes sure you can simply double-click the csv file so it opens correctly in your Excel.
As sidenote: Please do not use those curly so-called 'smart-quotes' in code as they may lead to unforeseen errors. Straighten these ’ thingies and use normal double or single quotes (" and ')

ForEach-Object changing result of SearchUnifiedAuditLog

I have a powershell script to retrieve the most recent login for guest users. It works fine.
(Based on https://gallery.technet.microsoft.com/office/Get-Guest-User-Last-Login-39f8237e)
$startDate = "{0:yyyy-MM-dd}" -f (get-date).AddDays(-365) #look 1 year back (not sure what the maximum is, but 1 year seems to work)
$endDate = "{0:yyyy-MM-dd}" -f (get-date) #current date.
$externalUserExtention = "*#EXT#*"
$filePath="c:\temp\guest_user_last_logins.txt"
function logtoText($filePath, $msg) {
$msg >> $filepath;
}
function find_last_login_date($user) {
$lastLoginDate = Search-UnifiedAuditLog -UserIds $user.UserPrincipalName -StartDate $startDate -EndDate $endDate| Foreach-Object {$_.CreationDate = [DateTime]$_.CreationDate; $_} | Group-Object UserIds | Foreach-Object {$_.Group | Sort-Object CreationDate | Select-Object -Last 1} | Select CreationDate
Write-Host "User " $user.UserPrincipalName "| Last Login Date -" $lastLoginDate.CreationDate
logtoText $filePath ($user.UserPrincipalName + "," + $lastLoginDate.CreationDate)
Write-Output $user.UserPrincipalName
}
Clear-Content $filePath
logtoText $filePath ('Username', 'Last Login DateTime')
#Get All External Users
$allExternalUsers = Get-MsolUser -All | Where-Object -FilterScript { $_.UserPrincipalName -Like $externalUserExtention }
ForEach($externalUser in $allExternalUsers) {
find_last_login_date($externalUser)
}
The output is as expected:
User user1#external.com#EXT##tenant.onmicrosoft.com | Last Login Date -
user1#external.com#EXT##tenant.onmicrosoft.com
User user2#external.com#EXT##tenant.onmicrosoft.com | Last Login Date - 11/04/2020 12:02:12
user2#external.com#EXT##tenant.onmicrosoft.com
...
User userNNNN#external.com#EXT##tenant.onmicrosoft.com | Last Login Date - 12/04/2020 14:22:25
userNNNN#external.com#EXT##tenant.onmicrosoft.com
...
The extra Write-Output inside find_last_login_date is just their for debugging this.
Now when I change it to ForEach-Object it does something weird. Probably it makes sense to the experts, but not to me :-)
Replace
ForEach($externalUser in $allExternalUsers) {
find_last_login_date($externalUser)
}
With
ForEach-Object -InputObject $allExternalUsers {
find_last_login_date($_)
}
And the output becomes
User userX#external.com#EXT##tenant.onmicrosoft.com | Last Login Date - 10/01/2021 14:05:36 05/01/2021 20:48:45 05/01/2021 16:09:25 04/01/2021 07:36:26 22/12/2020 08:01:07 19/12/2020 10:24:08 18/12/2020 14:20:51 18/12/2020 08:05:55 16/12/2020 17:32:28 14/12/2
020 08:20:56 13/12/2020 07:58:13 11/12/2020 10:58:58 10/12/2020 08:08:28
user1#external.com#EXT##tenant.onmicrosoft.com
user2#external.com#EXT##tenant.onmicrosoft.com
...
userNNNN#external.com#EXT##tenant.onmicrosoft.com
...
So it seems to trigger the script to only look for the last login date once and also the select top 1 fails, etc.
Any idea what I am doing wrong? At first I thought it was about the "nested" ForEach-Object but even with the code to do the search inside a function it keeps doing this.
Is it by default doing things in parallel and do I need to wait for completion of all tasks? Something telse?
I'm looking at ForEach-Object to parallelize the above script as it takes a very long time to run on a large tenant.
From the docs :
When you use the InputObject parameter with ForEach-Object, instead of piping command results to ForEach-Object, the InputObject value is treated as a single object. This is true even if the value is a collection that is the result of a command, such as -InputObject (Get-Process). Because InputObject cannot return individual properties from an array or collection of objects, we recommend that if you use ForEach-Object to perform operations on a collection of objects for those objects that have specific values in defined properties, you use ForEach-Object in the pipeline.
Change
ForEach-Object -InputObject $allExternalUsers {
find_last_login_date($_)
}
into
$allExternalUsers | ForEach-Object {
find_last_login_date($_)
}

Convert GUID string to octetBytes using PowerShell

I have a powershell script which outputs all Exchange 2003 mailboxes by size.
$computers = "vexch01","vexch02"
foreach ($computer in $computers) {
Get-Wmiobject -namespace root\MicrosoftExchangeV2 -class Exchange_Mailbox -computer $computer | sort-object -desc Size | select-object MailboxDisplayName,StoreName,#{Name="Size/Mb";Expression={[math]::round(($_.Size / 1024),2)}}, MailboxGUID | Export-Csv -notype -Path $computer.csv
}
Currently this outputs the MailboxGUID as a string type GUID (e.g. {21EC2020-3AEA-1069-A2DD-08002B30309D}). I want to look up users in AD by this, but AD stores them in octetBytes format.
I have found some powershell functions which will do the conversion but only when the curly braces are removed. The Guid.ToString method should supply this, but I can't get it to work in the above.
However, if I could figure out how to do that, the Guid.ToByteArray method might get me even closer.
Has anyone cracked this?
Update: the answers so far helped me write a function that converts the mailboxguid into the correct format for searching via LDAP. However, I now cannot get this working in the script. This is my updated script:
function ConvertGuidToLdapSearchString(
[parameter(mandatory=$true, position=0)]$Guid
)
{
$guid_object = [System.Guid]$Guid
($guid_object.ToByteArray() | foreach { '\' + $_.ToString('x2') }) -join ''
}
# Gets data through WMI from specified Exchange mailbox servers
$servers = "vexch01","vexch02"
foreach ($server in $servers) {
Get-Wmiobject -namespace root\MicrosoftExchangeV2 -class Exchange_Mailbox -computer $computer | sort-object -desc Size | select-object MailboxDisplayName,StoreName,#{Name="Size/Mb";Expression={[math]::round(($_.Size / 1024),2)}}, #{Name="LDAP Guid";Expression={ConvertGuidToLdapSearchString(MailboxGUID)}} | Export-Csv -notype -Path $server.csv
}
I'm not sure why using the function in the select-object with #{Name="LDAP Guid";Expression={ConvertGuidToLdapSearchString(MailboxGUID)}} doesn't work.
Is there another way of using this function in select-object that will give the string?
In conjunction with Andy Schneider's answer, you may find this function useful:
function Convert-GuidToLdapSearchString(
[parameter(mandatory=$true, position=0)][guid]$Guid
)
{
($Guid.ToByteArray() | foreach { '\' + $_.ToString('x2') }) -join ''
}
(I thought I had a more clever way to do this by adding a ScriptProperty to System.Guid, but I seem to have learned that you can't effectively add members to structs.)
I'm not sure I understand what you are trying to accomplish based on your comment, but I think you may have just left out a $_. Here is a somewhat contrived example that creates an object with a property that is a GUID, then uses select and Convert-GuidToLdapSearchString to convert the format. I hope it helps.
$o = New-Object PSObject -Property #{ GUID = $([Guid]::NewGuid()) }
$o
$o | select #{ Name='SearchString'; Expression={ Convert-GuidToLdapSearchString $_.GUID } }
This is not at all how I had imagined the function being used. I expected you would use it to create an LDAP search clause such as:
$searchString = Convert-GuidToLdapSearchString '{9e76c48b-e764-4f0c-8857-77659108a41e}'
$searcher = [adsisearcher]"(msExchMailboxGuid=$searchString)"
$searcher.FindAll()
Are you casting the string to a GUID ?
$guid = [System.Guid]"{21EC2020-3AEA-1069-A2DD-08002B30309D}"
$guid.ToString()
$guid.ToByteArray()

Exchange 2010 TotalItemSize.Value.ToBytes() always empty

I'm trying to create simple reports on Exchange 2010 mailbox size.
While this works:
Get-MailboxStatistics -server <serverfqdn> |ft displayname, TotalItemSize
this doesn't (second column stays empty):
Get-MailboxStatistics -server <serverfqdn> |ft displayname, {$_.TotalItemSize.Value.ToBytes()}
The problem is that I need the size as an integer, so the first line of code doesn't serve my purpose.
According to several websites the second line of code should work but unfortunately doesn't on my machine.
I know I could parse the value after using the first line but that would be unnecessarily inefficient, wouldn't it? :-)
Hope anyone can help.
Regards,
Kevin
If performing from a imported PS session the methods .ToMB() is lost as the type becomes a custom object.
The $variable.TotalItemSize.Value becomes a two element array [0] in KB,MB or GB and [1] always in bytes.
So to use this we can play with strings to achieve what we want.. in long hand for clarity
$mailboxes = Get-Mailbox -Filter{(RecipientType -eq "UserMailbox") -and (CustomAttribute12 -eq "whatever")}
foreach ($mailbox in $mailboxes)
{
$size1 = Get-MailboxStatistics $mailbox.id
[string]$bob = $size1.TotalItemSize.Value
[int]$bill = $bob.TrimEnd(" bytes)").Split("(")[1] # The bytes part of the array.
$bill = $bill/1mb # Convert into MB's
if ($bill -le 1500) {do something} Else {"Too Big " + $bill} # note -le 1500 NOT 1500MB
}
I hope this helps
this worked for me
$a = get-mailbox -id user | Get-MailboxStatistics
$a.TotalItemSize.Value.ToMB()
$a.TotalItemSize.Value.ToKB()
I have the same issue. I'm not sure if you resolved this.
I have this, which is quite ugly - but works:
$a = get-mailbox USER | get-mailboxstatistics
$intTotalItemSize = [int]$a.TotalItemSize.SubString($a.TotalItemSize.indexof("(")+1, $a.TotalItemSize.indexof(" b")-$a.TotalItemSize.indexof("("))
Try this for your size expression:
#{expression={$_.TotalItemSize.Value.ToMB()};label="Mailbox Size(MB)"}
I believe there is also a ToKB() method.
MVP Shay Levy has delved into this on his blog (http://blogs.microsoft.co.il/blogs/scriptfanatic/archive/2011/08/22/get-full-control-over-your-exchange-remote-powershell-session.aspx).
Basically, you have to modify a setting in the PowerShell virtual directory on the server that you are remoting to.
This is great news for those who are remoting to Exchange servers that they have this kind of control over, but is not helpful for those of us who use hosted Exchange solutions and cannot change these settings. I suppose we will just have to abandon some of the uber-coolness of PowerShell and go back to parsing the string to get the bytes and convert from there.
--EDIT--
This is how I tackled outputting a file of all of my users' mailbox sizes. It could be compressed a bit further, but is a little more readable this way.
$allMailboxes = Get-Mailbox -ResultSize Unlimited
ForEach ( $mailbox in $allMailboxes ) {
$itemSizeString = ( Get-MailboxStatistics $mailbox.Identity ).TotalItemSize.Value.ToString()
$posOpenParen = $itemSizeString.IndexOf("(") + 1
$numCharsInSize = $itemSizeString.IndexOf(" bytes") - $posOpenParen
$mailboxSizeInBytes = $itemSizeString.SubString($posOpenParen,$numCharsInSize).Replace(",","")
Write-Output "$($mailbox.alias),$($mailboxSizeInBytes)"
}
Please, see this article: http://blogs.technet.com/b/gary/archive/2010/02/20/the-get-mailboxstatistics-cmdlet-the-totalitemsize-property-and-that-pesky-little-b.aspx
Get-Mailbox | Get-MailboxStatistics | Add-Member -MemberType ScriptProperty -Name TotalItemSizeinMB -Value {$this.totalitemsize.value.ToMB()} -PassThru | Format-Table DisplayName,TotalItem*
I needed to have this work outside of a remoting session, so I simplified the answer from Greybear to this:
$a = get-mailbox USER | get-mailboxstatistics
$intTotalItemSize = [int64]($a.TotalItemSize -split '[\( ]')[3]
Or in the format of the original question::
Get-MailboxStatistics -Server <serverfqdn> | Select-Object -Property DisplayName,#{label="TotalItemSize";expression={[int64]($_.TotalItemSize -split '[\( ]')[3]}} | ft
Realized that [int] would fail for mailboxes over 4GB, so changed to [int64]. Alternately, display the mailboxes in MB:
Get-MailboxStatistics -Server <serverfqdn> | Select-Object -Property DisplayName,#{label="TotalItemSize";expression={[int64](([int64]($_.TotalItemSize -split '[\( ]')[3])/1048576)}} | ft
The name needs to go before the expression. This will work.
Get-MailboxStatistics -Identity [name] | select #{label=”User”;expression={$_.DisplayName}},lastlogontime,#{label=”Total Size (MB)”;expression={$_.TotalItemSize.Value.ToMB()}}
This works for me
#{Name="TotalSize (MB)"; Expression={((($_.TotalItemSize) -split " ")[0])}}
You might try:
get-mailbox -resultsize unlimited | Get-MailboxStatistics | ft displayname,#{label="Total Size (MB)";expression={$_.TotalItemSize.Value.ToMB()}}