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.

Multiple Dynamic Parameters

Multiple Dynamic Parameters

Dynamic parameters… OMG… Lets just say, when I first started using them, my brain melted. These things take so much effort to get going. Then the way to document them is even harder. After using PowerShell for many years, I finally figured them out and it’s time to help you out. There are many blogs out there and many posts on redit about Dynamic Paramters. All of them fall short one way or another. I am going to try and not do that today. By the end of this blog we will create a Add-SHDUserToGroup command with Two Dynamic Parameters of User and Group.

Structure

So, Dynamic Parameters is a parameter that is more fluid. You can pull the structure of Active Directory and tab through users as a parameter or you can look at the inside of a file. The limits are bound to a single column array. This means you can’t use all of the Get-ADuser command. You have to parse it out with something like select-object. We will get more into that later. First thing first. Structure. Where does a Dynamic Parameter start? It starts after the param() and before your Begin {} tag.

function Add-SHDUserToGroup {
    [cmdletbinding()]
    param ()
    DynamicParam {}
    Begin {}
    Process {}
    End {}
}

To use the Dynamic Parameters you have to have the param() tag. This tag can stay blank. As scripters we always want our functions to be advanced functions. This way we can see the verbose tags as we run our tests. Thus me, you will need to run an test more than once.

Planning Stage

Before you go any farther. You need to plan out your Dynamic Parameter. In our example, I will be working with a company that has over active 4000 users in their AD. So, a get-aduser -filter * would bring running the command to it’s knees. The object you target to gather information from will execute a command and it will effect the PS session you are in. It may even crash your powershell. I learned this the hard way when calling a script to gather every user’s name from Azure AD. I always suggest using the # and list out the commands you want to run. Study those commands and find the fastest approach. You don’t want your user waiting 5 minutes to hit tab or spend 5 minutes tabbing through options they don’t need. The name should also be reflective. In this example we will be using one a single object and not an array. So no string[].

The Setup

Lets go line by line. I found the original part of this bad boy online on github. However, where it failed was how to use it. Then I found a script that showed how to use the dynamic parameter. I was confussed, but Hopefully you will not be. The first thing we want to do is name it.

$ParamName = "User"

The next item is to create a collection of attributes. The first part of this command creates the array. Kind of like an $a =@() deal, but not. We are going to use the System > Collections > Object Model > collections and select the system attributes. Inside the object Model there are many other types of collection items. Avoid those for now.

$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

Now we need to create and set the parameter’s attributes. We create the attribute set with the new-object cmdlet like before. System > Management > Automation > ParameterAttribute. This is the life of the parameter’s attribute. Then we set the attribute to mandatory by adding the .Mandatory = $true flag. Then we finally add this to our AtrributeCollection object we made in the previous steps using the .Add() method.

$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$AttributeCollection.Add($ParameterAttribute) 

Now it’s time to create the Directory. Once again, this is an object like before, thus new-object. We are living inside the System > Management > Automation world. This time we want the RuntimeDefinedParameterDictionary.

$RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

The dirctionary has been created. Now we need to fill that dictionary up with information to select from. So, lets create an array object and fill it it up. Then we will place that object into the dictionary. This is also the important part because this is the “Dynamic” part of the dynamic parameters. We will be selecting all the enabled user. Then we create a new object for the validation set. Similar to the [validateset()] inside the parameters. Then we add the vlidateset to the attributecollection again using the .add() option.

$arrSet = (Get-ADuser -Filter {enabled -eq $true}).name
$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)
$AttributeCollection.Add($ValidateSetAttribute)

Now we need the run time parameter set. We do this using the new-object. This set will basically hold the object’s name, its type, and the collection. Imagine it as the [validateset(“A”,”b”,”c”)][string]$users. Once again System > management > Automation > RuntimeDefinedParameter. Name, type, and collection. Once we have made this with the previous information, we need to add it to our dictionary with an add() once again.

$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParamName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParamName_portgroup, $RuntimeParameter)

Finally we want to return the RunTimeParameterDictionary.

return $RuntimeParameterDictionary

Phew, that’s a lot for one parameter! However, now we have a parameter that gives us a userful list of users with their system names we can call from. How do we use that information?

Begin Tag

Inside the begin tag, lets put the parameter into something that we can use inside the process tag.

$UserName = $PsBoundParameters[$ParamName]

Process tag

Now in the process tag all we have to do is use the information above ($username) and use that instead of using the same old information over and over again. We search for the username of the user.

if ($PSBoundParameters.ContainsKey('Credential')) {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        } else {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        }

End Tag

We finally release the data with the $returnInfo at the end.

end {
     $ReturnInfo
}

The Script

function Get-SHDADUsername {
    [cmdletbinding()]
    param (
        [securestring]$Credential
    )
    DynamicParam {
               
        # Set the dynamic parameters' name
        $ParamName = 'Users'
        # 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, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParamName, $RuntimeParameter)
        return $RuntimeParameterDictionary
    }

    begin{
        
        $username = $PsBoundParameters[$ParamName] 
    }
    process {
        if ($PSBoundParameters.ContainsKey('Credential')) {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        } else {
            $ReturnInfo = Get-ADUser -Filter {name -like $username} -Credential $Credential | Select-Object Samaccountname
        }
    }
    end {$ReturnInfo}
}
GPO – Comments

GPO – Comments

Did you know you can add a comment to a group policy? Did you know you can report on that comment as well. Basically, if you put the purpose and any additional information like security groups, applied to and not to, and so on and so forth, you can document and report on the group policy using PowerShell. Let me show you how.

Commenting Group Policy

There are two ways you can comment group policy, the first is the GUI. The second is through PowerShell. First we will do a comment via the GUI.

The GUI Way

  1. 300start Group Policy Management Console
  2. Select the policy object you wish to comment
  3. Right click and click edit on this policy
  4. In the navigation plane, right click the policy’s name and select properties
  5. Click the Comment tab
  6. Enter your comment
  7. Apply and Ok.

The PowerShell Way

The command we are going to be using is the get-gpo command. This command allows you to grab the single GPO or all of the GPOs in the company.

Get-GPO -Name "Control Panel Access"
Results of Get-GPO

You can see the comment we added to the policy earlier as the description. Why Microsoft hasn’t changed the name on the tab, I will never know. For anyone who is familiar with PowerShell there is no Set-GPO commandlet native to PowerShell. So, it’s time to put our thinking caps on and figure out what we can do. I piped this command into get-member.

Get-GPO -Name "Control Panel Access" | Get-Member
Results of the get member

Look what we found. The Description property has a set feature to it. This means you can set the property using the get-gpo command.. Bingo! Lets do this!

(Get-GPO -Name "Control Panel Access").Description = "This policy blocks access for all members of the employees group, except for those inside the sg_control_panel_access group from accessing control panel. 04/25/2021"
Results from the above command.

I wanted to add today’s date into the description. This way whoever comes behind me sees when it was written. Now let’s see if the GUI updated accordingly as well.

The new information is inside the policy

It updated like we believed it would. This is because of the set feature inside the property. The command allowed us to set the property. Such a simple and yet lovely thing. Now, you can run a report asking for just the name and description to get much more information. If you do this with all of your policies, life gets a little easier.

Maybe, one day I will use a dynamic parameter and create a set-GPOdescriptoin command to set the description of any gpo you can select from. Hum…

Like always, if you have any questions feel free to ask.