I'm trying to find some way of flexibly altering/substituting pipeline elements in PowerShell:
Function Where-DirectlyReportsTo {
Param (
[Parameter(
ValueFromPipeline = $true,
HelpMessage = "The ADUser object to be tested"
)]
[Microsoft.ActiveDirectory.Management.ADUser] $ADUser,
[Parameter(
Mandatory = $true,
ValueFromPipeline = $false,
Position = 0
)]
[String] $mgrDN
)
Process {
If ($ADUser) {
If ($ADUser.Manager -eq $mgrDN) { Return $ADUser }
}
}
}
$Properties = #("Manager")
$users = Get-ADUser -Filter * -SearchBase $OU -Properties $Properties
[ScriptBlock] $sb = {Where-DirectlyReportsTo "CN=Colonel Foobar,$OU"}
$DNs = $users | $sb | %{$_.DistinguishedName}
Which I want to return the DNs of all the users that report to Colonel Foobar, but it gives me the error Expressions are only allowed as the first element of a pipeline.
This is a trivial example, but I'd ultimately like to be able to put the pipeline step inside a loop and pass it different ScriptBlocks to get different sets of users, or use more complicated ScriptBlocks (e.g.: {Where-IsEmployee | Where-IsInDepartment "Finance" | Where-LikesIceCream}).
I realize that I may be going about this all wrong, and I would very much appreciate being pointed in the right direction.
EDIT: To clarify, here's a rough outline of what I'd like to accomplish:
[ScriptBlock[]] $arrBlocks = #( # lots of different cases
)
ForEach ($sb In $arrBlocks) {
$DNs = $users | $sb | %{$_.DistinguishedName}
# Then do something with the DNs
}
Realistically, this will probably involve a hash table instead of an array, so that I know what to do with each set of results.
The syntax error is simple:
# Wrong:
$DNs = $users | $sb | %{$_.DistinguishedName}
# Correct; note the call operator in front of $sb:
$DNs = $users | &$sb | %{$_.DistinguishedName}
This still leaves the matter of your pipeline hitting a dead end. You don't need to get fancy here; just pipe $Input along to the next function:
[ScriptBlock] $sb = {$Input | Where-DirectlyReportsTo "CN=Colonel Foobar,$OU"}
You shouldn't have $sb itself enumerate the input. That's extra overhead, and if you use param(), bumps the script block up to a cmdlet. You really don't need that.
In fact, you could simplify this whole thing down to four lines, or even one really long line:
$properties = #("Manager")
$managers = #(
"CN=Colonel Foobar,$OU"
"CN=Sergeant Foobar,$OU"
"CN=Random Manager,$OU"
)
$users = Get-ADUser -Filter * -SearchBase $OU -Properties $properties
$DNs = $users | ?{ $managers -contains $_.Manager } | %{ $_.DistinguishedName }
I tested that with this code:
$OU = 'OU=test'
$users = #(
#{
Manager = "CN=Colonel Foobar,$OU";
DistinguishedName = "Show me!"
}
#{
Manager = "CN=Anon Y Mous,$OU";
DistinguishedName = "Don't show me!"
}
'rabbit'
42
$null
#{
DistinguishedName = "Don't show me, either!"
}
#{
Manager = "CN=Random Manager,$OU";
DistinguishedName = "Show me, too!"
}
)
$managers = #(
"CN=Colonel Foobar,$OU"
"CN=Sergeant Foobar,$OU"
"CN=Random Manager,$OU"
)
$DNs = $users | ?{ $managers -contains $_.Manager } | %{ $_.DistinguishedName }
$DNs | Write-Host
You could make it a bit more verbose, if you wanted to:
$properties = #("Manager")
$managers = #(
"CN=Colonel Foobar,$OU"
"CN=Sergeant Foobar,$OU"
"CN=Random Manager,$OU"
)
$filter = { $managers -eq $_.Manager }
$selector = { $_.DistinguishedName }
$users = Get-ADUser -Filter * -SearchBase $OU -Properties $properties
$DNs = $users | ?{ &$filter } | %{ &$selector }
You sound like you want to eventually have multiple filters. This is pretty easy, too:
$properties = #("Manager")
$managers = #(
"CN=Colonel Foobar,$OU"
"CN=Sergeant Foobar,$OU"
"CN=Random Manager,$OU"
)
$filters = #(
{ $managers -contains $_.Manager }
{ ![string]::IsNullOrWhiteSpace($_.DistinguishedName) }
)
$selector = { $_.DistinguishedName }
$users = Get-ADUser -Filter * -SearchBase $OU -Properties $properties
$DNs = $users | ?{ $filters.Invoke() -notcontains $false } | %{ &$selector }
There needs to be something at the head of the pipeline in your scriptblock and you have to define your scriptblock as taking pipeline input.g.:
[scriptBlock]$sb = {[CmdletBinding()]param([Parameter(ValueFromPipeline=$true)]$obj) `
process {
foreach ($o in $obj) {
$o | Where-DirectlyReportsTo "CN=Colonel Foobar,$OU"}}}
You also can't throw $sb into the pipeline quite like that, try this:
$users | &$sb | %{$_.DistinguishedName}
In PS 2.0 this somewhat more concise (readable?) syntax works. (doesn't work in PS 3.0, where setting isFilter throws an exception)
$filterSet =
{$_.property -eq "desired value"},
{$_.method() -eq "I like the return value"},
{$_.gettype() -eq [special.type.I.like]},
{arbritraryBoolPSfunction $_}
$filterSet | % {$_.isFilter = $true} # make 'em all filters
ForEach ($filter In $filterSet) {
$DNs = $users | & $filter | %{$_.DistinguishedName}
# Then do something with the DNs
}
Related
The following script works perfectly, but I think it's way too complex and slow for what it needs to do.
Basically, for a list of users in a variable (manually or obtained from Get-ADUser, doesn't matter), I want to query all Domain Controllers and get the LastLogonDate for each user. I'll later use it for bad password etc.
Any suggestions on cleaning it up please that would improve my coding skills?
$UserList = "User1", "User2"
$DCs = (Get-ADDomainController -Filter *).Name
$Combined = foreach ($User in $UserList)
{
$DCarray = [ordered] #{}
foreach ($DC in $DCs)
{
$DCresponse = Get-ADUser $User -Properties DisplayName, LastLogonDate -Server $DC | Select-Object Name, DisplayName, LastLogonDate
if( -not $DCarray.Contains("Name")) { $DCarray.Add("Name",$DCresponse.name) }
if( -not $DCarray.Contains("DisplayName")) { $DCarray.Add("DisplayName",$DCresponse.DisplayName) }
if( -not $DCarray.Contains($DC)) { $DCarray.Add($DC,$DCresponse.LastLogonDate) }
}
$Return = New-Object -TypeName psobject
foreach ($Key in $DCarray.keys)
{
$Each = $DCarray[$Key]
$Return | Add-Member -MemberType NoteProperty -Name $Key -Value $Each
}
$Return
}
$Combined | Format-Table -AutoSize
I think the logic is mostly the same but this should be easier to understand and maintain. In addition, the use of the LDAPFilter should improve the runtime a bit.
$UserList = "User1", "User2", "User3"
$filter = "(|(name={0}))" -f ($UserList -join ')(name=')
# LDAP query string would look like: (|(name=User1)(name=User2)(name=User3))
$DCs = (Get-ADDomainController -Filter *).Name
$props = #{
Properties = 'DisplayName', 'LastLogonDate'
LDAPFitler = $filter
}
$result = foreach($dc in $DCs)
{
$props.Server = $dc
$users = Get-ADUser #props
foreach($user in $users)
{
# If this User's LastLogonDate attribute is NOT null
if($user.LastLogonDate)
{
[pscustomobject]#{
DomainController = $dc
UserName = $user.Name
DisplayName = $user.DisplayName
LastLogonDate = $user.LastLogonDate
}
}
}
}
$result | Sort-Object UserName, LastLogonDate | Out-GridView
I can't leave a comment, so posting as an Answer instead.
For a non-Powershell solution have a look at this tool which will retrieve the the last logon time from all DC in the forest\domain for a list of users, based on a list of samaccountnames.
https://nettools.net/last-logon-time/
Gary
I am a powershell beginner and self learner. I finally made my script to get AD Users from NestedGroup. I now want to make a cache for them to make a quicker result when I run the script on the second time. I am totally lost on what I am doing and I want to ask for some approach from professionals here. Thank you for guidance.
Here is my code.
function Get-CachedGroupMembership($groupname){
$groupName = "cached_$($groupName)"
$cachedResults = Get-Variable -Scope Global -Name $groupName -ErrorAction SilentlyContinue
if($null -ne $cachedResults){
"found cached result"
$existing = write-host "Check: i found existing"
return $cachedResults, $existing
}
else{
$searching = write-host "Check: Searching"
$results = get-adgroup $groupname -properties memberof, members
Set-CachedGroupMembership -groupName $groupName -value $results
return $searching
}
}
Function Set-CachedGroupMembership($groupName,$value){
Set-Variable -Scope Global -Name $groupName -Value $value
return $value
}
function Get-ADUsers_cached {
param (
[Parameter(ValuefromPipeline = $true, mandatory = $true)][String] $GroupName
)
[int]$circular = $null
# result holder
$resultHolder = #()
$table = $null
$nestedmembers = $null
$adgroupname = $null
# get members of the group and member of
# $ADGroupname = get-adgroup $groupname -properties memberof, members
$ADGroupname = Get-CachedGroupMembership -GroupName $groupName
# list all members as list (no headers) and save to var
$memberof = $adgroupname | select -expand memberof
if ($adgroupname) {
if ($circular) {
$nestedMembers = Get-ADGroupMember -Identity $GroupName -recursive
$circular = $null
}
else {
$nestedMembers = Get-ADGroupMember -Identity $GroupName | sort objectclass -Descending
# if get adgroupmember returns nothing, it uses the members for ordinary getADGroup
if (!($nestedmembers)) {
$unknown = $ADGroupname |select -expand members
if ($unknown) {
$nestedmembers = #()
foreach ($member in $unknown) {
$nestedmembers += get-adobject $member
}
}
}
}
# loops through each member
ForEach($nestedmember in $nestedmembers){
# creates the properties into a custom object.
$Props = #{
Type = $nestedmember.objectclass;
Name = $nestedmember.name;
DisplayName = "";
ParentGroup = $ADgroupname.name;
Enabled = "";
Nesting = $nesting;
DN = $nestedmember.distinguishedname;
Comment = ""
EmployeeNumber = "";
LastLogonDate = "";
PasswordLastSet = "";
}
# if member object is a user
if ($nestedmember.objectclass -eq "user") {
# saves all the properties in the table.
$nestedADMember = get-aduser $nestedmember.Name -properties enabled, displayname, EmployeeNumber, LastLogonDate, PasswordLastSet
$table = new-object psobject -property $props
$table.enabled = $nestedadmember.enabled
$table.name = $nestedadmember.samaccountname
$table.displayname = $nestedadmember.displayname
$table.EmployeeNumber = $nestedadmember.EmployeeNumber
$table.LastLogonDate = $nestedadmember.LastLogonDate
$table.PasswordLastSet = $nestedadmember.PasswordLastSet
#save all in 1 storage
$resultHOlder += $table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment , EmployeeNumber, LastLogonDate, PasswordLastSet
}
# if member object is group
elseif ($nestedmember.objectclass -eq "group") {
$table = new-object psobject -Property $props
# if circular, meaning the groups member of list contains one of its members.
# e.g. if group 2 is a member of group 1 and group 1 is a member of grou 2
if ($memberof -contains $nestedmember.distinguishedname) {
$table.comment = "Circular membership"
$circular = 1
}
# for circular output
#$table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment
#calling function itself
$resultHOlder += Get-ADUsers_cached -GroupName $nestedmember.distinguishedName
}
else {
if ($nestedmember) {
$table = new-object psobject -property $props
$resultHolder += $table | select type, name, displayname, parentgroup, nesting, enabled, dn, comment, EmployeeNumber, LastLogonDate, PasswordLastSet
}
}
}
}
return $resultHOlder
}
function Get-NestedGroupUsers_cached {
param (
[Parameter(Mandatory = $true)][String]$FileName,
[Parameter(Mandatory = $true)][String]$searchFileURL
)
$storageHolder = #()
$groupList = Get-Content $searchFileURL
$groupList | ForEach-Object {
$allusers = Get-ADUsers_cached -GroupName $_
$storageHolder += $allusers
}
$storageHolder | select ParentGroup, Name, EmployeeNumber, Enabled, LastLogonDate, PasswordLastSet |Export-Csv -Path "C:\Users\***\Desktop\$FileName.csv" -NoTypeInformation -Force
}
You can make use of a global or module scoped variable to store the info. Then, check for the presence of the variable on second execution and pull from that, if present.
For instance, your query of AD Groups would be a good one to cache.
I would make two functions to make this easier:
function Get-CachedGroupMembership($groupname){
$groupName = "cached_$($groupName)"
$cachedResults = Get-Variable -Scope Global -Name $groupName -ErrorAction SilentlyContinue
if($null -ne $cachedResults){
"found cached result"
return $cachedResults
}
else{
"need to cache"
$results = get-adgroup $groupname -properties memberof, members
Set-CachedGroupMembership -groupName $groupName -value $results
}
}
Then to cache the results, I'd use this function too.
Function Set-CachedGroupMembership($groupName,$value){
Set-Variable -Scope Global -Name $groupName -Value $value
return $value
}
Finally, replace the call to $ADGroupname = get-adgroup $groupname -properties memberof, members with a call to Get-CachedGroupMembership -GroupName $groupName.
If you really want to persist the cache, you could even write results out to JSON and modify these functions to retrieve them, but then you could have an issue with stale results. So, you should always leave a way to force an update to the cache, for instance you could modify these functions to add a -Force switch to update your cached value.
With a little copy and paste, you could modify these functions into Get-CachedADUser as well, and have your entire script caching results.
I have a script from a previously answered question, but don't have enough reputation to comment. I tried to run that script and came across this error message:
Export-CSV : Cannot append CSV content to the following file: C:\users.csv. The appended object does not have a property that corresponds to the following column: User;Group. To continue with mismatched properties, add the -Force parameter, and then retry the command.
How can I debug this script to resolve this issue?
Function Get-ADGroupsRecursive{
Param([String[]]$Groups)
Begin{
$Results = #()
}
Process{
ForEach($Group in $Groups){
$Results+=$Group
ForEach($Object in (Get-ADGroupMember $Group|?{$_.objectClass -eq "Group"})){
$Results += Get-ADGroupsRecursive $Object
}
}
}
End{
$Results | Select -Unique
}}
import-module activedirectory
$users = get-aduser -Filter {Name -Like "*"} -Searchbase "OU=Sample Accounts,DC=domain,DC=com" -Properties MemberOf | Where-Object { $_.Enabled -eq 'True' }
$targetFile = "C:\users.csv"
rm $targetFile
Add-Content $targetFile "User;Group"
foreach ($user in $users)
{
$Groups = $User.MemberOf
$Groups += $Groups | %{Get-ADGroupsRecursive $_}
$Groups | %{New-Object PSObject -Property #{User=$User;Group=$_}}|Export-CSV $targetfile -notype -append
}
try this function
function Get-InChainGroups
{
param (
[parameter(mandatory = $true)]
$user,
$domain)
$user1 = (get-aduser -filter { name -eq $user } -server $domain).distinguishedname
Write-verbose "checking $user"
$ldap = "(&(objectcategory=group)(groupType:1.2.840.113556.1.4.803:=2147483648)(member:1.2.840.113556.1.4.1941:=$user1))"
try { Get-ADobject -LDAPFilter $ldap -server $domain | select #{ n = 'Identity'; e = { $user } }, Name, #{ n = 'DN'; e = { $_.distinguishedname } } | ft -a }
catch { "Exception occurred" }
}
I'm trying to combine the output of two functions with the output of the default Get-ADUser-cmdlet. I'm interested in when an account was created, if it's locked and what it's name is. I also want to know when the user logged on for the last time (using multiple DC's) and if the account is being used as a shared mailbox.
I've written two custom functions Get-ADUserLastLogon and isSharedMailbox, both functions use the Write-Output function to output their output. In case of Get-ADUserLastLogon this will be Lastlogon: time and in case of isSharedMailbox this will be shared: yes/no. I'm also using a standard Get-ADUser call in a foreach loop
Now, the default output of Get-ADUser is:
SAMAccountName LockedOut Created
-------------- --------- -------
ACC False 23-10-2015 8:20:20
Output of the custom functions is as following:
Lastlogon : 1-1-1601 1:00:00
Shared: yes
What I would like is to combine the LastLogon and Shared 'headers' to be combined into the Get-ADUser. So the output would become:
SAMAccountName LockedOut Created LastLogon Shared
Code of current code, where the accounts get imported from an Excel sheet:
foreach($username in $usernameWithTld){
if ($username -eq $NULL){
break
}
$usernameWithoutTld = $username.split('\')
Get-ADUser $usernameWithoutTld[1] -Properties LockedOut, SamAccountName,
Created -ErrorAction Stop | Select-Object SAMAccountName, LockedOut,
Created
Get-ADUserLastLogon -UserName $usernameWithoutTld[1]
# Shared mailbox?
isSharedMailbox -mailboxname $usernameWithoutTld[1]
}
Function code:
function isSharedMailbox([string]$mailboxname){
$isObject = Get-ADUser -Filter {name -eq $mailboxname} -SearchBase "..." | Select-Object DistinguishedName,Name
if ($isObject -match "DistinguishedName"){
$output = "Shared: no"
Write-Output $output
} else {
$output = "Shared: No"
Write-Output $output
}
}
function Get-ADUserLastLogon([string]$userName){
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$time = 0
foreach($dc in $dcs)
{
$hostname = $dc.HostName
$user = Get-ADUser $userName | Get-ADObject -Properties lastLogon
if($user.LastLogon -gt $time)
{
$time = $user.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
Write-Output "LastLogon : $dt"
}
I'm sure there are lots of improvements that can be made, I'm still learning how to write (proper) PowerShell. I hope someone can answer my question.
You could use a Calculated Property in your Select-Object. Have a look at example 4 for the MSDN page.
In your case this would be:
Get-ADUser $usernameWithoutTld[1] -Properties LockedOut, SamAccountName, Created -ErrorAction Stop | `
Select-Object SAMAccountName, LockedOut, Created, #{Name='LastLogon';Expression={Get-ADUserLastLogon -UserName $usernameWithoutTld[1]}}, #{Name='IsSharedMailbox';Expression={isSharedMailbox -mailboxname $usernameWithoutTld[1]}}
Or even better, you can use the object(s) that Get-ADUser puts in the pipeline to in turn call your functions for that specific object, and can be useful in case your query returns multiple results:
Get-ADUser $usernameWithoutTld[1] -Properties LockedOut, SamAccountName, Created -ErrorAction Stop | `
Select-Object SAMAccountName, LockedOut, Created, #{Name='LastLogon';Expression={Get-ADUserLastLogon -UserName $_.sAMAccountName}}, #{Name='IsSharedMailbox';Expression={isSharedMailbox -mailboxname $_.sAMAccountName}}
One way to do this is to get your functions to return the values you are interested in, store them in variables, and combine everything together afterwards into a PSObject containing the properties you are interested.
The benefits of storing as an object are many. For example, you can use Select-Object, Sort-Object etc in the pipeline, or Export-CSV and other Cmdlets that expect InputObject
foreach($username in $usernameWithTld){
if ($username -eq $NULL){
break
}
$usernameWithoutTld = $username.split('\')
$adDetails = Get-ADUser $usernameWithoutTld[1] -Properties LockedOut, SamAccountName,
Created -ErrorAction Stop | Select-Object SAMAccountName, LockedOut,
Created
$lastlogin = Get-ADUserLastLogon -UserName $usernameWithoutTld[1]
# Shared mailbox?
$isshared = isSharedMailbox -mailboxname $usernameWithoutTld[1]
# putting together the PSobject
[array]$myResults += New-Object psobject -Property #{
SAMAccountName = $adDetails.SAMAccountName
LockedOut = $adDetails.LockedOut
Created = $adDetails.Created
LastLogon = $lastlogin
Shared = $shared # true/false or yes/no, depending on function
#Shared = if($shared){"yes"}else{"no"} # yes/no, based on true/false from function
}
}
Functions:
function isSharedMailbox([string]$mailboxname){
$isObject = Get-ADUser -Filter {name -eq $mailboxname} -SearchBase "..." | Select-Object DistinguishedName,Name
return ($isObject -match "DistinguishedName") # returns true/false
<# if you prefer to keep yes/no
if ($isObject -match "DistinguishedName"){
return "Yes" # no in original code
} else {
return "No"
}
#>
}
function Get-ADUserLastLogon([string]$userName){
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$time = 0
foreach($dc in $dcs)
{
$hostname = $dc.HostName
$user = Get-ADUser $userName | Get-ADObject -Properties lastLogon
if($user.LastLogon -gt $time)
{
$time = $user.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
return $dt
#Write-Output "LastLogon : $dt"
}
You can store the result of the functions in global variables and finally concatenate them is one way.
Else you can use return the output from the function and use the value later or like : $value= functionname then $value will hold the return value of the function and later you can combine the results.
function isSharedMailbox([string]$mailboxname){
$isObject = Get-ADUser -Filter {name -eq $mailboxname} -SearchBase "..." | Select-Object DistinguishedName,Name
if ($isObject -match "DistinguishedName"){
$output = "Shared: no"
$Global:result1= $output
} else {
$output = "Shared: No"
$Global:result1= $output
}
}
function Get-ADUserLastLogon([string]$userName){
$dcs = Get-ADDomainController -Filter {Name -like "*"}
$time = 0
foreach($dc in $dcs)
{
$hostname = $dc.HostName
$user = Get-ADUser $userName | Get-ADObject -Properties lastLogon
if($user.LastLogon -gt $time)
{
$time = $user.LastLogon
}
}
$dt = [DateTime]::FromFileTime($time)
$Global:result2= "LastLogon : $dt"
}
## Calling the function . Change the placeholders accordingly
Get-ADUserLastLogon -UserName $usernameWithoutTld[1]
isSharedMailbox -mailboxname $usernameWithoutTld[1]
$FinalResult = "result1" + "result2"
$FinalResult
Hope it helps you better understanding.
I have been using for Office365 Licence Tracking. Actually it looks like good but but it takes too much time to complete the process. most of time is spent by Get-MsolUser it may be improved calculating them in parallel (while processing user 1 you're already fetching user 2's data and so on...) BTW we have about 3000+ cloud user How can I improve the speed of the script?
$T1 = #()
$O365Users = Get-MsolUser -All
ForEach ($O365User in $O365Users)
{
$ADuser = Get-ADUser -Filter { UserPrincipalName -eq $O365User.UserPrincipalName } -Properties whenCreated, Enabled, lastlogondate
$O365Stats = Get-MailboxStatistics $O365User.DisplayName -ErrorAction SilentlyContinue
$O365Smtp = Get-Recipient $O365User.DisplayName -ErrorAction SilentlyContinue
If ($O365Stats -and $O365Smtp) {
If (($ADUser.Enabled -eq $true) -and ($O365User.isLicensed -eq $true))
{
$T1 += New-Object psobject -Property #{
CollectDate = $(Get-Date);
ADUserUPN = $($ADUser.UserPrincipalName);
O365UserUPN = $($O365User.UserPrincipalName);
ADUserCreated = $($ADUser.whenCreated);
ADUserEnabled = $($ADUser.Enabled);
ADLastLogonDate = $($ADUser.LastLogonDate);
O365Licensed = $($O365User.isLicensed);
O365LastLogonTime = $($O365Stats.LastLogonTime);
O365SMTPAddress = $($O365Smtp.PrimarySMTPAddress)
}
}
}
}
$T1 = $T1 | Sort-Object -Property ADUserCreated
$T1 | Format-Table
$T1 | Export-Csv -Path $OutputFile -NoTypeInformation
Write-Host "Output to $OutputFile"
Using a pipeline, filtering early on, and avoiding appending to an array should already speed things up considerably:
Get-MsolUser -All | Where-Object {
$_.IsLicensed
} | ForEach-Object {
$upn = $_.UserPrincipalName
Get-ADUser -Filter "UserPrincipalName -eq '$upn'" -Properties whenCreated, Enabled, lastlogondate
} | Where-Object {
$_.Enabled
} | ForEach-Object {
$O365Stats = Get-MailboxStatistics $_.DisplayName -ErrorAction SilentlyContinue
$O365Smtp = Get-Recipient $_.DisplayName -ErrorAction SilentlyContinue
if ($O365Stats -and $O365Smtp) {
New-Object -Type PSObject -Property #{
'CollectDate' = Get-Date
'ADUserUPN' = $_.UserPrincipalName
'O365UserUPN' = $_.UserPrincipalName
'ADUserCreated' = $_.whenCreated
'ADUserEnabled' = $_.Enabled
'ADLastLogonDate' = $_.LastLogonDate
'O365Licensed' = $true
'O365LastLogonTime' = $O365Stats.LastLogonTime
'O365SMTPAddress' = $O365Smtp.PrimarySMTPAddress
}
}
} | Sort-Object -Property ADUserCreated | Export-Csv -Path $OutputFile -NoType
Also, why the heck is everybody so infatuated with subexpressions? Use them where you need them. Don't obfuscate your code with them when they're unnecessary.
To give you a set off with parallelism in Powershell.
I would like you to go through the PS Workflows.
We have -parallel in that which will help you in parallel call.
Apart from that, we have one function for Invoke-Parallel
This is the link for it : Invoke-Parallel Function
Note: Examples are mentioned inside the function itself . You can use get-help with that function also once compiled.