Read CSV and loop through in PowerShell - powershell

I am trying to Import a list of endpoints I have which is saved to a CSV, Loop through the list and Display the DeviceName, Owner, Depatment, Jobtitle. Please help.
$Endpoints = Import-Csv -Path "C:\Users\Ryan\Endpoints.csv"
$Result = #()
$Users = Get-AzureADDevice -filter $Endpoints.Endpoint | select DisplayName, objectID
$Users | ForEach -Object {
$user = $_
Get-AzureADDeviceRegisteredOwner -ObjectId $user.ObjectId -All $true |
ForEach -Object {
$Result += New-Object PSObject -property #{
DeviceOwner = $_.DisplayName
DeviceName = $user.Displayname
Department = $_.Department
Jobtitle = $_.Jobtitle
}
}
}
$Result | Select DeviceOwner, DeviceName, Department, Jobtitle
I had this working without Import using a static Endpoint Name e.g. $Users = Get-AzureADDevice -SearchString "LaptopName"

Based on assumptions this might be what you're looking for, note the outer loop to iterate over each row of your CSV:
Import-Csv -Path "C:\Users\Ryan\Endpoints Society\Desktop\Endpoint.csv" | ForEach-Object {
$devices = Get-AzureADDevice -SearchString $_.Endpoint
if(-not $devices) { return "$($_.EndPoint) not found, skipping" }
foreach($device in $devices) {
foreach($owner in Get-AzureADDeviceRegisteredOwner -ObjectId $device.ObjectId -All $true) {
[pscustomobject]#{
DeviceOwner = $owner.DisplayName
DeviceName = $device.Displayname
Department = $owner.Department
Jobtitle = $owner.Jobtitle
}
}
}
}

Not your answer, but how to find it
This script suffers greatly from 'one-liner-itis'. It is very hard to follow and going to be impossible to debug because of its over-the-top reliance on pipes and foreach-object commands§
Step one to make this even testable is to unwrap these commands into stand-alone commands, then you can debug line by line and see what the values are at that line.
$Endpoints = Import-Csv -Path "C:\Users\Ryan\Endpoints.csv"
$Result=#()
$devices= Get-AzureADDevice -filter $Endpoints.Endpoint |
select DisplayName, objectID
#for debugging, we will only look at the first two devices
$devices = $devices[0..1]
foreach($device in $devices){
$thisDeviceOwners = $_Get-AzureADDeviceRegisteredOwner -ObjectId $user.ObjectId -All $true
foreach($owner in $thisDeviceOwners){
$Result += New-Object PSObject -property #{
DeviceOwner = $_.DisplayName
DeviceName = $user.Displayname
Department = $_.Department
Jobtitle = $_.Jobtitle
}
}
}
$Result | Select DeviceOwner, DeviceName, Department, Jobtitle
Next, just run this one time in your IDE of choice, like PowerShell ISE or Visual Studio Code, then look to see what values you have for these variables. One of them will be empty or odd, and therein lies your problem:
$device
$devices
$thisDeviceOwners
$owner
Note that I changed $devices to only be the first two items in $devices, so you can see how the code is working. Find and fix your problem with one or two items and then the whole script will likely work.
§ - there is nothing wrong with your approach, and in fact PowerShell was built from the ground up to be like the Unix Bash Shell, loads of small commands that do one thing, and can be used to pass results on to the next command. It allows for ad-hoc powerful control.
But in an enterprise script, we should not use one-liners and should instead use commonly accepted coding practices. Each line of code should do only one thing, and shouldn't wrap around.

Related

Create csv file of all disabled AD users with mailboxes Output information from multiple cmdlets in powershell

I am trying to gather some information on disabled user accounts that have mailboxes. I am specifically looking for just user mailboxes not shared mailboxes.
Here is what I have so far.
$Mailboxes = Get-Mailbox | where {$_.RecipientTypeDetails -eq 'UserMailbox'}
$date = get-date -f "MMddyyyy_HHmm"
$Disabled = #()
Foreach ($Mailbox in $Mailboxes) {
if((Get-ADUser -Identity $Mailbox.SamAccountName).Enabled -eq $False){
$Disabled += Get-MailboxStatistics $Mailbox.SamAccountName | Select -Property DisplayName,TotalItemSize
}
}
$Disabled | Sort DisplayName | Export-Csv -Path "%path%\DisabledADUsersWithMailbox_$date`.csv" -NoTypeInformation
Additionally what I would like to collect is the users Title, Manager, LastlogonDate all of which can be found using Get-Aduser. I am unsure how I go about collecting the information from both cmdlets and then exporting it all to csv. I have read that I may need to create a custom object. I am struggling with setting that up in this script.
Any help would be much appreciated.
Thanks
the following lines should give you what you want, can't verify it as I have no exchange running here.
$date = get-date -f "MMddyyyy_HHmm"
$Disabled = #(
Foreach ($Mailbox in $Mailboxes) {
$adUser = get-aduser -Identity $Mailbox.SamAccountName -Properties enabled,manager,title,lastlogontimestamp
If ($adUser.Enabled -eq $False){
$mailStats = Get-MailboxStatistics $Mailbox.SamAccountName
$attrsht = [ordered]#{
displayname=$mailstats.displayname
totalitemsize=$mailStats.totalitemsize
samaccountname=$aduser.samaccountname
enabled=$aduser.enabled
manager=$aduser.manager
title=$aduser.title
lastlogontimestamp=[datetime]::FromFileTime($aduser.lastlogontimestamp)
}
new-object -TypeName psobject -Property $attrsht
}
}
)
$Disabled | Sort-Object DisplayName | Export-Csv -Path "%path%\DisabledADUsersWithMailbox_$date`.csv" -NoTypeInformation
Avoid adding elements to an array by using +=. It is slow, alternatively take a look at generic array lists.

How to optimize query across all domain controllers to find the latest logon date?

I'm currently trying to query the domain controllers to find the latest logon date for users. I know I can use the lastlogondate but that doesn't get replicated as often as I'd like and I have no control over the that. I tried making a script to query each domain controller and check it against a hashtable of users but this is proving to be really slow to even query one DC. I know I could run background jobs for each DC but still just going all the latest logon times for one domain controller is taking a long time. Any advice?
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$users = Get-ADUser -filter * -SearchBase "OU=users,DC=com"
$outfile = "c:users.csv"
$usertable = #{}
# creating table for all users
foreach($user in $users) {
$userobject = New-Object PSObject
$userobject | Add-Member -MemberType NoteProperty -Name DomainController -Value "0"
$userobject | Add-Member -MemberType NoteProperty -Name LastLogon -Value "0"
$usertable.Add($user.SamAccountName ,$userObject)
} # end foreach
# Method for looping through domain controllers to find last logon
foreach($dc in $dcs) {
$hostname = $dc.HostName
$logons = Get-ADUser -Filter * -SearchBase "OU=users,DC=com" -Property LastLogon -Server $hostname
foreach($u in $logons) {
$sam = $u.SamAccountName
if($u.LastLogon -gt $usertable.$sam.LastLogon) {
$usertable.$sam.LastLogon = $u.LastLogon
$usertable.$sam.DomainController = $dc.HostName
Write-Host "$sam has the latest date"
$usertable.$sam.DomainController
}
} #end inner foreach
} #end outer domain foreach
foreach($h in $usertable.GetEnumerator()) {
$sam = $($h.Name)
$newdate = $($h.Value).LastLogon
$append = "$sam,$newdate"
$append | out-file $outfile -Encoding ascii -Force -Append
}
I think I've figured this out. It's very clear from testing and comparing with my own similar program that the delay is in the following loop:
foreach($u in $logons) {
$sam = $u.SamAccountName
if($u.LastLogon -gt $usertable.$sam.LastLogon) {
$usertable.$sam.LastLogon = $u.LastLogon
$usertable.$sam.DomainController = $dc.HostName
Write-Host "$sam has the latest date"
$usertable.$sam.DomainController
}
} # end inner foreach
Now my program is not exactly the same as yours. One could sanitize this, but for all intents and purposes the below segment serves the same function as the above excerpt:
# Note: the re-use of $DC...
ForEach($DC in $RDCs )
{ # Should you add any measurements here??
Write-Host "Working on $DC : $(Get-Date -Format G) ..."
Get-ADUser -Filter $Filter -SearchBase $SearchBase -Server $DC -Properties $AD_Props |
ForEach-Object{
If ( $Users.Contains( $_.samAccountName ) )
{ # So you don't attemp when it wasn't in the root set.
If( $_.lastlogon -gt $Users[$_.samAccountName].LastLogon )
{
$Users[$_.samAccountName].LastLogon = $_.lastlogon
$Users[$_.samAccountName].DC = $DC
}
}
}
}
You can see that I'm using a ForEach-Object loop instead of the ForEach construct. However, this isn't a sufficient explanation, especially seeing as the latter is often faster.
Note: I timed your loop in several different ways and accounted for code earlier in the script. For example, I'm not creating the objects the same way, but I ruled that out as a factor.
Not able to figure out why your loop would be "dramatically" slower I relucantly rewrote it to:
$logons |
ForEach-Object{
If( $_.LastLogon -gt $usertable[$_.samAccountName].LastLogon )
{
$usertable[$_.samAccountName].LastLogon = $_.LastLogon
$usertable[$_.samAccountName].DomainController = $hostname
However, this didn't seem much faster if at all. So, even more reluctantly I tried it like:
Get-ADUser -Filter * -SearchBase $UsersOU -Property LastLogon -Server $hostname |
ForEach-Object{
If( $_.LastLogon -gt $usertable[$_.samAccountName].LastLogon )
{
$usertable[$_.samAccountName].LastLogon = $_.LastLogon
$usertable[$_.samAccountName].DomainController = $hostname
}
}
Now I started seeing performance similar to my own program. In fact, it seemed a little faster, probably due to not checking the Hash.
Based on experience I can only hypothesize that the objects are live and maintaining some kind of connection back to the source DC. I've seen this type of thing once before in the PowerShell Community Extensions (PSCX) the objects returned from Get-TerminalSession exibited similar behavior. I could be crazy, I'm happy to be corrected and frankly hoping someone has a better explanation.
There are a few other things I can advise here, none as impactful as above. Please also note, I didn't test through to the end, and so can't guarantee there aren't other issues.

find members of groups excluding disabled users

I have 10 security groups all that are called " 'companyname' RDS Users"
I am trying to create a script that does the following: List all the groups and then list all of the members excluding the disabled members, then have it email a csv. I have done the following but cant get the disabled user excluded.
The Script belows shows how far i got but the disabled users show in there which basically means the script is pointless.
$mailServer = ""
$mailFrom = ""
$mailTo = ""
$mailSubject = ""
$file = "somepath\RDSUsers.csv"
Import-Module ActiveDirectory
$US = Get-ADUser -Filter * -Property Enabled |where {$_.Enabled -eq "True"}| FT Name, Enabled -Autosize
$Groups = (Get-AdGroup -filter * | Where {$_.name -like "*RDS Users" -and $_.name -ne "RDS Users"}| select name -expandproperty name)
$Table = #()
$Record = [ordered]#{
"Group Name" = ""
"Name" = ""
"Username" = ""
}
Foreach ($Group in $Groups)
{
$Arrayofmembers = Get-ADGroupMember -identity $Group |select name,samaccountname
foreach ($Member in $Arrayofmembers)
{
$Record."Group Name" = $Group
$Record."Name" = $Member.name
$Record."UserName" = $Member.samaccountname
$objRecord = New-Object PSObject -property $Record
$Table += $objrecord
}
}
if ($Table -eq "RDS Users") {}
$Table
there is usualy a line here that sends the email with excel attachment
The following should produce the output you want in the $Table variable. You can then pipe $Table to one of the format-* commands.
Import-Module ActiveDirectory
$US = Get-ADUser -Filter "Enabled -eq '$true'" -Property Enabled
$Groups = Get-ADGroup -Filter "Name -like '*RDS Users' -and Name -ne 'RDS Users'" |
Select-Object -ExpandProperty Name
$Table = Foreach ($Group in $Groups)
{
try
{
$Arrayofmembers = Get-ADGroupMember -Identity $Group -ErrorAction Stop | Select-Object Name, SamAccountName
$compare = Compare-Object -ReferenceObject $US -DifferenceObject $Arrayofmembers -ExcludeDifferent -IncludeEqual -PassThru -Property SamAccountName -ErrorAction Stop |
Select-Object Name, SamAccountName
$compare | ForEach-Object {
[pscustomobject]#{
"Group Name" = $Group
"Name" = $_.Name
"UserName" = $_.SamAccountName
}
}
}
catch
{
[pscustomobject]#{
"Group Name" = $Group
"Name" = $null
"UserName" = $null
}
Continue
}
}
$Table
Explanation:
The Get-ADGroupMember command will not provide the Enabled property of its returned objects. You will need to feed its output into another command like Get-ADUser for that data. Since you already stored all of the enabled users in $US, we can simply compare $US collection to the results of each Get-ADGroupMember output.
I removed most of the Where-Object commands in favor of using the -Filter parameter on the AD commands. Almost always, the -Filter parameter will be faster especially when you are comparing AD indexed attributes like Name and Enabled.
You do not need to store each output object in a variable unless you are going to further manipulate it. This is why $Record was removed. Instead, all returned objects are stored in the array $Table. I removed the += operator mainly because of its inefficiency when repeatedly building arrays. Also, you can simply set a variable to the output of a foreach loop, which will result in the array you require. Since we created a custom object on each loop iteration and provided the properties at the time of declaration, [ordered] is not required. However, if you create the hash table first and then create a corresponding object, you will potentially need to use [ordered]. As an aside when you are creating custom objects that are involved in a loop, it is usually best practice to create a new object each time. Otherwise, you could unintentionally update values on the wrong objects. Just because you add an object to an array, you can still update its properties after the fact.
The Compare-Object command ties everything together. The -ExcludeDifferent -IncludeEqual parameter combination will only output objects with matching property values. Since we are comparing $Arrayofmembers and $US, that is ideal. The -PassThru switch allows the objects to be returned with all of the properties that were passed into the command. Then you can use the Select-Object command to pick which properties matter to you.

How to get list of selected AD Groups, that a large list of users are members of?

I have the below working script that checks if a large list of users in a CSV file are a member of an AD group and writes the results to results.csv.
Not sure how to convert the script so I can change $group = "InfraLite" to $group = DC .\List_Of_AD_Groups.CSV.
So the script doesn't just return matches for one AD group but so it returns matches for the 80 AD groups contained in the List_of_AD_groups.csv also. Writing a YES/NO for each AD group in a new column in the CSV (or if that's not possible creating a seperate .csv file for each group with results would do also.
I could do this manually by changing the value of $group and export file name, and re-running the script 80 times but must be a quick was with PS to do this?
e.g. results.csv:
NAME AD_GROUP1 AD_GROUP2 AD_GROUP80 etc etc.
user1 yes no yes
user2 no no yes
user3 no yes no
echo "UserName`InfraLite" >> results.csv
$users = GC .\user_list.csv
$group = "InfraLite"
$members = Get-ADGroupMember -Identity $group -Recursive |
Select -ExpandProperty SAMAccountName
foreach ($user in $users) {
if ($members -contains $user) {
echo "$user $group`tYes" >> results.csv
} else {
echo "$user`tNo" >> results.csv
}
}
I played with this for a while, and I think I found a way to get you exactly what you were after.
I think Ansgar was on the right path, but I couldn't quite get it to do what you were after. He mentioned that he didn't access to an AD environment at the time of writing.
Here is what I came up with:
$UserArray = Get-Content 'C:\Temp\Users.txt'
$GroupArray = Get-Content 'C:\Temp\Groups.txt'
$OutputFile = 'C:\Temp\Something.csv'
# Setting up a hashtable for later use
$UserHash = New-Object -TypeName System.Collections.Hashtable
# Outer loop to add users and membership to UserHash
$UserArray | ForEach-Object{
$UserInfo = Get-ADUser $_ -Properties MemberOf
# Strips the LPAP syntax to just the SAMAccountName of the group
$Memberships = $UserInfo.MemberOf | ForEach-Object{
($_.Split(',')[0]).replace('CN=','')
}
#Adding the User=Membership pair to the Hash
$UserHash.Add($_,$Memberships)
}
# Outer loop to create an object per user
$Results = $UserArray | ForEach-Object{
# First create a simple object
$User = New-Object -TypeName PSCustomObject -Property #{
Name = $_
}
# Dynamically add members to the object, based on the $GroupArray
$GroupArray | ForEach-Object {
#Checking $UserHash to see if group shows up in user's membership list
$UserIsMember = $UserHash.($User.Name) -contains $_
#Adding property to object, and value
$User | Add-Member -MemberType NoteProperty -Name $_ -Value $UserIsMember
}
#Returning the object to the variable
Return $User
}
#Convert the objects to a CSV, then output them
$Results | ConvertTo-CSV -NoTypeInformation | Out-File $OutputFile
Hopefully that all makes sense. I commented as much of it as I could. It would be very simple to convert to using ADSI if you didn't have RSAT installed on whatever machine you're running this on. If you need that let me know, and I'll make some quick modifications.
I've also tossed a slightly modified version of this in a Gist for later reference.
The trivial solution to your problem would be to wrap your existing code in another loop and create an output file for each group:
$groups = Get-Content 'C:\groups.txt'
foreach ($group in $groups) {
$members = Get-ADGroupMember ...
...
}
A more elegant approach would be to create a group mapping template, clone it for each user, and fill the copy with the user's group memberships. Something like this should work:
$template = #{}
Get-Content 'C:\groups.txt' | ForEach-Object {
$template[$_] = $false
}
$groups = #{}
Get-ADGroup -Filter * | ForEach-Object {
$groups[$_.DistinguishedName] = $_.Name
}
Get-ADUser -Filter * -Properties MemberOf | ForEach-Object {
$groupmap = $template.Clone()
$_.MemberOf |
ForEach-Object { $groups[$_] } |
Where-Object { $groupmap.ContainsKey($_) } |
ForEach-Object { $groupmap[$_] = $true }
New-Object -Type PSObject -Property $groupmap
} | Export-Csv 'C:\user_group_mapping.csv' -NoType

Value from one command as parameter to another command

I am trying to figure out how to get a value from one command as a parameter to another command, and use the output of both in a single table. Specifically I am using two cmdlets, Get-Mailbox and Get-MailboxStatistics (this is against an Exchange 2010 server).
From Get-Mailbox I need the DisplayName, UseDatabaseQuotaDefaults and Database fields. Added to this, I need to get from Get-MailboxStatistics the TotalItemSize and StorageLimitStatus fields.
I can run each of these commands individually, but cannot figure out how to use the DisplayName value from Get-Mailbox fed into the Identity value for Get-MailboxStatistics command and then output the whole lot into a single table.
I was trying something along these lines:
get-mailbox | ForEach-Object {write-host $_.DisplayName, $_.UseDatabaseQuotaDefaults, $_.Database, Get-MailboxStatistics $_.SamAccountName}
Instead of actually processing the Get-MailboxStatistics as a command it just display it as text. How can I get PS to treat this as a command and not text for the write-host cmdlet?
You need to use parentheses, something along these lines:
get-mailbox | ForEach-Object { Write-Host `
$_.DisplayName, `
$_.UseDatabaseQuotaDefaults, `
$_.Database, `
(Get-MailboxStatistics $.SamAccountName) }
# ^------- note the parentheses ---------^
Using bits of info from the previous answers, combined with some Google searching and more than my fair share of cursing, came up with the following script that actually works:
$mbx = Get-Mailbox
ForEach ($cur in $mbx)
{
$stat = (Get-MailboxStatistics $cur.DisplayName)
New-Object PSObject -Property #{
DisplayName = $cur.DisplayName
UseDatabaseQuotaDefaults = $cur.UseDatabaseQuotaDefaults
SamAccountName = $cur.SamAccountName
StorageLimitStatus = $stat.StorageLimitStatus
TotalItemSize = $stat.TotalItemSize
Database = $stat.Database
}
}
thanks everyone!
Get a list of all mailboxes, for each one assign its statistics to a variable, then create a custom object with properties from both objects:
Get-Mailbox | ForEach-Object {
$stats = $_ | Get-MailboxStatistics
New-Object PSObject -Property #{
DisplayName = $_.DisplayName
UseDatabaseQuotaDefaults = $_.UseDatabaseQuotaDefaults
Database = $_.Database
SamAccountName = $_.SamAccountName
TotalItemSize = $stats.TotalItemSize
StorageLimitStatus = $stats.StorageLimitStatus
}
}