Exchange Online – Mailbox Size Audit

Exchange Online – Mailbox Size Audit

Here is a little powerhouse script I wrote to audit the mailbox sizes. The focus recently is to see who’s mailbox sizes are about to be over and if it’s the deleted folder. I can’t show you all of the scripts, but I can show you part of it. This part will pull the data down in such a way that you can view the mailbox sizes, who has the largest mailboxes, what folder is the largest in those mailboxes, and what their deleted folder is sitting at. Let us take a look at the script itself.

The Script

function Get-SHDEXOMailboxSizeAudit {
    [cmdletbinding()]
    param (
        [pscredential]$Credential
    )
    Begin {
        
        #Installs required modules
        Write-Verbose "Installing required modules"
        if (!(Get-InstallEdModule ExchangeOnlineManagement)) { Install-Module ExchangeOnlineManagement }

        Write-Verbose "Checking and importing required modules"
        # Starts importanting required modules
        if (!(Get-Command Connect-ExchangeOnline)) { Import-Module ExchangeOnlineManagement }

    }
    process {
        
        #Connecting to exchange. Grabing credentials. 
        if (!($PSBoundParameters.ContainsKey('Credential'))) {
            $Credential = (Get-Credential) 
        }
        Connect-ExchangeOnline -credential $Credential
 
        #Grabs all the mailboxes at once. As this report will look at all the mailboxes. 
        $Mailboxes = Get-Mailbox

        #Starts looping through each mailbox
        For ($M = 0; $M -le $Mailboxes.count; $M++) {

            Write-Verbose "Gathering Info on: $($Mailboxes[$M].UserPrincipalName)"

            #Grabs the mailbox stats.
            $Stats = Get-EXOMailboxStatistics $mailboxes[$M].UserPrincipalName
            $FolderStats = Get-EXOMailboxFolderStatistics $mailboxes[$M].UserPrincipalName

            #Starts looping through those folders to get their sizes. 
            $MainFolderStats = foreach ($Folder in $FolderStats) {
                $FolderSize = [math]::Round((($Folder.FolderSize.split('(')[1].split(' ')[0] -replace ',', '') / 1gb), 2)
                [pscustomobject]@{
                    Name = $Folder.name
                    Size = $FolderSize
                }
            }

            #Adds this information to the mailbox object
            $Mailboxes[$M] | add-member -Name "Statistics" -MemberType NoteProperty -Value $Stats
            $Mailboxes[$M] | add-member -Name "FolderStats" -MemberType NoteProperty -Value $MainFolderStats
        }

        #Creates a return value. 
        $Return = foreach ($Mail in $Mailboxes) {

            #Starts looking at mailboxes that are not the discovery mailbox.
            if (!($mail.UserPrincipalName -like "DiscoverySearchMailbox*")) {

                #Grabs the deleted folder as that is a folder we want to see in this report. 
                $Deleted = $Mail.FolderStats | where-object { $_.name -like "Deleted Items" }

                #Grabs the largest folder. If  it's not the deleted folder, then we might want to increase their mailbox sizes.
                $LargestFolder = $Mail.FolderStats | Sort-Object Size -Descending | select-object -first 1 

                #Doing some math on a string. The string format (# bytes). Thus, we work the string to get the bytes. Divide and round. 
                $Size = [math]::Round(($Mail.Statistics.TotalItemSize.value.tostring().split('(')[1].split(' ')[0].replace(',', '') / 1gb), 2)

                #Grabs the mailboxes percentage.
                $DeletedToMailboxPercent = [math]::Round((($Deleted.Size / $size) * 100), 0)

                #Outputs the data to the return value. 
                [pscustomobject]@{
                    DisplayName             = $Mail.Displayname
                    UserPrincipalName       = $Mail.UserPrincipalName
                    MailboxSize             = $Size
                    LargetsFolder           = $LargestFolder.Name
                    LargetsFolderSize       = $LargestFolder.Size
                    DeletedItemSize         = $Deleted.Size
                    DeletedToMailboxPercent = $DeletedToMailboxPercent
                }
            }
        }
        
        #Disconnects exchange
        Disconnect-ExchangeOnline -confirm:$false > $null
    }
    End { 
        $Return | sort-object MailboxSize -Descending 
    }
}

The breakdown

First this script is designed to work on powershell 7. It will not work on powershell 5.

The first part we come to is the [pscredential] object. Notice it’s not mandatory. Notice no pipelining either. I have found PS credentials pipped intend to do very poorly. So, it’s simple, bam wam done.

Begin

Inside our begin tab, we have the module setup. We check installed modules for exchangeonlinemanagement. if it’s there, we ignore it and import the module, if it’s not we install it. Same way with importing. If it’s imported, then we do nothing, if it’s not, we import it.

Process

Next, we grab credentials if need be and connect to exchange. We use the get-credential command to grab the credentials. Then we use the connect-exchangeonline command to connect to the exchange online.

Once we are connected, the magic can start. This is the sweetness of this script. The first step is to grab all of the mailboxes at once with Get-Mailbox. Then we start a loop, not any loop, a for a loop. Wait! David, WHY A FOR LOOP! It’s simple, we want to add information to the index. So, we need to be able to call the index. We use the Get-EXOMailboxStatistics and choose the userprincipalname of the index we are looking for. We do the same thing with Get-EXOMailboxFolderStatistics. These gives us some clear stats we can use later on in the script. Now we loop through the folder stats that we just collected and math ourselves some bytes to gb. See the output of the get-exomailboxfolderstatistics looks like “24mb (#### bytes). So we need to filter that out and get the ####. I use a split to do this. I split out the string at the ‘(‘. This way, the bytes are on object 1. Then we split it again by the space. Now the bytes are on the 0 object. Then we replace the ‘,’ with nothing. Now we divide all that by 1gb to convert it to gbs. Then we drop that all into the mainfolderstats. Next, we add all of that information into the mailbox variable we created before this looping madness using add-member. We are adding it as a noteproperty.

Now we have prepped our data, it’s time to sort it. We start the loop once more, but this time it’s simple for each loop as we don’t need the array index value. We first grab all the deleted items folder. Then we grab the largest folder using the sort-object command on the size object of the folderstats note property that we made in the last step. Then we do some math. Like before to get the mailbox overall size. Finally, we grab the deleted mailbox percentage with a little more math. This time its percentage math. Now we have all of this useful information we use our pscustomobject and put it all together.

Then we disconnect using the disconnect-exchangeonline command.

End

Finally we display the return information inside our end tab. We sort the object by the mailbox size in a descending order.

SHD – Quickbook Search

SHD – Quickbook Search

This past week I needed to find all of the quickbook files on a computer without accessing quickbooks itself. The core of the script is a simple Get-Childitem command looped for each logical disk on the machine. Looking for one of the four main extensions for quickbooks. The four extensions are as follows

NameExtension
Company Filesqbw
Backup Filesqbb
Portable Filesqbm
Bank Statement filesqbo

The Script

function Search-QuickbookFiles {
    [cmdletbinding()]
    Param (
        [parameter(Mandatory = $True)][Validateset("Company Files", "Backup Files", "Portable Files", "Bank Statement Files")][string]$FileType
    )
    if ($FileType -like "Company Files") { $EXT = "QBW" }
    if ($FileType -like "Backup Files") { $EXT = "QBB" }
    if ($FileType -like "Portable Files") { $EXT = "QBM" }
    if ($FileType -like "Bank Statement Files") { $EXT = "QBO" }

    $Disks = Get-CimInstance -ClassName win32_logicaldisk 
    $QB = foreach ($Disk in $Disks) {
        Get-ChildItem -Path "$($Disk.DeviceID)\" -Filter "*.$EXT" -Recurse | Select-Object FullName,Length,LastWriteTime
    }
    $QB
}
$DateTime = (Get-Date).tostring("yyyy-MM-dd_hh-mm-ss")
if (!(Test-Path C:\temp)) {mkdir c:\temp}
Search-QuickbookFiles -FileType 'Company Files' | Export-Csv "c:\temp\$($Env:COMPUTERNAME)_CompanyFiles_$($DateTime).CSV"
Search-QuickbookFiles -FileType 'Backup Files' | Export-Csv "c:\temp\$($Env:COMPUTERNAME)_BackupFiles_$($DateTime).CSV"
Search-QuickbookFiles -FileType 'Portable Files' | Export-Csv "c:\temp\$($Env:COMPUTERNAME)_PortableFiles_$($DateTime).CSV"
Search-QuickbookFiles -FileType 'Bank Statement Files' | Export-Csv "c:\temp\$($Env:COMPUTERNAME)_BankStatementFiles_$($DateTime).CSV"

The Breakdown

Since I want this to be a little easier to use, I broke it down between the file types with a validate set parameter. This way you can choose which extension you want. Then I go through each extension and make an if statement for each one matching up the extension.

Next we get the disks using the Get-CimInstance -classname win32_logicaldisk. This grabs all the mapped drives, local drives, and anything else that has a drive letter.

Now we loop through those disks and search the root of each drive for any files with the extension we choose. We select the fullname as this gives us the full path. I also like having file size and last write time to determine if the file is valid still. Once we go through this loop we display the information.

Improvements

I can add remote computers to this setup.

That’s it, if you have any questions feel free too reach out.

Group Policy – Block Batch Scripts correctly

Group Policy – Block Batch Scripts correctly

Recently a IT specialist left a client’s company, and they left some time bombs. One of them was blocking command prompt for the end users. Normally this is not a problem, but they set the policy to a bad scope. Even the admins couldn’t use the command prompt. It was not pretty. So, lets take a look at the policy in our own lab.

Block Command Prompt

The blocking command prompt is very simple but can be done wrong. To start create the policy, it’s located at User > Policies > Administrative Templates > System > Prevent access to command prompt. Set the policy to enable and select yes to block script processing.

Scoping

Since this is a user GPO the scope is very important. Scope is based on the location where you link the GPO. So, if you link the gpo at the top of the domain or site level, everything will get it unless, of course, a counter policy is linked at a lower level that is enforced the same way. Here is the scope applied list.

  • Local GPOs
  • Site-Linked GPOs
  • Domain-Linked GPOs
  • OU-linked GPOs
  • Child OU-linked GPOs

In this case, the user linked the scope at the top of the domain instead of the OUs that need to be applied to. The simple fix was to move the policy link to the sub OUs.

GPO – Legal Notice/Forget Last User

GPO – Legal Notice/Forget Last User

Recently I came across a client that had an amazing legal notice before you logged into a computer. I also noticed that it didn’t remember who last logged in. This gave a unique level of security and provided a good AUL at the same time. I wanted to guide you through the process. This is done with Group Policy, thus a domain structure is the best bet. The first part is to forget the last logged-in user.

Forgetting the Last logon user

The first step is to start Group Policy Management. Then Create a new group policy object. There are multiple ways to do this, pick your favorite. I named my policy, ForgetLastLogonUser.

Inside the windows Group policy, lets navigate to Computer Configuration > Policies > Windows Settings > Security Settings > Local Policies > Security Options. Then we will enable Interactive logon: don’t display last signed-in.

This setup will remove the last logged on user which will help users remember their username and increase security through obscurity.

Legal Notice

The next thing they did was setup a legal notice. This is also known as a banner. Lets create another policy like above. I’m going to name my policy, LogonBanner. Click edit on your logonbanner and navigate to Computer Configuration > Policies > Windows Settings > Security Settings > Local Policies > Security Options and double click Interactive Logon: Message text for user attempt to log on. This is where you will define your login message.

Next do the same thing for Interactive Logon: Message title for users attempting to log on.

With both of those in place, this is what it finally looks like.

That’s pretty rad. I hope you all like this little tutorial.

SHD Resource – User to Groups

SHD Resource – User to Groups

This little guy is a simple dynamic parameter resource for you all. Take a look at my previous blog post about how these parameters work. Totally worth adding these things to your scripts. This script is simple, it uses the add-adgroupmemeber and dynamic parameters to help pad against mistakes.

The Script

function Set-SHDADGroupMemebers {
    [cmdletbinding()]
    param (
        [parameter(mandatory=$true)][validateset('Add','Remove')][string]$Action,
        [securestring]$Credential,
        [switch]$Output
    )
    DynamicParam {
        
        
        # Set the dynamic parameters' name
        $ParamName_portgroup = 'Group'
        # Create the collection of attributes
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute) 
        # Create the dictionary 
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        # Generate and set the ValidateSet 
        $arrSet = (Get-ADGroup -Filter *).name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)    
        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)
        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_portgroup, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParamName_portgroup, $RuntimeParameter)

        
        # Set the dynamic parameters' name
        $ParamName_datastore = 'Username'
        # Create the collection of attributes
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $true
        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)  
        # Generate and set the ValidateSet 
        $arrSet = (Get-ADUser -Filter *).name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)
        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName_datastore, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParamName_datastore, $RuntimeParameter)
        return $RuntimeParameterDictionary
    }

    begin{
        $Group = $PsBoundParameters[$ParamName_portgroup]
        $username = $PsBoundParameters[$ParamName_datastore] 
    }
    process {
        if ($PSBoundParameters.ContainsKey('Credential')) {
            if ($Action -like "Add") {
                Add-ADGroupMember -Identity $group -Members $username -Credential $Credential
            } elseif ($Action -like "Remove") {
                Remove-ADGroupMember -Identity $group -Members $username -Credential $Credential
            } else {
                Get-ADGroupMember -Identity $group -Credential $Credential
            }
        } else {
            if ($Action -like "Add") {
                Add-ADGroupMember -Identity $group -Members $username
            } elseif ($Action -like "Remove") {
                Remove-ADGroupMember -Identity $group -Members $username
            } else {
                Get-ADGroupMember -Identity $group
            }
        }
    }
    end {
        if ($Output) {
            if ($PSBoundParameters.ContainsKey('Credential')) {
                Get-ADGroupMember -Identity $Group -Credential $Credential
            } else {
                Get-ADGroupMember -Identity $group
            }
        }
    }
}

Example 1

Set-SHDADGroupMemebers -Action Add -Group Administrators -Username Adam.Long -Output

Adds Adam.Long to the administrators group. Then it outputs all the users inside that group.

Example 2

Set-SHDADGroupMemebers -Action Remove -Group Administrators -Username Adam.Long

Removes Adam.Long from the Administrators group without outputting any additional information.