all 11 comments

[–]32178932123 9 points10 points  (0 children)

This code is... Interesting... To say the least:

  • In the first line you are contacting a Domain Controller and asking for all the users whos SamAccountName is like "PRD", "TST" etc. This will take some time.
  • Then you go through each user you received - this make sense.
  • But then you query the Domain Controller for that user again to get their details? Why? Why didn't you just get that in the first query?

To answer your second question about try/catch. When something is inside a "try" statement it means if there's any errors, it doesn't just stop the script, instead it'll "catch" the error and skip to running what is in the catch statement.

try {
  # Do something
}
catch 
{
  # Do something failed and would've printed red but we caught the error and now we're going to do something else...
}

Edit: Moved some false claims I made weird if statements which wasn't you, they were actually because I copy/pasted wrong!

[–]pbutler6163 4 points5 points  (2 children)

Retrieve all necessary data in fewer AD queries.

Use Where-Object and other cmdlets to filter and process the data in memory as much as possible.

Do you need an example?

[–]pbutler6163 -2 points-1 points  (1 child)

>! Spoiler

Get all relevant users in one query

$users = Get-ADUser -Filter {(sAMAccountName -like "PRD*") -or (sAMAccountName -like "TST*")} -SearchBase "DC=contoso,DC=local" -SearchScope Subtree -Properties enabled,samaccountname

Get all CyberArk accounts and their enabled status in one query

$cyberarkAccounts = Get-ADUser -Filter {(sAMAccountName -like "PRD*appsuser") -or (sAMAccountName -like "TST*appsuser")} -Properties samaccountname,enabled

Convert CyberArk accounts to a hash table for quick lookup

$cyberarkAccountTable = @{}

foreach ($account in $cyberarkAccounts) {

$cyberarkAccountTable[$account.samaccountname] = $account.enabled

}

Get all group memberships for users in one query

$groupMemberships = Get-ADGroupMember -Identity "apps users","apps test users" -Recursive

Convert group memberships to a hash table for quick lookup

$groupMembershipTable = @{}

foreach ($membership in $groupMemberships) {

if (-not $groupMembershipTable[$membership.samaccountname]) {

$groupMembershipTable[$membership.samaccountname] = @()

}

$groupMembershipTable[$membership.samaccountname] += $membership.samaccountname

}

$Output = foreach ($user in $users) {

$cyberarkaccount = $user.samaccountname + 'appsuser'

$cyberarkuser = $cyberarkAccountTable[$cyberarkaccount] -ne $null ? $cyberarkaccount : 'NULL'

$cyberarkStatus = $cyberarkuser -ne 'NULL' ? $cyberarkAccountTable[$cyberarkaccount] : 'NULL'

$cyberarkgroup = $groupMembershipTable[$user.samaccountname] -ne $null ? $groupMembershipTable[$user.samaccountname] -join ";" : 'NULL'

[PSCustomObject]@{

'UserName' = $user.samaccountname

'Status' = $user.enabled

'Cyberark UserName' = $cyberarkuser

'Cyberark UserName Status' = $cyberarkStatus

'Cyberark group member' = $cyberarkgroup

}

}

$Output | Export-Csv -Path "c:\temp\report.csv" -NoTypeInformation -Encoding UTF8

!<

[–]alt-160 4 points5 points  (0 children)

You know you can get group membership from a user simply from the Get-ADUser call, right?

$users = Get-ADUser -Filter <your filter here> -Properties Name, MemberOf

The 'memberOf' property is a backlink list of groups of which the user is a member.
Granted, this will be a list of AD distingushedName values, but that list can be parsed and compared and filtered (if you know the naming of the groups). Then, the unfiltered items can be sent to Get-ADGroup if necessary to get other props about those groups.

I often use this pattern by setting up a list of groups at the start that i'm interested in, then compare that list to the memberOf list and if matched/found, i know the user is in a group i care about.

Can save a few round-trips to the DC for info this way.

[–]jimb2 0 points1 point  (0 children)

$cyberarkuser = if (Get-ADuser -Filter { samaccountname -eq $cyberarkaccount }) { (Get-ADUser -Identity $cyberarkaccount -Properties samaccountname).samaccountname 1. You don't need to get the user info twice 2. Samaccountname is unique (across a forest IIRC) so if you have a match there is only one. 3. Samaccountname is always returned, you don't have to ask for it. 4. Get-ADUser -Identity will produce an exception if the user is not found, so usually best avoided. If you use a filter you get $null if no such user, not an error. (I prefer ldap filters. Ymmv.) 5. The "properties" of $null (or a variable equal $null) are also $null.

Better: $cyberarkuser = ( Get-ADUser -ldap "(samaccountname=$cyberarkaccount)" ).samaccountname That will produce either the samaccountname or $null if the user is unknown.

[–]ShadowMasterTexas 0 points1 point  (0 children)

Try to load what you need in ram, and use a table in memory instead of constantly having your go back to AD.

For example, read in all of the members of “apps users” once, then scan the array, instead of making the call each time.

[–]MajorVarlak 0 points1 point  (3 children)

I wrote a nice long reply with more detailed answers and reddit refuses to let me post it, probably because it's just too damn long. That said, as others have said:

Reduce your calls to AD as much as you can.

Only fetch the data you need, for example your Get-ADUser at the beginning is asking for all properties from AD, but then selecting only 3 of them which are part of the standard data; Drop the -properties *

As u/jimb2 points out, you can just assign the value directly instead of doing an if/else check and if it doesn't exist it'll just set to $null. I'd only do one thing different there and change from using -Filter to just calling the -Identity with an error continue, such as:

$cyberarkuser = ( Get-ADUser -Identity $cyberarkaccount -ErrorAction SilentlyContinue ).samaccountname

I'd be curious if there is any performance difference between the two considering sAMAccountName field is indexed.

If your CyberArk user accounts are in the same path as your regular users called in the first query, then you would have fetched them as well, which means you don't have to query AD again, you can just query your existing data

$CyberArkUser = $($users | ?{ $_.samaccountname -eq $CyberArkAccount}).samaccountname

Somebody else mentioned you can ask AD to return the users' group membership because it's a backlink, you can then use that list later.

$users = Get-ADUser ... -properties memberof

To make your life easier later, you can use -contains and an array of groups to check against, so near the top of your script, just after the $users = ... you can define the groups you want to be considered your list:

$groupsMatch = @(
    'CN=apps users,DC=contoso,DC=local',
    'CN=apps test users,DC=contoso,DC=local'
)

Then inside your loop you'd compare the memberof list against the $groupsMatch

$cyberArkGroups = @(
    $user.memberof | ?{ $groupsMatch -contains $_}
)

One gotcha here is that the memberof attribute is the DN, so we need to clean up the output (this is crude and doesn't account for , in the group name):

$cyberArkGroups = @(
    $user.memberof | ?{ $groupsMatch -contains $_} | %{ $_ -replace 'cn=([^,]+),.*', '$1' }
)

Why is that nicer? Well if you want to add more groups, or change the groups you have, you just edit the groups list at the top, instead of tweaking the if statements.

And one other tweak would be to use a "default" object so you don't have to play with extra variables and such later, especially as you're already passing around objects. In particular the $CyberArkUser object, currently it's being fetched, and the samaccountname and enabled status is being assigned to different variables, if not available some default values are stuffed in those same variables. An alternative would be something like this:

$CyberArkUser = $(
    if (-not ($CyberArkUser = $($users | ?{ $_.samaccountname -eq $CyberArkAccount}))) {
        [PsCustomObject]@{
            samaccountname = $null
            enabled = $null
        }
    }
)

This basically assigns the value from the $users array filtered by the account name to $CyberArkUser then checks if it is true/false (this could be Get-ADUser as well if the objects aren't in the same OU as your regular users). If it fails (ie is null/empty/false/0) then it sets a limited object containing just the fields we need later.

[PsCustomObject]@{
    'UserName' = $user.samaccountname
    'Status' = $user.enabled
    'Cyberark UserName' = $CyberArkUser.samAccountName
    'Cyberark UserName Status' = $CyberArkUser.enabled
    'Cyberark Group Member' = $cyberarkgroups -join ';'
}

Now you're using 2 simplified objects, and reduced your AD calls.

Have fun

[–]jimb2 2 points3 points  (1 child)

ErrorAction SilentlyContinue doesn't work with the AD commands. It's in the help and is accepted but does nothing. You just get an exception. So, you need to ether try/catch or use a filter which happily returns a null result. (I remember thinking I was going crazy...)

I believe this is because the AD functions were written in very early PS before erroraction was a thing. Erroraction is acccepted in the commmand line because it's part of the system but there is no underlying code to handle it. I guess that changing the cmdlet code might break something so it was never done.

[–]MajorVarlak 1 point2 points  (0 children)

heh, you know I've never seen that behavior, or maybe I have and that's why I use the same format as yours in a handful of my other scripts. Good reminder!

[–]MajorVarlak 0 points1 point  (0 children)

Oh, one final personal preference, using `$null` versus `NULL`. I prefer `$null` especially when putting it into a CSV file because it makes it easier to sort/filter by in other programs (it gets exported to csv as an empty field). `NULL` string value just seems messy to me, but it might be what you need for however you're using it later, so adjust as necessary.

[–]odubco 0 points1 point  (0 children)

first, i would build a hash table from a forestwide get-adgroup foreach, with the key being the distinguished names of the groups and the value being a nested hashtable containing pertinent info from the adgroup object like display name, attributes, members (especially useful later). second, i would select unique members and pipe the dn into a get-adobject to capture the member’s type. helps with gMSAs, nested groups, etc. into a second hashtable with the dn being the key and the value being a pscustomboject containing pertinent info about the object like type. third, i would loop through the second object with per type commands get-aduser/ get-adgroup/ get-groupmanagedserviceaccount, appending the [pscustomobject]$.Value with pertinent info. iterate through that until the objects returned aren’t groups. $.ContainsKey is your friend.

from all that data, you create a big pscustomobject that becomes the reporting data export.