Powershell export Active Directory User data not complete - powershell

I have to export the user data of accounts before we delete them. The problem is, that not all group memberships are written in the .txt file (example below).
This is the code:
Get-ADUser -Properties * -Filter "cn -eq '$name'" |
format-list -property #{Label = "Name";Expression = {$_.sAMAccountName}},
#{Label = "Initials";Expression = {$_.initials}},
#{Label = "Email";Expression = {$_.Mail}},
#{Label = "Groups";Expression = {%{(Get-ADPrincipalGroupMembership $name | select -expandproperty name)}}},
#{Label = "Creation";Expression = {$_.whenCreated}},
#{Label = "Deletion";Expression = {%{(Get-Date)}}},
#{Label = "Last change";Expression = {$_.whenChanged}} |
#write data into txt file
Out-File -append "C:\temp\deleted.txt" -Encoding utf8
And this is the output:
Name : John Doe
Initials : Jdo
Email : John.Doe#acme.com
Groups : {Domain-User, Remotedesktopuser, Administrator, Share-User...}
Creation : 23.03.2018 13:36:44
Deletion : 17.12.2018 08:46:30
Last Change : 16.12.2018 10:42:21

It's really not the Format-List causing this, the same thing would happen with a select, though using Format-* like this is not really a thing. This will be a list by default, so, no real reason to use it for what you are after.
You don't even need that expand.
The issue is the fact that you cannot use that loop and expect that to work, the auto formatters won't allow it. You have to directly handle the collection, something like this...
Get-ADUser -Properties * -Filter * |
Select-Object -property #{Label = "Name";Expression = {$_.sAMAccountName}},
#{Label = "Initials";Expression = {$_.initials}},
#{Label = "Email";Expression = {$_.Mail}},
#{Label = "Creation";Expression = {$_.whenCreated}},
#{Label = "Deletion";Expression = {%{(Get-Date)}}},
#{Label = "Last change";Expression = {$_.whenChanged}},
#{Label = "Groups";Expression = {%{(Get-ADPrincipalGroupMembership $_.SamAccountName).Name -join ','}}} |
Out-File -append "C:\temp\deleted.txt" -Encoding utf8
Get-Content -Path "C:\temp\deleted.txt"
# Results
Name : Administrator
Initials :
Email : Administrator#contoso.com
Creation : 3/31/2017 8:02:15 PM
Deletion : 12/17/2018 4:07:52 AM
Last change : 12/9/2018 7:23:22 PM
Groups : Domain Users,Administrators,Schema Admins,Enterprise Admins,Domain Admins,Group Policy Creator Owners,Organization Management,Recipient
Management,ADSyncAdmins,ADRMSSuperUsers
…
Update as per the OP comment / question
No worries, glad it worked for you.
As for ...
Would you mind to explain me what the difference between that two AD
Group commands are?
If you mean ...
Get-ADPrincipalGroupMembership Administrator | select -expandproperty
name
... vs ...
(Get-ADPrincipalGroupMembership Administrator).Name
... they are ostensibly the same thing, each producing and array list of group names.
# use the expand switch to show the group name list
Get-ADPrincipalGroupMembership Administrator | select -expandproperty name
Domain Users
Administrators
Schema Admins
Enterprise Admins
Domain Admins
Group Policy Creator Owners
Organization Management
Recipient Management
ADSyncAdmins
ADRMSSuperUsers
# Use the property to view the group name list
(Get-ADPrincipalGroupMembership Administrator).Name
Domain Users
Administrators
Schema Admins
Enterprise Admins
Domain Admins
Group Policy Creator Owners
Organization Management
Recipient Management
ADSyncAdmins
ADRMSSuperUsers
However, the formatters, when the data is being serialized, will try to put this all on one line. Yet, they will truncate this to fit the screen / page width.
So, if you want a different layout, then either you need to go in and muck with the default formatter files, or handle it is code. Personally, I never try to mess with them, and just work to handle it in code. So, this...
(Get-ADPrincipalGroupMembership Administrator).Name -join ','
... just says, I know this collection is an array list. I know that this will get truncated per screen/page width, so, concatenate this list of strings as one string and autowrap.
You could have done the same thing with your original expand the same way ...
(Get-ADPrincipalGroupMembership Administrator | select -expandproperty name) -join ','
I stuck the groups list at the end for aesthetic reasons as well as the shorter form, as I prefer not writing unnecessary code or using unneeded options as much as possible. Everyone has their preferences.

Related

Exporting last logon date for inactive users via PowerShell

I have a command that will export a list of users who have logged in for 12 months but I am struggling to export the last login date and time.
The command is as follows:
Search-ADAccount –AccountInActive -UsersOnly –TimeSpan 365:00:00:00 –ResultPageSize 2000 –ResultSetSize $null |?{$_.Enabled –eq $True} | Select-Object Name, SamAccountName, DistinguishedName, lastLogon| Export-CSV “C:\Users\Me\Desktop\InactiveUsers.CSV” –NoTypeInformation
But lastLogon is showing a blank in the CSV file.
I am new to PowerShell I understand the command can be made much smoother.
Any help on this is much appreciated.
Search-ADAccount doesn't have an option to pull other attributes from the AD Objects than the default ones, you can use Get-ADUser with an elaborate filter to query the users who haven't logged on for the past year. One option is to query the user's lastLogonTimeStamp attribute however by doing so you're risking not getting accurate results because this attribute is not replicated in real time. To get accurate one must query the user's lastLogon attribute but, since this attribute is not replicated across the Domain, one must query all Domain Controllers to get the latest logon from the user.
For more information on this topic, please check this excellent TechNet Article: Understanding the AD Account attributes - LastLogon, LastLogonTimeStamp and LastLogonDate.
$dateLimit = [datetime]::UtcNow.AddYears(-1).ToFileTimeUtc()
$AllDCs = Get-ADDomainController -Filter *
$logons = #{}
$params = #{
LDAPFilter = -join #(
"(&" # AND, all conditions must be met
"(!samAccountName=krbtgt)" # exclude krbtgt from this query
"(!samAccountName=Guest)" # exclude Guest from this query
"(userAccountControl:1.2.840.113556.1.4.803:=2)" # object is Disabled
"(lastLogon<=$dateLimit)" # lastLogon is below the limit
")" # close AND clause
)
Properties = 'lastLogon'
}
foreach($DC in $AllDCs) {
$params['Server'] = $DC
foreach($user in Get-ADUser #params) {
# this condition is always met on first loop iteration due to ldap filtering condition
if($logons[$user.samAccountName].LastLogon -lt $user.LastLogon) {
$logons[$user.samAccountName] = $user
}
}
}
$logons.Values | ForEach-Object {
[PSCustomObject]#{
Name = $_.Name
SamAccountName = $_.SamAccountName
DistinguishedName = $_.DistinguishedName
lastLogon = [datetime]::FromFileTimeUtc($_.lastLogon).ToString('u')
}
} | Export-CSV "C:\Users\Me\Desktop\InactiveUsers.CSV" -NoTypeInformation

removing 'CN' from DistinguishedName in Powershell script

I have the below script that I use to periodically export the list of users in all of the OUs in AD, and it works pretty well, but 'DistinguishedName' isn't all that great to have as an output as it makes it hard to filter the output by OU. I've been looking, and it should be possible to remove the 'CN=' portion, leaving just the OUs, but scripting is my Achilles Heel and everything I've tried to add in from examples I've found has either returned strange results or blown up when run. How could the below script be modified to make the last column the Distinguished Name minus the 'CN=_______,'?
import-module ActiveDirectory
#Set the domain to search at the Server parameter. Run powershell as a user with privilieges in that domain to pass different credentials to the command.
#Searchbase is the OU you want to search. By default the command will also search all subOU's. To change this behaviour, change the searchscope parameter. Possible values: Base, onelevel, subtree
#Ignore the filter and properties parameters
$ADUserParams=#{
'Server' = 'DC01'
'Searchbase' = 'OU=Users,OU="OU2",OU=OU1,DC=Domain,DC=Local'
'Searchscope'= 'Subtree'
'Filter' = '*'
'Properties' = '*'
}
$maxPasswordAgeTimeSpan = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge
#This is where to change if different properties are required.
$DNList | ForEach-Object{($_ -split "," | Select-Object -Skip 1) -join ","}
$SelectParams=#{
'Property' = 'DisplayName', 'SAMAccountname', 'enabled', 'lastlogondate', 'logoncount', 'passwordlastset', 'created', 'DistinguishedName'
}
get-aduser #ADUserParams | select-object #SelectParams | #DNList | export-csv "c:\temp\userlist_test_$(get-date -f yyyy-MM-dd).csv"
First things first, 'Properties' = '*' is not a good idea, you would be querying for all user's Attributes when you only need those in $ADUserParams. You should always query for those attributes you actually need as, not only, your query will run faster but also it will have less impact on your Domain Controller(s) depending on the size of it and how many objects you have in your Domain.
As for getting the Organizational Unit from the user's DistinguishedName, I personally would use any of the nice regexes shown in this question, for this particular example I'm using the one shown in mjolinor's answer which would suffice in almost any case.
The code would look like this:
$ADUserParams = #{
Server = 'DC01'
Searchbase = 'OU=Users,OU="OU2",OU=OU1,DC=Domain,DC=Local'
Searchscope = 'Subtree'
Filter = '*'
Properties = #(
'DisplayName'
'SAMAccountname'
'enabled'
'lastlogondate'
'logoncount'
'passwordlastset'
'created'
'DistinguishedName'
)
}
Get-ADUser #ADUserParams | Select-Object #($ADUserParams.Properties + #{
Name = 'OU'
Expression = { $_.DistinguishedName -replace '^CN=.+?(?<!\\),' }
}) | Export-Csv path\to\export.csv -NoTypeInformation
you can accomplish that retrieving also name attribute, then you can create a calculated property
#{l='OU';e={$_.distinguishedname -replace "CN=$($_.name),"}}
where you replace name data with in distinguishedname string.
$SelectParams=#{ 'Property' = 'DisplayName', 'SAMAccountname', 'enabled', 'lastlogondate', 'logoncount', 'passwordlastset', 'created', 'DistinguishedName','name' ,#{l='OU';e={$_.distinguishedname -replace "CN=$($_.name),"}}}
get-aduser #ADUserParams | select-object #SelectParams

Nested For Each in PowerShell to pull out AD Group Membership for AD Group

I am trying to pull out the members of a list of AD Groups, Get-ADGroupMember runs into an issue with the maximum number of members so I have tried to Get-ADGroup then expand the members but I don't get the output in the output file.
Import-Module ActiveDirectory
$LogDate = get-date -f "dd-MM-yy"
$groups = "Group1", "Group2", "Group3"
foreach ($group in $groups) {
$results = Get-ADGroup $group -properties member | select-object -expandproperty member | ForEach {
Get-ADUser "$_" -properties displayname,mail | Select GivenName,Surname,DisplayName,SamAccountName,Mail,enabled
}
$results |
Select-Object #{Label = "First Name";Expression = {$_.GivenName}},
#{Label = "Last Name";Expression = {$_.Surname}},
#{Label = "Display Name";Expression = {$_.DisplayName}},
#{Label = "Logon Name";Expression = {$_.sAMAccountName}},
#{Label = "Email";Expression = {$_.Mail}},
#{Label = "Account Status";Expression = {if (($_.Enabled -eq 'TRUE') ) {'Enabled'} Else {'Disabled'}}},
#{Label = "Group";Expression = {$group}} |
Export-csv F:\GroupMemberShip_$LogDate.csv -NoTypeInformation
}
I have tried with below PowerShell Script and able to get the list of members of a list of AD Groups.
$groups = "Group1", "Group2", "Group3"
$results = foreach ($group in $groups) {
Get-ADGroupMember $group | select samaccountname, name, #{n='GroupName';e={$group}}, #{n='Description';e={(Get-ADGroup $group -Properties description).description}}
}
$results
$results | Export-csv C:\GroupMemberShip.txt -NoTypeInformation
Reference: https://social.technet.microsoft.com/Forums/ie/en-US/c2e6bc41-30f8-444e-aa63-4cbedc79238f/get-members-list-from-multiple-ad-groups?forum=winserverpowershell
Sorry #RahulKumarShaw-MT but your answer is not very helpful.
Get-ADGroupMember cannot specify the attributes requested, like Mail. He doesn't want mail of the group but mail of the user behind the member DN.
You cannot really guess what class the member's DN may be. So using Get-ADUser on a DN that point to a group would at best return $null. Using Get-ADObject is better when working on a link attribute like member.
This kind of script is generally incredibly slow for medium/large groups. Avoid unless you know your groups are really small.
Better approach's but a bit more difficult
System.DirectoryServices.DirectorySearcher
I would personally use an Attribute Scope Query which is a very fast and powerful LDAP Query.
return the link to the DN member. (or object behind the DN)
works for any link attribute, member being the most used.
you can filter the members you want to return (i.e. only members with a mail by example by specifying a LDAP filter).
you load only the attributes you're interested in.
The only issue with this request concerns multi-domain Active Directory environnements where a group contains a member from another domain than the group's domain. I don't know exactly what a System.DirectoryServices.DirectorySearcher would return, but generally it would normally return an error like Partial Results and allow access to a list of references that you can follow.
This may be combined with ReferralChasingOption to check if references are automatically followed. But I don't think in an [Attribute Scope Query] request.
Range Request
Another option is to use Range Request to retrieve the member's DN really fast.
Then iterate over each DN (can even be done in parallel) to retrieve the desired attributes and filter if necessary. Generally this does involve using [System.DirectoryServices.Protocols] but some PowerShell modules exist on top of it like S.DS.P

How to export a multi line CSV that every item is in one cell?

I'm kinda new into the PowerShell world related to building gui's but I've managed to create a gui that in a few words, searches users in an AD and if the looked up information is correct it will store it on a CSV with the Out-File command.
An example looks like this.
Enabled : False
Locked : False
DisplayName : John Doe_test
GivenName : John
SurName : doe_test
SamAccountName : johndoe
Mail : ftestaddress#somemail.com
OfficePhone : 9999
Last Logon : 31/12/1600 21:00:00
Date Created : 7/6/2020 18:02:56
passwordlastset : 7/6/2020 18:02:56
And the code that outputs that is this one (this is the part that searches the user, displays it on a read only textbox and if the end user is right with the click of another button it will store the data. (The Write Host value is only to test the data, otherwise I'll have to enter to the csv file every time I store it.
$Formselected.controls.addrange(#($datousr,$save_btn))
$datousr.Text= Get-ADUser -filter {DisplayName -eq $username} -Properties * |Select-Object Enabled, #{Expression={$_.LockedOut};Label='Locked';}, DisplayName, GivenName, SurName, SamAccountName, Mail, OfficePhone, #{ Expression ={[DateTime]::FromFileTime($_.LastLogon)}; Label='Last Logon';}, #{Expression={$_.Created};Label='Date Created';}, passwordlastset | Out-String
$data_usr=$datousr.text
$save_btn.Add_Click{
Write-Host "$data_usr"
$data_usr |Out-File "C:\scripts\data load.csv" -Encoding UTF8 -Append -Force
}
I want to know, because it's driving me nuts how to assing "enable" on A1 and the result, which is "False" on A2 and so on because every item is in a line.
I've tried exporting to csv, but, because it comes from a variable it only stores the length of the output, not the value.
I'd like to be stored in this way:
Enabled Locked Username
False False JohnDoe
Export-CSV has a switch called -NoTypeInformation. With that appended to the cmdlet it saves the data only. However, you need to keep the data as Object for this, not converted to string as you have it now.
To do this, change the code block where the user information is gathered from AD into this:
# properties DistinguishedName, Enabled, GivenName, Name, ObjectClass, ObjectGUID, SamAccountName, SID, Surname, UserPrincipalName are returned by default.
$props = 'LockedOut','DisplayName','EmailAddress','OfficePhone','LastLogonDate','Created','PasswordLastSet'
# use the `script:` scope on the variable, so the button click has access to it
$script:data_usr = Get-ADUser -Filter "DisplayName -eq '$username'" -Properties $props -ErrorAction SilentlyContinue |
Select-Object Enabled,
#{Name = 'Locked'; Expression = {$_.LockedOut}},
DisplayName, GivenName, SurName, SamAccountName,
#{Name = 'Mail'; Expression = {$_.EmailAddress}},
OfficePhone,
#{Name = 'Last Logon'; Expression = {$_.LastLogonDate}},
#{Name = 'Date Created'; Expression = {$_.Created}},
PasswordLastSet
# display the data in the text box by formatting it as list and converting it to a string
$datousr.Text = ($data_usr | Format-List | Out-String)
Then in the code where you save the data to CSV, do:
$save_btn.Add_Click({
Write-Host $data_usr # display in the console
# save as proper CSV file (append to if the file already exists)
$script:data_usr | Export-Csv -Path "C:\scripts\data_load.csv" -Encoding UTF8 -Append -NoTypeInformation
})
Please note that it is better to name the properties you need from Get-ADUser then to use -Properties *. You don't need to add the properties returned by default (see the first code comment)
Also, I would recommend searching for the user on a user attribute other then the users DisplayName, because this tends the users of your GUI to enter variations like Bloggs, Joe vs. Joe Bloggs. EmailAddress could be more precise.

Using PowerShell to generate a unique username in AD

I'm writing a script to be used by help desk staff to quickly (and accurately) create AD user accounts by inputting basic employee information. We use portions of a persons name in order to create the samAccountName. Which I've achieve by the following.
$GivenName = "Joe"
$Surname = "Smith"
$SamAccountName = $Surname.substring(0, [System.Math]::Min(5, $Surname.Length)) + $GivenName.Substring(0,1)
This works great as long as there isn't already a "Joe Smith" (or John or Jennifer Smithers, etc). The solution would be to add a number to the end. When manually creating accounts the help desk would search AD look at what number suffix to use if necessary. I'm trying to figure out how PowerShell can do that for us. I've gone through several ideas with help from what I've found online but so far I've been unsuccessful.
My first thought was to do something like this.
$SamSuffix = 2
If ((Get-ADUser -LDAPFilter "(SamAccountName=$samAccountName)")-eq $Null)
{
"$SamAccountName does not exist in AD" #out result for testing.
}
Else{
do
{
Get-ADUser -LDAPFilter "(SamAccountName=$SamAccountName + $SamSuffix++)"
}
until (Get-ADUser -LDAPFilter "(SamAccountName=$SamAccountName + $SamSuffix)")-eq $Null)
}
This obviously doesn't work. Even if it did I don't know how I'd get to the 'next step' to create the account.
I also tried pulling the existing names into a list
$SamExist = (Get-ADUser -LDAPFilter "(SamAccountName=$SamAccountName*)" | Select SamAccountName)
do {$SamAccountName + $SamSuffix++}
until ($SamExist -notcontains $SamAccountName -or $SamAccountName + $SamSuffix)
This also doesn't work but if it did I can see that it would automatically add the suffix even if it wasn't needed.
You approach where you get all the existing matches first would be where I would start. Lets assume $SamAccountName is smithj
$existingAccounts = Get-ADUser -Filter "samaccountname -like '$SamAccountName*'" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty samaccountname
So $existingaccounts that have a samaccountname's starting with smithj. If there are not any then $existingAccounts would be null and we can test for that.
if($existingAccounts){
# Figure out what the suffix will be
$highestValue = $existingAccounts -replace "^$SamAccountName" |
ForEach-Object{[int]$_} |
Measure-Object -Maximum |
Select-Object -ExpandProperty Maximum
} else {
# Create the user as normal.
}
Pretending there are some accounts that exist we trim the leading characters from the samaccountname, convert the remaining to an integer and select the highest from that. So $highestValue is the last number used in a conflicting account.
Add one to that and you have a guaranteed username you can create assuming nothing changes in those moments i.e. two users making to smithj accounts.
If you are looking to fill gaps where a user might have left and you want to use the next available after 1 then you could do this.
$existingAccounts = "smithj1", "smithj5", "smithj10", "smithj2", "smithj3"
# Figure out what the next unused suffix will be
$existingSuffixes = $existingAccounts -replace "^$SamAccountName" | ForEach-Object{[int]$_}
# Once again determine the maximum one in use
$highestValue = $existingSuffixes | Measure-Object -Maximum | Select-Object -ExpandProperty Maximum
# Find the first gap between 1 and the max suffix
$nextAvailableSuffix = 1..($highestValue + 1) | Where-Object{$existingSuffixes -notcontains $_} | Sort-Object desc | Select -First 1
$nextAvailableSuffix would contain 4 using the above example. We add 1 to highest value in case the only other one is 2 so that way there will only be an answer to $nextAvailableSuffix