#StandWithUkraine | AD's Blog - ActiveDirectory2023-09-04T12:57:25+02:00urn:md5:b3d750c750593265108ebdd12bcf7fdaDotclearGet a List of Enabled Users in Group(s)urn:md5:e2fc0f78b968f11d124c4a713078c09d2023-01-08T12:03:00+01:002023-01-10T09:28:33+01:00Andrii DykhlinActiveDirectorypowershell<p>In case you have an audit, and being asked to provide all the members of the group(s), and have a very specific pattern (like ending with "RW" as a random example), and you are not removing the users from the groups when they leave the company (anything if not everything is possible), you can use the following function (as it is, or add it to your <a class="ref-post" href="https://dykhl.in/post/9">$profile</a>).</p> <p>To do the above, all we need to do is to create a function that accepts the mandatory parameter to match with our need. The line 8 check for active users only, you can remove it if you like. Also, you can add Export-CSV with the file you like, also it accepts the pipeline input, that means, you can provide a set of strings separated by comma before the pipeline and add the function with no parameters in the end, for example:</p>
<pre>
<code class="language-powershell">"Group 1", "RW", "Domain" | Get-EnabledADGroupMember</code></pre>
<p>You can also change the <strong>Get-ADGroupmember</strong> with something like <strong>(Get-ADGroup $Group -properties member).member</strong> in case you get the unspecified error, but it doesn't support a recursive search.</p>
<p>Here is the script. You don't need to specify the domain unless you have a domain trust with someone else's, and want to limit the results for this or another reason:</p>
<div class="container" title="Hint: double-click to select code">
<div class="line number1 index0 alt2" data-bidi-marker="true"> </div>
<pre>
<code class="language-powershell">function Get-EnabledADGroupMember {
[CmdletBinding()]
param(
[Parameter(Mandatory,ValueFromPipeline)]
[string]$ADGroup
)
process {
$groups=(Get-ADGroup -filter "name -like '*$ADGroup*'" | sort name).name
foreach ($group in $groups) {
$Members = Get-ADGroupMember $Group -recursive | Get-ADUser -Server contoso.com -ErrorAction Ignore | Sort-Object samaccountname | ?{$_.enabled -eq $True}
foreach ($Member in $Members) {
[PSCustomObject]@{
"Group" = $Group
"Name" = $Member.Name
"SamAccountName" = $Member.samaccountname
"Enabled" = $Member.enabled
}
}
}
}
}</code></pre>
<div class="line number22 index21 alt1" data-bidi-marker="true"> </div>
<div class="line number22 index21 alt1" data-bidi-marker="true">Let's break it down and explain some lines from above.</div>
<div class="line number22 index21 alt1" data-bidi-marker="true"> </div>
<div class="line number22 index21 alt1" data-bidi-marker="true">Firstly, we specify the function name, and the input parameter. We have just one, so it's easy. Also, it will be mandatory, so you will be asked to enter something even if you just type the function. The ValueFromPipeline is explained a bit above. It basically allows you to specify more inputs at the same time, which might be useful in some complex cases.</div>
<div class="line number22 index21 alt1" data-bidi-marker="true"> </div>
<div class="line number22 index21 alt1" data-bidi-marker="true">In the process section, we specify a set of groups that match the pattern. It takes the name, and puts everything after and before the string, so if you select "RW", it will match "Photos_<strong>RW</strong>", "S<strong>RW</strong>ork" and "<strong>RW</strong>orkers", so if you really want it to end or start with it, please update the filter.</div>
<div class="line number22 index21 alt1" data-bidi-marker="true">We also want to ignore the warnings and proceed with just a list of groups. After all, we create a custom PS object, which is basically a table, and put what we need there. And as far as you check for enabled users only, you might not need the part with that column after all (Line 17)</div>
</div>Check Sensitive Groups Membershipurn:md5:5a23add01d5cf64c6a0f23c2ac27bfcc2021-12-21T09:11:00+01:002022-01-01T20:22:17+01:00Andrii DykhlinActiveDirectorypowershell<p style="text-align: justify;">In the world of audit and escalated accounts, you will need to have an overview of the privileged groups and members there, this could be a regulator requirement, too. This small script will show how to do this, and have just one file per check with all the groups you need. Of course, it could be applied to any set of groups, no need to limit yourself.</p> <p>We will use the text file with the list of groups in it, as in our example we have the groups in several OUs and for different purposes, so we can have either a list of the groups in the script itself (which could be harder to maintain and to overview), or we can have a file with that list. The groups are put one per line, and it is more visible overall. In this example, our text file is located in the same directory as the script (LIne 5), so it is possible to use the <strong>.\</strong> notation, but the absolute path might be more reasonable in general. Just to note, if you want to replace the file with the groups in the script itself, you can put the in a form of <em>$Group=("Group 1", "Group 2", "Group 3", ..., "Group N")</em>.</p>
<pre>
<code class="language-powershell">$Day = Get-Date -uformat "%Y-%m-%d"
$File = "Admin_" + $Day + ".csv"
$Path="\\Path\to\the\$File"
$Groups = (Get-Content .\ADGroups.txt)
$Table=@()
$Record = [ordered]@{"Group" = ""; "SamAccountName" = ""; "Name" = ""; "Enabled" = ""}
Foreach ($Group in $Groups) {
$Members = Get-ADGroupMember -identity $Group -recursive | ? {$_.objectclass -eq 'user'} | Get-ADUser
foreach ($Member in $Members) {
$Record."Group" = $Group
$Record."Name" = $Member.Name
$Record."SamAccountName" = $Member.samaccountname
$Record."Enabled" = $Member.enabled
$objRecord = New-Object PSObject -property $Record
$Table += $objrecord
}
}
$Table | export-csv $Path -NoTypeInformation</code></pre>
<p>On the first lines, we create a file name to be saved in the desired location. I am using the file server, but it's up to you where to have it - later on you can make the scheduled task and just check for the files afterwards.</p>
<p>Then, we create a few objects: an empty array <strong>$Table</strong> and an empty hashtable with the ordered keys <strong>$Record</strong>, It will ensure we will have the result we really want to see. In my particular case, I wanted to see the group name on the first place, then the sAMAccountName of the user, display name, and finally, if the user is enabled or not.</p>
<p>Then, we go through each line of the file (or through each object in the array/OU), and verify that we are checking for the users for sure, and do that recursively, so the nested groups (when the user is a member of Group A, and not Group B directly, but is still a member of a Group B, as Group A is a member of it) are also included in the output (Line 10). We also create a separate array of the user in each group, and call it <strong>$Members</strong>, so we can check each member of it individually now. We ensure to get all the required information we need by adding <em>Get-ADUser</em> in the end, as we apply it to the output of the <em>Get-ADGroupMember</em>.</p>
<p>Then, we create another loop to check for each member of each group. We assign the attributes of the users we are interested in to the values of our hashtable, and in the end, we create a temporary object <strong>$objRecord</strong>, so we can add its content based on the input above to the array <strong>$Table</strong>.</p>
<p>In the end, when all loops are stopped and proceeded, we export the whole "table" to the .CSV file, where <strong>$Path</strong> is specified on lines 1-3, and <em>-NoTypeInformation</em> will remove some irrelevant "rubbish" data.</p>
<p>You will get something like this (I've hidden some rows for the demonstration purpose:</p>
<p><img alt="" class="media" src="https://dykhl.in/public/.admingroups0_m.png" style="margin: 0 auto; display: table;" /></p>Checking the *Real* Password Expiration Dateurn:md5:13fa0e53ce22545f1c3cd5a3578957d02021-06-20T10:54:00+02:002023-04-19T08:43:35+02:00Andrii DykhlinActiveDirectorypowershell<p>If you ever happened to have used <abbr title="Active Directory Admin Center">ADAC</abbr> for the users to set up their password policy to be different from the default for your domain, then you might have noticed the <strong>net user %username% /domain</strong> command will return not the expected value, as it checks the information quite differently. The script below will actually help to determine the real expiry date.</p> <p>As reading from AD doesn't require the escalated privileges, it could be done with any account you like. For now, it will be using the command line, as it is more generic, faster, and just works. The GUI alternative is possible too, but it will be covered later.</p>
<p>For now, we can just have 2 lines of text (if you want to use credentials, then more of course, but as mentioned below, this is not significant for reading).</p>
<pre>
<code class="language-powershell">$user = Read-Host -Prompt "Username or part of it"
Get-ADUser -filter "samaccountname -like '*$user*' -and enabled -eq 'True' -and PasswordNeverExpires -eq 'False'" -Properties msDS-UserPasswordExpiryTimeComputed | Select-Object Name,samaccountname,@{n="Password Expiry Date";e={[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}} | Sort-Object "Password Expiry Date"</code></pre>
<p>What we have, is asking for input. You don't need to enter the colon or the space after - with the <strong>Read-Host -Prompt</strong>, PowerShell will take care of it for you. You can select to show you name, samaccountname and so on - whatever you prefer to see in the output. Sometimes, displayname is also fine. If this is a non-standard property - add it to the <strong>-Properties</strong> part of the command. The one we have there is what we exactly need - the expiry date. But originally, it shows the information in so-called <em>ticks, </em>and that won't make much sense, so in the structure <strong><code class="language-powershell">@{n="Expiry Date";e={[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}} </code></strong><code class="language-powershell">we are converting it to the human-readable view, which should be based on your system date settings.</code></p>
<p><code class="language-powershell">With filtering, you should better use quotes, so you can search with wildcards, and have the matches included before the input, and after it. For example, if the input is "jd", then we will get the result for "jdoe", "abcjd", "abcjdoe" etc. Adding the <strong>-and PasswordNeverExpires -eq 'False'</strong> to the sctipt is optional - it will just skip the service accounts (which don't ususally have the expiration day), and they won't appear in the list with an empty field. But as far as we sort by the expiry date, they will be on the top, so you will see them separately from the rest. It's up to you, those scripts are rather adjustable.</code></p>
<p><code class="language-powershell">Alternatively, you can add the function to your <a class="ref-post" href="https://dykhl.in/post/9">powershell profile</a>. You can add something like this, just to ensure the user is entered. We also allow the pipeline input, so you can enter a few strings separated by coma, and then pipe them to the function without any extra arguments.</code></p>
<pre>
<code class="language-powershell">function Get-ADPasswordExpiryDate {
param(
[Parameter(Mandatory,ValueFromPipeline)]
[string]$Username
)
process {
Get-ADUser -filter "samaccountname -like '*$Username*' -and enabled -eq 'True' -and PasswordNeverExpires -eq 'False'" -Properties msDS-UserPasswordExpiryTimeComputed | Select-Object Name,sam*,@{n="Password Expiry Date";e={[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}} | Sort-Object "Password Expiry Date"
}
}</code></pre>
<p>An additional trick is to "demote" the mandatory parameter to a regular one, and specify it equals the current user. So, if the user provides input - perfect, they get a result. Alternatively, the function will return a value of the user who ran the command. What to choose from is your own liberty, you can achieve it changing the lines 2-5 from above to the following:</p>
<pre>
<code class="language-powershell"> param(
[Parameter(ValueFromPipeline)]
[string]$Username="$env:username"
)</code></pre>
<p><code class="language-powershell">Open a new instance or PowerShell - and you can use it either with <strong>Get-PasswordExpiryDate user</strong>, or just <strong>Get-PasswordExpiryDate</strong> - the command line will ask you to input the user afterwards. You can notice the expiry date of 1601 - it happens when the password has been never set:</code></p>
<p style="text-align: center;"><code class="language-powershell"><img alt="" class="media" src="https://dykhl.in/public/.passwdexp_m.png" /></code></p>Remove the User from a Group if They are the Members of Another Oneurn:md5:7417c9507ff27f17bc000ad5570889db2021-06-13T12:04:00+02:002022-01-01T20:20:57+01:00Andrii DykhlinActiveDirectorypowershell<p>If you need to remove the user from a particular group based on the membership of another, you can do that <em>super easy, barely an inconvenience.</em></p> <p>For that, we need a short PowerShell script, like the one below. I won't use any particular names, you can put your own of course.</p>
<pre>
<code class="language-powershell">$groupA = Get-ADGroupMember -Identity "Group 1" -Recursive | Select-Object -ExpandProperty samaccountname
$groupB = Get-ADGroupMember -Identity "Group 2" -Recursive | Select-Object -ExpandProperty samaccountname
foreach ($user in $groupA) {
If ($groupB -contains $user) {
Write-Host "$user is a member of both groups, removing from the second one"
Remove-ADGroupMember -Identity "Group 2" -Member $user -Confirm:$false
}
}</code></pre>
<p>We set up that $groupA and $groupB contain the list of samaccountnames of their members, after that we check each user in $groupA list for being a member of the $groupB as well. We can check both groups for the same user, but if we're checking all the members of groupA, that pretty much means they are already there, so the extra check is not that important.</p>
<p>On the next line we just notify ourselves the user is going to be removed from the group, and the last line removes the user indeed. And <strong>-Confirm:$false </strong>is used to make a more flawless automation: if you need to remove 100 users, you will need to confirm every user from the group removal, which might be time-consuming. If you need to ensure nothing is going wrong, you can remove that part and confirm every user manually.</p>Check Users with No Groups by Patternurn:md5:8d7141abd91708b42736e0f7a7e899df2021-06-13T11:29:00+02:002022-01-01T20:20:28+01:00Andrii DykhlinActiveDirectorypowershell<p>Imagine the situation when someone (probably, from Audit) comes to you and asks to provide the list of users (if any) without a particular group (or groups by imaginary pattern). It could happen, so you need to be more or less prepared.</p> <p>In this case you can use the script like below. It uses the group membership to distinguish users from service accounts, or accounts used for meeting rooms, or on some sort of dashboards, monitoring machines etc., you can also specify by different attributes in AD, for example, you can use employeeID, especially if it is used to sync to Azure AD. It might also help, if you want to find users that are synced to AAD, but don't have the password policy applied (we are using the password policy group in the example).</p>
<pre>
<code class="language-powershell"><#
This is a script to check the users that are not a part of a group with a matching criteria (specified on Line 12).
Will return a list of the users, the output could be expanded if needed
#>
$results = @()
$users = Get-ADUser -Properties memberof -Filter {enabled -eq $True} | ? {$_.memberof -like "*PasswordPolicyUsers*"}
foreach ($user in $users) {
$groups = $user.memberof -join ';'
$results += New-Object psObject -Property @{'User'=$user.name;'Groups'= $groups}
}
$results | Where-Object { $_.groups -notmatch 'SRP' } | Select-Object User | Sort-Object User</code></pre>
<p>We create a few variables: $results is used to create an empty array, $users will create a set of users with the criteria as of being enabled, and a member of the group PasswordPolicyUsers. The <strong>?</strong> is the alias for <strong>Where-Object</strong>.</p>
<p>After that we are going through the loop, and check each entry from the users output one by one, creating for each user the variable $groups, that will join all the groups togeter with a semicolon to one string. With the next line you create a table of users and their groups. All the groups are in the distinguished name format, so like <strong>CN=Group,OU=Security Groups,DC=domain,DC=local</strong></p>
<p>Last line will just show the list of the users without the matching. In this case is "SRP", but it could be anything. You will have a list of names, so you can go and check why there is no such group for them,</p>UPN Suffix Change for the Required Usersurn:md5:3be3320fbc77d537e3cf571eac20c3162020-09-28T13:09:00+02:002021-12-21T08:29:16+01:00Andrii DykhlinActiveDirectorypowershell<p style="text-align: justify;">Sometimes you will need to change the <abbr title="UPN stands for the User Principal Name">UPNs</abbr> for the users. Either to have the sync with Azure AD or for something else – it's good to be compliant with the 21st century.</p> <p style="text-align: justify;">It happens to be a good practice to have the UPN change in the case of the moving forward the Azure environment. I will not describe the synchronization between the on-prem and Azure AD yet, it's just about the requirements to have all the required people to have switched from the UPN suffix they have (for example, domain.local) to the new one, specifically configured for your company's tenant (for example, domain.com).</p>
<p style="text-align: justify;">In the case of Hybrid environment it might be useful to have the synchronization based on OU - in case of the security group you might have troubles of having everything synced correctly and managed easily: the Azure AD Connect synced group doesn't support group inheritance, and nested groups will not make it into the succeeding - you should have the OUs specifically for users and devices you want to have in Azure, all the rest are skipped (service accounts, devices with no need to have the presence in Azure etc.).</p>
<p style="text-align: justify;">In our case we still had a test group for this movement in the moment of script execution, it was used to determine some other accesses and permissions (password policies, for example). So the command looked like this:</p>
<pre>
<code>Get-ADGroupMember "Group" | %{ $UserObj = Get-ADUser $_; $OldUPN = $UserObj.UserPrincipalName; $NewUPN = $OldUPN.Replace("domain.local","domain.com"); Set-ADUser $UserObj -UserPrincipalName $NewUPN }
</code></pre>
<p style="text-align: justify;">In this case we have a AD group with a mysterious name "Group", you can choose whatever you want of course.</p>
<p style="text-align: justify;">After that we check each user from the group, take the UPN, replace the part "domain.local" in it with a "domain.com" string from the suffix, and then we set up the same user a value of a new changed UPN.</p>
<p style="text-align: justify;">If you want to filter the users by the OU instead, you can go with the command below:</p>
<pre>
<code>Get-ADUser -Filter * -SearchBase "OU=O365,OU=Users,DC=domain,DC=local" | %{ $UserObj = Get-ADUser $_; $OldUPN = $UserObj.UserPrincipalName; $NewUPN = $OldUPN.Replace("domain.local","domain.com"); Set-ADUser $UserObj -UserPrincipalName $NewUPN }
</code></pre>
<p style="text-align: justify;">The <abbr title="Organizational Unit">OU</abbr> is up to you to determine, you might have the special one for specifically Azure, or another one for the accounts not being synced (believe or not - some service accounts need to have the *.local UPN suffix).</p>
<p style="text-align: justify;">To execute the command you should run it with a privileged account (for example, your domain admin).</p>