Add NoteProperty and populate object for eventual CSV export - powershell

Full question below, here is the setup:
# Gets all room mailboxes
$rooms = Get-Mailbox -RecipientTypeDetails RoomMailbox -ResultSize Unlimited
if ( !$places ){
$places = #()
# Gets Places for Room Mailboxes
foreach ( $room in $rooms ){
$places += Get-Place $room.PrimarySmtpAddress
}
}
$populatedTags = $places | Where-Object { $_.Tags -notlike $null }
$populatedTags | Select-Object Identity, Tags
Identity Tags
-------- ----
Room1#example.com {Microsoft Teams Room}
Room2#example.com {BYOD}
Room3#example.com {Microsoft Surface Hub}
Room4#example.com {Microsoft Teams Room, Microsoft Surface Hub, BYOD}
$populatedTags[3].Tags.GetType()
IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True ArrayList System.Object
So this is the code I use to obtain data. What I am trying to achieve is a CSV that would look like this:
Identity,TAGS_Microsoft Teams Room,TAGS_BYOD,TAGS_Microsoft Surface Hub
Room1#example.com,TRUE,,
Room2#example.com,,TRUE,
Room3#example.com,,,TRUE
Room4#example.com,TRUE,TRUE,TRUE
I can add the Tags as NoteProperty's by using the below code:
$uniqueTags = $places | Select-Object Tags -Unique
$tags = $uniqueTags.Tags -join ","
$tagsArray = $tags.Split( "," )
$definitiveTags = $tagsArray | Select-Object -Unique
foreach ( $definitiveTag in $definitiveTags ){
if ( $definitiveTag -notlike $null ){
$places | Add-Member -MemberType NoteProperty -Name "TAGS_$definitiveTag" -Value $Null
}
}
Now I can do:
$places[0] | Select-Object Identity, TAGS_*
Identity : Room1#example.com
Microsoft Teams Room :
BYOD :
Microsoft Surface Hub :
Anyone have any ideas on how I populate those fields? I've been scratching my head for far too long.

One simple way is to calculate the value of all the tag properties, and add them to each place object at the same time:
# Gets all room mailboxes
$rooms = Get-Mailbox -RecipientTypeDetails RoomMailbox -ResultSize Unlimited
# Gets Places for Room Mailboxes
$places = foreach ( $room in $rooms ){ Get-Place $room.PrimarySmtpAddress }
# get the list of all used tags
$populatedTags = $places | Where-Object { $_.Tags -notlike $null }
$uniqueTags = $populatedTags | Select-Object Tags -Unique | Where Tags -NotLike $null
# iterate over places
$result = foreach ($place in $places) {
$entry = $place | select Identity #, other, properties, etc
# populate true/null values for each unique tag
foreach ($tagname in $uniqueTags) {
# calculate property value
$value = if ($place.tags -contains $tagname) {'TRUE'} else {$null}
# add property
$entry | Add-Member -MemberType NoteProperty -Name "TAGS_$tagname" -Value $value
}
# add to $result
Write-Output $entry
}
# view a table of places with any tag
$result |
# you cannot filter on a wildcard property, so have to awkwardly check like this
Where {($_.psobject.Properties|where Name -like 'TAGS_*').Value -like 'TRUE'} |
Select Identity,TAGS_*
This is good enough for reports without much data, where the calculations are very simple.

Related

Powershell to get active user OU, username, and group memberships

I am trying to get a powershell script to export all users in an OU and sub OUs which I can do fine, but when I try to get the user's OU, I get nothing for the OU. I have looked all over online and found a few scripts that pull just the user's OU, but they are a little slow and I can't seem to get them to pull groups or is for pulling from one group instead of listing all users and their groups.
I am trying to export this list and sort by OU so that I can ensure each student is in the proper groups. We have had a few students that were in extra groups and I want a quick and easy look to find those students.
#Student
$Report = #()
#Collect all users
$Users = Get-ADUser -Filter * -SearchBase 'OU=Student,DC=domain,DC=com' -Properties distinguishedname, Name, GivenName, SurName, SamAccountName, UserPrincipalName, MemberOf, Enabled -ResultSetSize $Null
# Use ForEach loop, as we need group membership for every account that is collected.
# MemberOf property of User object has the list of groups and is available in DN format.
Foreach($User in $users){
$UserGroupCollection = $User.MemberOf
#This Array will hold Group Names to which the user belongs.
$UserGroupMembership = #()
#To get the Group Names from DN format we will again use Foreach loop to query every DN and retrieve the Name property of Group.
Foreach($UserGroup in $UserGroupCollection){
$GroupDetails = Get-ADGroup -Identity $UserGroup
#Here we will add each group Name to UserGroupMembership array
$UserGroupMembership += $GroupDetails.Name
}
#As the UserGroupMembership is array we need to join element with ',' as the seperator
$Groups = $UserGroupMembership -join ','
#Creating custom objects
$Out = New-Object PSObject
$Out | Add-Member -MemberType noteproperty -Name DistinguishedName -Value #{Name="DistinguishedName";Expression={$_.distinguishedname | ForEach-Object {$_ -replace '^.+?(?<!\\),',''}}}
$Out | Add-Member -MemberType noteproperty -Name Name -Value $User.Name
$Out | Add-Member -MemberType noteproperty -Name UserName -Value $User.SamAccountName
$Out | Add-Member -MemberType noteproperty -Name Status -Value $User.Enabled
$Out | Add-Member -MemberType noteproperty -Name Groups -Value $Groups
$Report += $Out
}
#Output to screen as well as csv file.
$Report | Sort-Object DistinguishedName | FT -AutoSize
$Report | Sort-Object DistinguishedName | Export-Csv -Path $env:temp\students.csv -NoTypeInformation
There you go, I added some comments to help you understand the thought process.
This should be a lot faster than what you were doing.
The problem while adding your OUs was here:
$Out | Add-Member -MemberType noteproperty -Name DistinguishedName -Value #{Name="DistinguishedName";Expression={$_.distinguishedname | ForEach-Object {$_ -replace '^.+?(?<!\\),',''}}}
Which should've been:
$Out | Add-Member -MemberType noteproperty -Name DistinguishedName -Value ($user.distinguishedname -replace '^.+?(?<!\\),','')
#Student
$Report = [system.collections.generic.list[pscustomobject]]::new()
# Using Collection.Generic.List instead of System.Array for efficiency
#Collect all users
$Users = Get-ADUser -Filter * -SearchBase 'OU=Student,DC=domain,DC=com' -Properties MemberOf
# -> Use -SearchScope Subtree if you want to go all the way down in OU recursion starting from the 'OU=Student'
# -> distinguishedname, Name, GivenName, SurName, SamAccountName, UserPrincipalName and Enabled are Default Properties
# of Get-ADUser, no need to call them.
# -> -ResultSetSize $Null is default for Get-ADUSer, no need to call it
# Use ForEach loop, as we need group membership for every account that is collected.
# MemberOf property of User object has the list of groups and is available in DN format.
Foreach($User in $users)
{
#This Array will hold Group Names to which the user belongs.
$UserGroupMembership = [system.collections.generic.list[string]]::new()
#To get the Group Names from DN format we will again use Foreach loop to query every DN and retrieve the Name property of Group.
Foreach($UserGroup in $User.MemberOf)
{
# $GroupDetails = Get-ADGroup -Identity $UserGroup
# -> Instead of this, we can do some string manipulation
# which will be a lot faster and give you the same results.
$UserGroupMembership.Add($UserGroup.Split(',OU=')[0].replace('CN=',''))
}
#As the UserGroupMembership is array we need to join element with ',' as the seperator
$Groups = $UserGroupMembership -join ','
#Creating custom objects
<#
$Out = New-Object PSObject
$Out | Add-Member -MemberType noteproperty -Name DistinguishedName -Value #{Name="DistinguishedName";Expression={$_.distinguishedname | ForEach-Object {$_ -replace '^.+?(?<!\\),',''}}}
$Out | Add-Member -MemberType noteproperty -Name Name -Value $User.Name
$Out | Add-Member -MemberType noteproperty -Name UserName -Value $User.SamAccountName
$Out | Add-Member -MemberType noteproperty -Name Status -Value $User.Enabled
$Out | Add-Member -MemberType noteproperty -Name Groups -Value $Groups
$Report += $Out
-> Again, Add-Member is highly inefficient compared to casting PSCustomObject
-> += is evil ( •̀ᴗ•́ )و ̑̑
#>
$Report.Add(
[pscustomobject]#{
OrganizationalUnit = ($user.DistinguishedName -replace '^.+?(?<!\\),','')
Name = $user.Name
UserName = $user.sAMAccountName
Status = $user.Enabled
Membership = $Groups
})
}
#Output to screen as well as csv file.
$Report | Sort-Object OrganizationalUnit | FT -AutoSize
$Report | Sort-Object OrganizationalUnit | Export-Csv -Path $env:temp\students.csv -NoTypeInformation
I don't know how many users you have but every time you += on an array the entire array plus the new element is copied to a completely new array. This is a bad practice and gets exponentially worse with every item added the array. You can avoid this by building the arrays as a loop result or by using dotnet list object with an efficient add() method.
You also look up the same group names repeatedly. I don't know the numbers but it's probably a lot better to put all your groups in a hashtable once and then look them up.
Your question is unclear, but if you want a list of users and their groups, you are going the long way around. You mention the ou but AFAICS there is no org unit used in the code. Do you want the AD ou property or a part of the DN? You don't seem to be using either.
Note that the DN is a string and sorting by DN will just give an alpha string sort which is not helpful. Are your students in separate org units under OU=students ? This is not clear. If so, use the AD canonicalName to sort the list.
No need to include default properties in -property. Splatting is nice.
You should improve your question by indicating what your AD structure looks like and what you think your output should look like.
Also, format your code for readability.
You want something along these lines:
# group hashtable, for efficient name lookup
$groupName = #{}
$ignoredGroups = #( 'AllStudents','AllUsers', 'etc' ) # don't clutter list with these groups
Get-AdGroup -filter '*' | # any restrictions? searchbase, etc
ForEach-Object {
if ( $ignoredGroups -notcontains $_.Name ) {
$groupName[ $_.distinguishedName ] = $_.Name
}
}
# ADsplat, for readability
$AD_Splat = #{
Filter = '*'
SearchBase = 'OU=Student,DC=domain,DC=com'
Properties = 'MemberOf,CanonicalName,sn,givenName'.split(',') # split to array
ResultSetSize = $Null # !? also, there are system limits to size
}
$results = Get-ADUser #ad_splat |
ForEach-Object {
$DN = $_.distinguishedName # do you need this at all?
$CName = $_.canonicalName # for sorting by AD org unit
$XName = $_.sn + ', ' + $_.givenName
if ( $_.Enabled ) { $Enabled = 'Y'} else { $Enabled = '.' }
$groups = (
$_.memberOf |
ForEach-Object { $GroupName[ $_ ] } | # lookup name
where-Object { $_ } | # ignore nulls (when group not in hashtable)
sort-object # consistent ordering between users
) -join ';' # don't use comma, csv conflict
# leave custom object in pipe! This builds the array efficiently.
New-Object PSObject -Property #{
DistinguishedName = $dn
Name = $_.name
XName = $XName
Login = $_.SamAccountName
CName = $CName
Groups = $Groups
}
} | Sort-Object CName # sort the objects by canonical name
$results | format-table
$results | Export-Csv -Path 'c:\temp\usersgroups.csv' -NoTypeInformation

Convert Array value as object property dynamically

I have list of users and i am trying to see if they belong to specific Azure AD group. At the end i want the result to be something like this.
EmailAddress Group1 Group2
User1#email.com Y N
User2#email.com N Y
User3#email.com Y Y
Here is what i got so far:
#authenticate
Connect-MsolService
$users = "User1#email.com", "User2#email.com", "User3#email.com"
$groupLists = "Group1", "Group2"
#create the object with Email, and group name as property name
$output = New-Object -TypeName psobject
$output | Add-Member -MemberType NoteProperty -Name Email -Value ""
$groupLists | ForEach-Object{
$output | Add-Member -MemberType NoteProperty -Name $_ -Value ""
}
#go through each group and user and update the output array
$userExistsInGroup;
foreach ($groupName in $groupLists) {
#get group info
$group = Get-Msolgroup -All | Where-Object {$_.DisplayName -eq $groupName}
#get all members of the group
$members = Get-MsolGroupMember -GroupObjectId $group.ObjectId | Select-Object -ExpandProperty EmailAddress
foreach ($user in $users) {
If ($members -contains $user) {
$userExistsInGroup; = "Y"
} Else {
$userExistsInGroup = "N"
}
# update Email and group property in $output object
......
}
}
Need help updating $output object so that i can display the result the way i want it on the top?
since the same user might show up in different group during loop, if there is existing user in the object, then it will need to update property of the same user that matches with the group so that at the end each row output belongs to one user similar to what i have on the top.
To accommodate a dynamic list of groups, you can use this approach.
$userlist = "User1#email.com", "User2#email.com", "User3#email.com"
$grouplist = "Managers","Directors","Information Technology"
$grouphash = #{}
foreach($group in $grouplist)
{
$grouphash[$group] = Get-MsolGroupMember -GroupObjectId (Get-Msolgroup | Where-Object {$_.DisplayName -eq $group}).objectid
}
foreach($user in $userlist)
{
$userhash = [ordered]#{
EmailAddress = $user
}
$grouplist | ForEach-Object {
$userhash.add($_,($user -in $grouphash[$_].emailaddress))
}
[PSCustomObject]$userhash
}
Each group name will be the property for that group containing true/false if the user is a member.
To collect all the output in a variable simply put $variable = in front of the user foreach loop.
Here is what the output looks like in this example

Powershell - build a custom object with an unknown number of columns

I need to create a CSV that contains all possible emails addresses that an Active Directory user has. The CSV must be in the following format (very rigid API for where their going to be imported to):
Username | EmailAddress1 | EmailAddress2 | EmailAddressN
My script so far looks like this:
$Group = "GroupNAme
$usersObj = #()
$countMax = 0
$GetAdGroup = Get-AdGroup -Identity $Group -Properties *
[array]$members = $GetAdGroup.Members
ForEach ($member in $members) {
$currentUser = get-aduser -Identity $member `
-Properties EmailAddress, ProxyAddresses |
Where {$_.Enabled -eq "True"}
$countTemp = ($currentUser.ProxyAddresses).count
if ($countTemp -gt $countMax){ $countMax = $countTemp}
foreach ($mailAdd in $currentUser.ProxyAddresses) {
$usersOBJ += [pscustomobject]#{
'Username' = $currentUser.SamAccountName;`
'ProxyAddresses' = $mailAdd.SubString(5)
}
}
$usersOBJ | Export-CSV -NoTypeInformation C:\Export.csv
Now my existing Object spits out the Users as follows:
UserName | Emailaddress1
Username | Emailaddress2
Username | EmailsaddressN
I can't seem to make the leap into working out how to create a better object. I can get the max number of ProxyAddresses that occur but I'm not sure how to figure that into building my object and then populating the values of the $currentUser.ProxyAddresses into those columns.
I've read about Hash tables but they don't seem to fit my requirements, usually taking the form:
Username1 | Username2
Emailaddress1 | Emailaddress1
Emailaddress2 | Emailaddress2
Can anyone please point me in the right direction?
Cheers!
You just need to separate the values you're adding a bit. Add the username to a single object before the loop, then add additional properties on an as-needed basis. Finally, add the object to your array to be exported.
Try this:
...
$x = New-Object System.Object
$x | Add-Member –type NoteProperty –Name UserName –Value $currentUser.SamAccountName
$i=0
foreach ($mailAdd in $currentUser.ProxyAddresses) {
$i++
$x | Add-Member –type NoteProperty –Name "Emailaddress$i" –Value $mailAdd.SubString(5)
}
$usersOBJ += $x
Assuming you add all the users to $usersOBJ before exporting the csv, the columns should work perfectly for any number of proxy addresses.

Script output that will work on the console as well as with Export-Csv

I'm working on a basic PowerShell script that inputs a pair of dates then gets all accounts with passwords expiring between those times. I'd like to output the data to the console in a way that is compatible with Export-Csv. That way the person running the script can either just view in the console, or get a file.
Here is my script:
[CmdletBinding()]
param(
[string]$StartDate = $(throw "Enter beginning date as MM/DD/YY"),
[string]$EndDate = $(throw "Enter end date as MM/DD/YY")
)
$start = Get-Date($StartDate)
$end = Get-Date($EndDate)
$low = $start.AddDays(-150)
$high = $end.AddDays(-150)
$passusers = Get-ADUser -Filter { PasswordLastSet -gt $low -and PasswordLastSet -lt $high -and userAccountControl -ne '66048' -and userAccountControl -ne '66080' -and enabled -eq $true} -Properties PasswordLastSet,GivenName,DisplayName,mail,LastLogon | Sort-Object -Property DisplayName
$accts = #()
foreach($user in $passusers) {
$passLastSet = [string]$user.PasswordLastSet
$Expiration = (Get-Date($passLastSet)).addDays(150)
$obj = New-Object System.Object
$obj | Add-Member -MemberType NoteProperty -Name Name -Value $user.DisplayName
$obj | Add-Member -MemberType NoteProperty -Name Email -Value $user.mail
$obj | Add-Member -MemberType NoteProperty -Name Expiration -Value $expiration
$accts += $obj
}
Write-Output ($accts | Format-Table | Out-String)
This prints to the console perfectly:
Name Email Expiration
---- ----- ----------
Victor Demon demonv#nsula.edu 1/3/2016 7:16:18 AM
However when called with | Export-Csv it doesn't:
#TYPE System.String
Length
5388
I've tried multiple variations using objects, and data tables, however it seems like I can only get it to work for console or for CSV, not for both.
Replace
Write-Output ($accts | Format-Table | Out-String)
with
$accts
That way your users can run your script any way they like, e.g.
.\your_script.ps1 | Format-Table
.\your_script.ps1 | Format-List
.\your_script.ps1 | Export-Csv
.\your_script.ps1 | Out-GridView
...
Format-Table | Out-String converts your output to a single string whereas Export-Csv expects a list of objects as input (the object properties then become the columns of the CSV). If Export-Csv is fed a string, the only property is Length, so you get a CSV with one column and one record.
$accts | ConvertTo-Csv | Tee -File output.csv | ConvertFrom-Csv

Method to export objects with varying properties?

I hit a common problem with my scripting lately and decided to throw it into the wild to see how other people deal with this problem.
tl;dr; I want to export objects which have a varying number of properties. eg; object 1 may have 3 IP address but object 2 has 7 IP addresses.
I've evolved to creating a new object with custom properties and then injecting these objects into an array as my method of catching results - happy to hear if there is a better way but this is how I roll. This method works 100% when outputting to the screen as the objects are shown in list format - I've tried a number of export/out-file methods to no avail when I want to capture and store the output for reading in something like Excell.
The following is an example of me building an object and storing it (the function of the code is not important here - just the results it generates):
add-pssnapin Quest.ActiveRoles.ADManagement
$Groups = get-qadgroup AmbigousGroupNameHere-
$UserInfo = #()
ForEach ( $Group in $Groups ) {
$CurrentGroupMembers = get-qadgroupmember $Group
write-host "Processing group:" $Group
ForEach ( $GroupMember in $CurrentGroupMembers ) {
If ( $GroupMember.type -eq "User" ) {
$counter = 1
write-host "Processing member:" $GroupMember
$UserObject = get-qaduser $GroupMember | select SamAccountName,FirstName,LastName
$objUserInfo = New-Object System.Object
$objUserInfo | Add-Member -MemberType NoteProperty -Name SamAccountName -Value $UserObject.SamAccountName
$objUserInfo | Add-Member -MemberType NoteProperty -Name FirstName -Value $UserObject.FirstName
$objUserInfo | Add-Member -MemberType NoteProperty -Name LastName -Value $UserObject.LastName
$GroupMembership = get-qadgroup -ContainsMember $GroupMember | where name -like "AmbigousGroupNameHere-*"
ForEach ( $GroupName in $GroupMembership ) {
$objUserInfo | Add-Member -MemberType NoteProperty -Name CtxGroup$counter -Value $GroupName.SamAccountName
$counter++
}
$UserInfo += $objUserInfo
} else {
write-host "This is a group - we are ignoring it."
}
}
}
$UserInfo | Export-Csv UsersOutput.csv -NoType
From the above - you can see I scale the object property name by 1 for each group. CtxGroup$counter allows me to scale an object for the correct number of groups each user has. Confirmed this works great when outputting to the screen by default. The object is listed and I can see a new property for each group that matches for that user.
Now for the problem. When I export-csv or out-file the file is generated with enough headers based off the first object - so it creates the headings based on the amount of properties the first object has. So lets say the first user has 3 matching groups, it will create heading CtxGroup1, CtxGroup2, CtxGroup3. Great! No.
If the next user has 5 matching groups - only the first three are included in the output and the additional 2 are discarded as we don't have headings for CtxGroup4, CtxGroup5.
How on earth do other people deal with this?
side note; I considered creating my first object as a dummy with a massive amount of object (and hence headings) but well - that is not cool and really makes me feel inefficient.
You can obtain what you want ordering $UserInfo array by the number of properties, it can be done, but it's not so simple, in your case I would add another propertie with the count of groups added:
...
ForEach ( $GroupName in $GroupMembership ) {
$objUserInfo | Add-Member -MemberType NoteProperty -Name CtxGroup$counter -Value $GroupName.SamAccountName
$counter++
}
$objUserInfo | Add-Member -MemberType NoteProperty -Name NbCtxGroup -Value ($counter - 1)
$UserInfo += $objUserInfo
...
And then order descending the array on this propertie :
$UserInfo | Sort-Object -Property NbCtxGroup -Descending | Export-Csv .\F.csv
It's not so nice but it will do the trick.
Have a look at :
$objUserInfo.psobject.Properties | Measure-Object