Invoke File Service Integrity Check

Invoke File Service Integrity Check

This little script will allow you to check a folder location, if that folder location has x number of files, it will restart the service of your choice on a target computer. Then it will log that information and finally it will email someone this information.

function Invoke-FileServiceIntegrityCheck {
    <#
    .SYNOPSIS
        Checks a file path for number of files. If greater, restarts a service and emails a message and logs a log.
    .DESCRIPTION
        Checks a file path for a set number of files that are predefined by the end user. If the file count is greater, it will restart a service. It will log the results and email a person stated by the end user.
    .PARAMETER SearchPath
        Target Path to scan for file count.
    .PARAMETER FileCount
        File count that will be used to determine the request size.
    .PARAMETER ComputerName
        Target Computer Name that the service lives on.
    .PARAMETER ServiceName
        Name of the service to restart.
    .PARAMETER SendReportTo
        The email address to which the email report will be sent to.
    .PARAMETER EmailDomain
        The domain name. For example, example.local
    .PARAMETER MailServer
        The mail server to send the email from.
    .PARAMETER Recurse
        Recurses the file search path.
    .PARAMETER Credential
        The Credentials, if needed for the email server.
    .EXAMPLE
        Invoke-FileServiceIntegrityCheck -SearchPath \\server1\share -FileCount 25 -ComputerName server2 -ServiceName intergrator -SendReportTo bob@example.com -EmailDomain example.com -Mailserver mail.example.com -Credential (Get-Credential)

        Checks the folder \\server1\share to see if there is 25 or more files. If it does, it will restart the service named intergrator on server 2.
        Then it will send an bob@example.com from watcheye@example.com using mail.example.com and the credentials provided.
    .EXAMPLE
        Invoke-FileServiceIntegrityCheck -SearchPath \\server1\share -FileCount 25 -ComputerName server2 -ServiceName intergrator -SendReportTo bob@example.com -EmailDomain example.com -Mailserver mail.example.com

        Checks the folder \\server1\share to see if there is 25 or more files. If it does, it will restart the service named intergrator on server 2.
        Then it will send an bob@example.com from watcheye@example.com using mail.example.com. Email will be sent from the server. If no relay is setup, the server will reject the email.
    .INPUTS
        [none]
    .OUTPUTS
        email and log
    .NOTES
        Author: David Bolding
        Date: 10/14/2020

    .LINK
        https://github.com/rndadhdman/PS_Super_Helpdesk
    #>
    [cmdletbinding()]
    param (
        [parameter(HelpMessage = "Folder Search Path", Mandatory = $True)][string]$SearchPath,
        [parameter(HelpMessage = "File Count", Mandatory = $True)][int]$FileCount,
        [parameter(HelpMessage = "Computer That the server Lives", Mandatory = $True)][String]$ComputerName,
        [parameter(HelpMessage = "Service Name", Mandatory = $True)][String]$ServiceName,
        [parameter(HelpMessage = "Service Name", Mandatory = $True)][String]$SendReportTo,
        [parameter(HelpMessage = "Service Name", Mandatory = $True)][String]$EmailDomain,
        [parameter(HelpMessage = "Service Name", Mandatory = $True)][String]$Mailserver,
        [parameter(HelpMessage = "Recurse the Search")][switch]$Recurse
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )

    #Checks Files
    if ($Recurse) { $Files = Get-ChildItem "$SearchPath" -Recurse } else { $Files = Get-ChildItem "$SearchPath" }


    if ($Files.Count -ge $FileCount) {

        $StopTime = Get-date
        Get-Service -ComputerName mtxs-lifetecapp -Name "Interlink" | Stop-Service

        sleep 30

        Get-Service -ComputerName mtxs-lifetecapp -Name "Interlink" | Start-Service
        $StartTime = Get-date

        sleep 30

        if ($Recurse) { $Files2 = Get-ChildItem "$SearchPath" -Recurse } else { $Files2 = Get-ChildItem "$SearchPath" }

        if (!(Test-Path c:\logs)) {mkdir c:\logs}

        #Gathers Log Info
        $Log = [pscustomobject]@{
            ComputerName = $ComputerName
            ServiceName = $ServiceName
            SearchPath = $SearchPath
            StopFileCount = $Files.count
            ServiceStopTime = $StopTime.ToString()
            ServiceStartTime = $StartTime.tostring()
            StartFileCount = $Files2.count
            CallingComputer = $env:COMPUTERNAME
        }

        $Log | Export-csv c:\temp\Invoke_FileServiceIntegrityCheck_log.csv -Append

        #Builds EMail
        $ErrorHtmlbody = @"
<html>
<head>
<style>
body {
    Color: #252525;
    font-family: Verdana,Arial;
    font-size:11pt;
}
h1 {
    text-align: center;
    color:#C8102E;
    Font-size: 34pt;
    font-family: Verdana, Arial;
}
</style>
</head>
<body>
<h1>$ServiceName Restarted</h1>
<hr>
<br>
Dear Watcher,<br>
<br>
During my last check there were <b>$($Files.Count)</b> files inside <i>$SearchPath</i>. The Integrator was stopped on <b>$($StopTime.tostring())</b> and started on <b>$($StartTime.tostring())</b>. There are now <b>$($files2.count)</b> in <i>$SearchPath</i>. Please investigate on why the $ServiceName failed on $ComputerName.<br>
<br>
<br>
Thank you,<br>
<i>$ServiceName WatchEye</i><br>
<br>
<hr>
</body>
</html>
"@
        if ($PSBoundParameters.ContainsKey('Credential')) {
            Send-MailMessage -To $SendReportTo -From "Service Watch Eye <WatchEye@$EmailDomain>" -Subject "$ServiceName Failure" -BodyAsHtml $ErrorHtmlbody -SmtpServer $Mailserver -Credential $Credential
        } else {
            Send-MailMessage -To $SendReportTo -From "Service Watch Eye <WatchEye@$EmailDomain>" -Subject "$ServiceName Failure" -BodyAsHtml $ErrorHtmlbody -SmtpServer $Mailserver
        }
    }

}

Example

Invoke-FileServiceIntegrityCheck -SearchPath \\server1\share -FileCount 25 -ComputerName server2 -ServiceName integrator -SendReportTo bob@example.com -EmailDomain example.com -Mailserver mail.example.com

In this example, we will search for the share on server1 if it has more than 25 files. If it does, then we will restart the integrator on server2. Finally, we will email bob@example.com with an email from watcher@example.com using the mail server mail.example.com.

Copy A User’s Group to Other Users

Copy A User’s Group to Other Users

Often times I am asked to give user a the same rights as user b. Since everything is setup with groups, this is easy to do. All I have to do is copy all the security groups to the new users. Normally I would use the command Get-ADPrincipalGroupMembership whoever, that has become less trust worthy over time for us. So I made a different approach.

$Groups = (Get-ADUser -Identity $SourceUser -Properties memberof).memberof -Replace '^cn=([^,]+).+$', '$1' | Sort-Object | ForEach-Object { (Get-ADGroup -Filter "name -like '$_'")}

The above command grabs the source users member of and filters it out for just the name. Then it loops through each and gets the group name. From here we can filter even farther to get the security and then loop those and added the single member making a one liner, but I want this thing to be bigger and better. A good function that can do distribution, security, universal, global and domain local groups to more than one single user with credentials or not. I want this thing to be nice. Here is the heart beat of the command:

foreach ($Target in $TargetUser) {
        $Groups | ForEach-Object {
            try {
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target -Credential $Credential
                } else {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target
                }
            } catch {
                Write-Verbose "Failed to add $($_.Samaccountname) to $Target"
                Write-Warning "$_ could not apply to $Target"
            }
        }
    }

We target each user with this information. Then we add the group accordingly. So, how do you split the Security group vs Distribution group? We do this by using PSboundparameters and a Where-object group. So we declare a parameter in the parameter area and then ask if it is being used. If it is, we use the where object to search for the groupcategory.

$Groups = (Get-ADUser -Identity $SourceUser -Properties memberof).memberof -Replace '^cn=([^,]+).+$', '$1' | Sort-Object | ForEach-Object { (Get-ADGroup -Filter "name -like '$_'")}
    if ($PSBoundParameters.ContainsKey('GroupCategory')) {$Groups = $Groups | Where-Object {$_.GroupCategory -like "$GroupCategory"}}
    if ($PSBoundParameters.ContainsKey('GroupScope')) {$Groups = $Groups | Where-Object {$_.GroupScope -like "$GroupScope"}}

Lets put it all together shall we, so you can get back on with your day.

Function Copy-SHDUserGroupToUser {
    [cmdletbinding()]
    param (
        [Parameter(
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True,
            HelpMessage = "Enter a users Name",
            Mandatory = $true)][String]$SourceUser,
        [Parameter(HelpMessage = "Target User", Mandatory = $True)][string[]]$TargetUser,
        [parameter(Helpmessage = "Group Category")][validateset("Security","Distribution")]$GroupCategory,
        [parameter(Helpmessage = "Group Scope")][validateset("Universal","Global","DomainLocal")]$GroupScope,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    $Groups = (Get-ADUser -Identity $SourceUser -Properties memberof).memberof -Replace '^cn=([^,]+).+$', '$1' | Sort-Object | ForEach-Object { (Get-ADGroup -Filter "name -like '$_'")}
    if ($PSBoundParameters.ContainsKey('GroupCategory')) {$Groups = $Groups | Where-Object {$_.GroupCategory -like "$GroupCategory"}}
    if ($PSBoundParameters.ContainsKey('GroupScope')) {$Groups = $Groups | Where-Object {$_.GroupScope -like "$GroupScope"}}
    foreach ($Target in $TargetUser) {
        $Groups | ForEach-Object {
            try {
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target -Credential $Credential
                } else {
                    Write-Verbose "Add $($_.Samaccountname) a $($_.GroupCategory.tostring()) a $($_.GroupScope.tostring()) to $Target"
                    Add-ADGroupMember -Identity $_.samaccountname -Members $Target
                }
            } catch {
                Write-Verbose "Failed to add $($_.Samaccountname) to $Target"
                Write-Warning "$_ could not apply to $Target"
            }
        }
    }
} 

Examples

Copy-SHDUserGroupToUser -SourceUser bobtheuser -TargetUser test1,test2,test3 -GroupCategory Security -GroupScope Global -Verbose

This will add all of bobtheuser’s global security groups to test1, test2, and test3 users and verbose the info out.

Copy-SHDUserGroupToUser -SourceUser bobtheuser -TargetUser test1,test2,test3 

This will add all the bobtheuser’s groups to test1, test2, and test3.

Get Lock Out Info From DC

Get Lock Out Info From DC

Have you ever had trouble getting lockout information for a single user? The DCs have the logs, but getting them can be tough. The core idea is to use the Get-Winevent command to gather the logs needed. Doing this from a remote computer is time-consuming. However, using the Invoke-Command tends to speed things up. Inside this example, I am going to use something called Splatting. Splatting allows you to drop everything a command needs into a command quickly and easily by creating a hash table. Now, not all commands like splatting, so be aware of that. Here is the heart of the code:

$parameters = @{
        ComputerName = $ComputerName
        ScriptBlock  = {
            Param ($param1)
            $FilterTable = @{
                'StartTime' = $param1
                'EndTime'   = (Get-date)
                'LogName'   = 'Security'
                'Id'        = 4740
            }
            $Events = Get-WinEvent -FilterHashtable $FilterTable
            foreach ($E in $Events) {
                [pscustomobject]@{
                    Time     = $E.TimeCreated
                    ID       = $E.ID 
                    DC       = $E.Properties[4].value
                    SID      = $E.Properties[2].value
                    Domain   = $E.Properties[5].Value
                    Username = $E.Properties[0].value
                    Computer = $E.Properties[1].value
                }
            }
        }
        ArgumentList = $time           
    }
Invoke-command @Parameters

The first thing we are doing is creating the parameters that will be splatted. Just like a hashtable, it’s something = something. So the computer name is the Array computername. The computer name inside the Invoke-command can handle arrays. Making this much easier as we want the DCs.

Next is the script block. This is where the invoke-command will be executing everything from. We start off with a Param () block. It will get it’s input from the Argumentlist later. The Argumentlist is the variables we will be dropping into the command. In this case, it will be time. As in this script you can build your time based on how far back you want to search your logs.

Next is the filter table. The Get-winevent command likes hashtables. So we start off with our starttime, basically how far back we are going to go. Then the end time which is the time of execution. The Logname is the type, there is an application, security, system, and more. Since the lockouts are security, we choose security. Finally, we want the ID. For account lockouts, the ID is 4740. There is an online Encyclopedia for windows logs. (Link)

The Get-WinEvent command is followed by the FilterHashtable flag. We dump this into the events variable for sort throu. Now we search each Event in Events. We express this with an E because $event is a system variable and we don’t want to cause any issues. We want the time it was created. The ID, what DC it came from. The SID of the user. The Domain. The username of the user, and finally we want the calling workstation. Once all that is put together we splat it into the command Invoke-Command.

That is the heart, but not the power. This script allows you to call back from the day, hour, and minute back. If you don’t put in a input, it makes it a day for you. It also can target a single user or different computers. Finally it gives you the ability to use different credentials. Lets take a look at the parameters.

param (
        [parameter(Helpmessage = "Username of target")][Alias('User', 'Samaccountname')][String[]]$Username,    
        [parameter(Helpmessage = "Days back")][int]$Day,
        [parameter(Helpmessage = "Days back")][int]$Hour,
        [parameter(Helpmessage = "Days back")][int]$Minute,
        [parameter(Helpmessage = "Computers to target, default is domain controllers.")][Alias('Computer', 'PC')][String[]]$ComputerName = (Get-ADDomain).ReplicaDirectoryServers,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )

We first are looking for a list of usernames. Notice we don’t have to declare this variable. So, if we just want all the lockouts, we just don’t delcare it. This is done by using the PSBoundParameters.ContainsKey option. So, if the script sees the username is being used, it will follow this command.

if ($PSBoundParameters.ContainsKey('Username')) {
        foreach ($user in $Username) {
            $Return | Where-Object { $_.Username -like "$user" } | Sort-Object Time | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
        }
    }

The next parameters are also optional. It is days, hour, and minutes back. So, you can state 1 day, 3 hours, and 2 minutes ago and it will find the times during starting at that moment forwarder. This is good if you plan to use this script as an auditor (what it was built for). How this is achived once again is the use of psboundparameters.containskey.

$time = Get-date
if ($PSBoundParameters.ContainsKey('Day')) { $time = ($time.AddDays( - ($day))) } 
    if ($PSBoundParameters.ContainsKey('Hour')) { $time = ($time.AddHours( - ($Hour))) } 
    if ($PSBoundParameters.ContainsKey('Minute')) { $time = ($time.AddMinutes( - ($Minute))) } 
    if (!($PSBoundParameters.ContainsKey('Day')) -and !($PSBoundParameters.ContainsKey('Hour')) -and !($PSBoundParameters.ContainsKey('Minute'))) {
        $time = $time.AddDays( - (1))
    }

We first declare the time with the current time/date. Then based on the input, we remove days, hours, or minutes from the current time. If there is no input, we tell time to remove 1 day. Depending on the size of your organization and how many logs you have, this can be useful. The $time will be used in the argumentlist.

Now we have the Computername parameter. If you have differentiating computers than your DCs that handles these logs, you can target them with this command. Otherwise, we grab the dc information with a single line of code.

(Get-ADDomain).ReplicaDirectoryServers

Finally we have the ability to add the credentials. Once you delcare the credentials, we add the credential flag to our splat. To do this we create a hashtable with a single item and add it to the parameter.

if ($PSBoundParameters.ContainsKey('Credential')) {
        $parameters += @{Credential = $Credential }
    }

Now, lets put all this together in a single script so you can copy it and move along with your day.

Function Get-SHDLockoutInfo {
    [cmdletbinding()]
    param (
        [parameter(Helpmessage = "Username of target")][Alias('User', 'Samaccountname')][String[]]$Username,    
        [parameter(Helpmessage = "Days back")][int]$Day,
        [parameter(Helpmessage = "Days back")][int]$Hour,
        [parameter(Helpmessage = "Days back")][int]$Minute,
        [parameter(Helpmessage = "Computers to target, default is domain controllers.")][Alias('Computer', 'PC')][String[]]$ComputerName = (Get-ADDomain).ReplicaDirectoryServers,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    $time = Get-date
    if ($PSBoundParameters.ContainsKey('Day')) { $time = ($time.AddDays( - ($day))) } 
    if ($PSBoundParameters.ContainsKey('Hour')) { $time = ($time.AddHours( - ($Hour))) } 
    if ($PSBoundParameters.ContainsKey('Minute')) { $time = ($time.AddMinutes( - ($Minute))) } 
    if (!($PSBoundParameters.ContainsKey('Day')) -and !($PSBoundParameters.ContainsKey('Hour')) -and !($PSBoundParameters.ContainsKey('Minute'))) {
        $time = $time.AddDays( - (1))
    }
    $parameters = @{
        ComputerName = $ComputerName
        ScriptBlock  = {
            Param ($param1)
            $FilterTable = @{
                'StartTime' = $param1
                'EndTime'   = (Get-date)
                'LogName'   = 'Security'
                'Id'        = 4740
            }
            $Events = Get-WinEvent -FilterHashtable $FilterTable
            foreach ($E in $Events) {
                [pscustomobject]@{
                    Time     = $E.TimeCreated
                    ID       = $E.ID 
                    DC       = $E.Properties[4].value
                    SID      = $E.Properties[2].value
                    Domain   = $E.Properties[5].Value
                    Username = $E.Properties[0].value
                    Computer = $E.Properties[1].value
                }
            }
        }
        ArgumentList = $time           
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $parameters += @{Credential = $Credential }
    }
    $Return = Invoke-Command @parameters 
    if ($PSBoundParameters.ContainsKey('Username')) {
        foreach ($user in $Username) {
            $Return | Where-Object { $_.Username -like "$user" } | Sort-Object Time | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
        }
    }
    else {
        $Return | Sort-Object UserName | Select-Object Username, Domain, SID, Time, Computer, PSComputerName
    }
} 

I hope you enjoy this little script and I hope it helps you grow your company. If you find any bugs, let me know. Thank you so much and have a great day!

SHD – Disable Inactive Users

SHD – Disable Inactive Users

Now we have a way to find the disable OU, and to disable a user, now it’s time to disable old accounts. We do this by targeting the Last Logon dates.

Word of warning before we continue. When you do this, target a single OU instead of all of the company. Target only enabled users as well. Targeting all of AD will cause you to disable service accounts and system accounts. This can cause your system to crash. Also, target enabled users as many companies us templates that are disabled.

Now we got the warning out of the way, lets get started. The code is very simple. The first step is to get the days back you want to go and find that date/time. We are going to us Get-Date and your input which we are calling $DaysBack.

$Time = (get-date).AddDays(-($DaysBack))

We get the Current date. Then we want to add days to that date, but backwards. So the (Get-Date) gives us the current date. Then .AddDays() will add the days. To make $DaysBack Negative, we simple multiple it by -1. if you completed algebra 1 in high school, you already see it. -($DaysBack) is the multiplication $DaysBack * -1.

Now we have the date we want to search the OU in question and get the users that haven’t log on since $DaysBack. We will do this with the simple Get-ADuser and the Where-Object commands.

$users = Get-aduser -Filter { Enabled -eq $true } -SearchBase $SearchOU -Properties LastLogondate | Where-Object {($_.LastLogonDate -le $Time) -and ($null -ne $_.LastLogonDate)}

We use the get-aduser filter to filter out everything that is enabled. Then we use the search base to search the OU we want to pull the information from. This saves us heartache of “oh crap I disabled my bosses test user.” Next, we pass it through the Where-object. We filter out everyone who last logon date that is less than or equal to the time we set earlier. We also search if nothing is not equal to the last logon date. We do this part because if an account hasn’t logged into the system yet, it will appear. Doing this avoids removing new hires on accident. Another note on the $null -ne $_.lastlogondate, it’s faster to search like this instead of $_.lastlogondate -ne $null. I don’t know why, but it is much faster.

Now we have a list of users who we need to disable, we just start up a for loop and disable them using the Disable-SHDUser.

foreach ($User in $users) {
     Disable-SHDUser -Username $user.samaccountname -OU $DisabledOU 
}

After the loop is done, it’s done. They are all disabled. But David, I want to see who was disabled! I agree. So I added a simple ShowResults switch for this loop. This is for those who need to see the results.

foreach ($User in $users) {
     Disable-SHDUser -Username $user.samaccountname -OU $DisabledOU
     if ($ShowResults) {
          Get-aduser -Identity $user.samaccountname -Properties LastLogonDate,Memberof | Select-Object DistinguishedName,Samaccountname,Name,Lastlogondate,Enabled,@{l="GroupCount";e={$_.memberof.count}}
     }
}

Combining functions in a module is a great way to get things done. These last two blogs should have shown this to you. Now, lets look at the script itself.

The Script

function invoke-SHDDisableInactiveUsers {
    [cmdletbinding()]
    param (
        [parameter(HelpMessage = "Days after", Mandatory = $True)][int]$DaysBack,
        [parameter(HelpMessage = "Moves to this OU if provided, If not, finds disable ou and moves it.")][string]$DisabledOU,
        [parameter(HelpMessage = "Moves to this OU if provided, If not, finds disable ou and moves it.", Mandatory = $true)][string]$SearchOU,
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential,
        [parameter(HelpMessage = "Show results")][switch]$ShowResults
    )
    $Time = (get-date).AddDays(-($DaysBack))
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $users = Get-aduser -Filter { Enabled -eq $true } -Credential $Credential -SearchBase $SearchOU -Properties LastLogondate | Where-Object {($_.LastLogonDate -le $Time) -and ($null -ne $_.LastLogonDate)} | Sort-Object Samaccountname
        if ($PSBoundParameters.ContainsKey('DisabledOU')) {
            $DisabledOU = $DisabledOU
        }
        else {
            $DisabledOU = Find-SHDDisabledUsersOU -Credential $Credential
        }
        foreach ($User in $users) {
            Disable-SHDUser -Username $user.samaccountname -OU $DisabledOU -Credential $Credential 
            if ($ShowResults) {
                Get-aduser -Identity $user.samaccountname -Properties LastLogonDate,Memberof -Credential $Credential | Select-Object DistinguishedName,Samaccountname,Name,Lastlogondate,Enabled,@{l="GroupCount";e={$_.memberof.count}}
            }
        }
    }
    else {
        $users = Get-aduser -Filter { Enabled -eq $true } -SearchBase $SearchOU -Properties LastLogondate | Where-Object {($_.LastLogonDate -le $Time) -and ($null -ne $_.LastLogonDate)} | sort-object Samaccountname
        if ($PSBoundParameters.ContainsKey('OU')) {
            $DisabledOU = $DisabledOU
        }
        else {
            $DisabledOU = Find-SHDDisabledUsersOU
        }
        foreach ($User in $users) {
            Disable-SHDUser -Username $user.samaccountname -OU $DisabledOU
            if ($ShowResults) {
                Get-aduser -Identity $user.samaccountname -Properties LastLogonDate,Memberof | Select-Object DistinguishedName,Samaccountname,Name,Lastlogondate,Enabled,@{l="GroupCount";e={$_.memberof.count}}
            }
        }
    }
}

Next time I will explain the PSBoundParameters that you see in almost all scripts I run. It gives me the ability to give you the ability to have credentials but not require it.

If you have any questions, feel free to email me. These functions can be found on my git hub.

SHD – Find Disabled OU

SHD – Find Disabled OU

Have you ever started in a company and there was no documentation? The disabled OU isn’t named “Disabled Users” and things are just what the heck? This powershell script will help find that disabled user OU. Believe it or not, it’s a one liner.

((Get-ADUser -filter { enabled -eq $false }).Distinguishedname -replace '^CN=.*?,', '' | Group-Object | Sort-Object -Property Count -Descending | Select-Object -First 1).name

Lets tare this bad boy apart. First we have a Get-Aduser -filter { Enabled -eq $False}. So we want all the users in the company who are disabled. From there we are selecting only the DistinguishedName. We want to remove the first part of the DistinguishedName with a replace command. The Regex is ^CN=.*?,’,” Lets break this down.

.Distinguishedname -replace '^CN=.*?,', ''

^CN= tells us we are looking for the first “CN=” inside this string. Then we ask for the wild cards up to the first , with .*?. We tell it to replace it with nothing, aka double single quotes, .

| Group-Object

Now this gives us all the OUs that everyone who is disabled lives in. Next we group them together with Group-Object. Group-object is going to give us a clean count of each OU and how many unique items there are for each OU.

| Sort-Object -Property Count -Descending 

Next we want to organize everything with a Sort-Object. We select the count property and put it in descending order. This way we can select the first one in the final piece of the puzzle.

| Select-Object -First 1).name

Now we use the Select-object -First 1 command to get the first object from the descending list. This will give you the highest disabled users counted OU.

The Script

function Find-SHDDisabledUsersOU {
    [cmdletbinding()]
    param (
        [Parameter(HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    if ($PSBoundParameters.ContainsKey('Credential')) {
        ((Get-ADUser -filter { enabled -eq $false } -Credential $Credential).Distinguishedname -replace '^CN=.*?,', '' | Group-Object | Sort-Object -Property Count -Descending | Select-Object -First 1).name
    }
    else {
        ((Get-ADUser -filter { enabled -eq $false }).Distinguishedname -replace '^CN=.*?,', '' | Group-Object | Sort-Object -Property Count -Descending | Select-Object -First 1).name
    }
}