Clear Google Cache with Powershell

Clear Google Cache with Powershell

Yesterday I had to clear out a few users’ google chrome cache. It was a little aggravating going computer by computer. We were doing this because recently a core web app was updated. The application left traces of itself in the Google Chrome Cache and it caused all kinds of problems. So the last few I looked for a way to do it with PowerShell. Long and behold you can Clear Google Cache with Powershell.

The Script

We are starting with the script, to begin with. We are doing this because the core of this script is wrapped around a remote template I use. I will cover the template later down the road.

Function Clear-SHDGoogleCache {
    param (
        [parameter(
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True,    
            HelpMessage = "Hostname of target computer", 
            Mandatory = $true)][alias('ComputerName')][String[]]$Computer,
        [Parameter(
            HelpMessage = "Allows for custom Credential.")][string[]]$username,
        [Parameter(
            HelpMessage = "Allows for custom Credential.")][System.Management.Automation.PSCredential]$Credential
    )
    begin {
        if ($null -eq $username) { $username = "*" }
    }
    process {
        foreach ($PC in $Computer) {
            foreach ($user in $username) {
                $Parameters = @{
                    Computername = $PC
                    ScriptBlock  = {
                        if ($username -ne "*") {
                            if (Test-Path C:\Users\$user) {
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\default\cache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\default\code cache\js\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\default\media cache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\Default\Service Worker\CacheStorage\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\Default\Service Worker\ScriptCache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                            }
                            else {
                                Write-Error "$user is not present."
                            }
                        }
                        else {
                            Remove-Item "c:\users\*\appdata\local\google\chrome\user data\default\cache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                            Remove-Item "c:\users\*\appdata\local\google\chrome\user data\default\code cache\js\*" -Recurse -Force -ErrorAction SilentlyContinue 
                            Remove-Item "c:\users\*\appdata\local\google\chrome\user data\default\media cache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                            Remove-Item "c:\users\*\appdata\local\google\chrome\user data\Default\Service Worker\CacheStorage\*" -Recurse -Force -ErrorAction SilentlyContinue 
                            Remove-Item "c:\users\*\appdata\local\google\chrome\user data\Default\Service Worker\ScriptCache\*" -Recurse -Force -ErrorAction SilentlyContinue   
                        }
                    }
                    Asjob        = $true
                    JobName      = $PC
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $Parameters | Add-Member -MemberType NoteProperty -Name Credential -Value $Credential
                }
                if (Test-Connection -ComputerName $PC -Count 1 -Quiet) {
                    try {
                        Invoke-Command @Parameters
                    }
                    catch {
                        Write-Warning "$PC Invoke Command Failed"
                    }
                }
                else {
                    Write-Warning -Message "$PC is offline"
                }
            }
        }
    }
    end {}
}

The Breakdown

Let’s break down the script and see what is needed and how it is needed. The first thing you will notice is the computer and the user are both lists of strings. [String[]]. This means I will have to loop through each one. This is important because this means you can target a single user on multiple machines or multiple users on a single machine or both. The second thing I want to point out is the credentials. So, if you are not in admin mode, you can deploy the script with your admin creds, or with the local admin creds.

The Username is not a required item. Why did I do this? The simple answer is, if you don’t put a username, then it will clear every user’s google chrome cache. Notice in the “begin” area, we have if null is equal to username, then we want the username to be *. Later we ask, if the username is not equal to *, then we use the user. If not, we use the * which will go through all the users at once. Also notice in the do the user, we test if the user exists. If it doesn’t we deploy an error, if it does, we do our work.

if ($null -eq $username) { $username = "*" }
if ($user-ne "*") { do the user
    if (Test-Path C:\Users\$user) { Its there, lets go for it.} else { Error will robison.}
} else {do everyone}

The Core

At the core of this script is the remove-item blurp. We are going through each user data area and clearing out the different types of cache. There is the default cache, code, media, storage, and script caches. Each of these folders will have folders inside of them. So we need to recurse. We want to force it and we don’t care about the errors as some cache will not delete while chrome is active. Could I have added a kill chrome, yes, but why? if the end-user is working in chrome, this is going to be disruptive and force them to restart chrome. Lets look at the code.

Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\default\cache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\default\code cache\js\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\default\media cache\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\Default\Service Worker\CacheStorage\*" -Recurse -Force -ErrorAction SilentlyContinue 
                                Remove-Item "c:\users\$user\appdata\local\google\chrome\user data\Default\Service Worker\ScriptCache\*" -Recurse -Force -ErrorAction SilentlyContinue 

That’s all the code you will need. If you selected not to use username the $user will turn into * which will do every user, including default. if you have something like sccm, pdq, intune, ninja, pulse, etc, then just user this part of the code with * instead of user. This will clear the cache as needed.

We close it all up and send it as a job to the machine in question. This way we are not stuck on each computer. It speeds things up. With Powershell 7, you can loop with a number of objects that you want which would speed this up even more.

Additional Reading

SSH with Powershell

SSH with Powershell

Recently I went over how to get information from the Unifi Controller. Now I want to show you how to grab information from the unifi device itself. In this example, we are going to use SSH with Powershell to pull some basic information from an unifi AP.

SSH with Powershell

The first thing you will need is to make an ssh connection. Most computers have open ssh installed and you can type in ssh username@ipaddress and access it after inputting passwords and such. However, I want to work with powershell and not whatever shell the unifi will present me. Thus, we will run through a module called POSH-SSH by darkoperator.

Install Posh-ssh

The first thing you will need to do, if you haven’t already, is installing the module. This module lives in the standard repos, which makes life so much easier. Here is the command:

Install-Module -Name Posh-SSH

Once you have the command installed, that’s when we import with the import module command.

Making a connection

The next step is to make the connection. The feel of this command is just the same as that of enter-pssession. We call the computer name, normally an IP address, and we add the credentials. The biggest difference is we add an acceptkey flag. You can also give your session an ID number, this is good if you are doing more than one at a time. Today we are only focusing on one connection. So it’s not needed, but it becomes very useful with loops. Like always, these sessions start at 0.

New-SSHSession -ComputerName 10.0.0.8 -AcceptKey -Credential (Get-Credential)

This creates a session of 0 which we can send commands to later. Remember, each session connection adds another session number. So the next one will be 1 and so on so forth. if you need more information or become lost with which sessions you are using, get-sshsession will help resolve this question.

Invoking SSH commands with Powershell

The next thing we need to do is invoke commands. What is the point of just connecting when we have the power of ssh and powershell? The command we can use is called invoke-sshcommand. When working with any system, you need to be mindful of that system’s shell. Ssh gives you the connection, and that system determines what you can use. This took me a while to figure out. One of the commands inside the unifi devices is info. This gives you useful information. So when you are connected, typing info will telling you IP addresses, mac address, inform statuses, and more. However, with the SSHcommand, it produces nothing useful. As the image below shows.

So, what is needed to do from here is adding a flag to let it know you are coming from somewhere else. This flag is “mca-cli-op” followed by the command you want. However, as the image below suggests, it parses the output as a single object. We need to go deeper by selecting the output.

$Output = (Invoke-SSHCommand -Command "mca-cli-op info" -SessionId 0).output

This is nice to know, however… It’s a string. You will need to parse this data out accordingly. I do this by using select-string then from there working each item. When I select the string, for example, model. I take the output and select-string model. Then i convert that to a string as select string make it into an match info object. Which isn’t as helpful as you would think. Then from there, I replace any spaces and split from the : to get the information I need. Below is the code.

$Return = [PSCustomObject]@{
        model = ($output | select-string model).tostring().replace(' ','').split(":")[1]
        version = ($output | select-string version).tostring().replace(' ','').split(":")[1]
        MacAddress = ($output | select-string mac).tostring().split(' ')[-1]
        IPaddress = ($output | select-string ip).tostring().replace(' ','').split(":")[1]
        Hostname = ($output | select-string hostname).tostring().replace(' ','').split(":")[1]
        UptimeSeconds = ($output | select-string uptime).tostring().replace(' ','').split(":")[1] -replace("\D",'')
        Status = ($output | select-string status).tostring().split(':').split('(').replace(' ','')[1]
        Inform = ($output | select-string status).tostring().split(' ')[-1] -replace('[()]','')
    }

From here you can change the inform, or other items using the flag and the command accordingly. Once you are finished with your session, make sure to remove it with the Remove-SSHSession command.

The Script

Here is the script. This script allows you to do more than one IP address.

function Get-UnifiSSHDeviceInformation {
    param (
        [parameter(
            ValueFromPipeline = $True,
            ValueFromPipelineByPropertyName = $True,    
            HelpMessage = "Unifi Username and Password", 
            Mandatory = $true)][alias('IP')][ipaddress[]]$IPaddress,
        [parameter( 
            HelpMessage = "Unifi Username and Password", 
            Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential
    )
    $return = @()
    foreach ($IP in $IPaddress) {
        New-SSHSession -ComputerName $IP -AcceptKey -Credential $Credential
        $Output = (Invoke-SSHCommand -Command "mca-cli-op info" -SessionId 0).output
        Remove-SSHSession -SessionId 0
        $Return += [PSCustomObject]@{
            model = ($output | select-string model).tostring().replace(' ','').split(":")[1]
            version = ($output | select-string version).tostring().replace(' ','').split(":")[1]
            MacAddress = ($output | select-string mac).tostring().split(' ')[-1]
            IPaddress = ($output | select-string ip).tostring().replace(' ','').split(":")[1]
            Hostname = ($output | select-string hostname).tostring().replace(' ','').split(":")[1]
            UptimeSeconds = ($output | select-string uptime).tostring().replace(' ','').split(":")[1] -replace("\D",'')
            Status = ($output | select-string status).tostring().split(':').split('(').replace(' ','')[1]
            Inform = ($output | select-string status).tostring().split(' ')[-1] -replace('[()]','')
        }
    }
    $Return
}

Additional Reading

Find Forwarding Rules

Find Forwarding Rules

Not too long ago, I needed to do some rule auditing for forwarders in a client’s exchange online. They believed someone had a rule in their exchange account that was forwarded to a spammer. They believed this because new employees were receiving emails within a few days of creation. So, it’s time for some PowerShell magic to save the day. It’s time to Find Forwarding Rules in your mailboxes with PowerShell.

The Script

Connect-ExchangeOnline
$Mailboxes = Get-Mailbox -ResultSize Unlimited
$ForwarderRules = foreach ($Mailbox in $Mailboxes) {
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{
                Username = $Mailbox.Alias
                Rule = $Rule.name
                ID = $Rule.RuleIdentity
                Enabled = $rule.enabled
                ForwardTo = $rule.ForwardTo | where-object {$_ -like "*@*"}
                ForwardAsAttachmentTo = $rule.ForwardAsAttachmentTo | where-object {$_ -like "*@*"}
            }
        }
    }
}
$ats = $ForwarderRules | where-object {($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo)}
$ats

The Breakdown

The script today requires the Exchange Online Module to be installed. If you don’t have it, go install it. Once you have it, you will need to connect using the Connect-ExchangeOnline commandlet.

Connect-ExchangeOnline

By doing it this way, MFA will be triggered and we want MFA to be at this level. Security first yall. This brings me to my next point, soon exo 3 will come out and security will be improved greatly.

Once you are connected, we need now to pull all the mailboxes from the system. This command can take some time if you have a large company. In fact, this script with only 300 users took around an hour. The Larger your company is, the longer it will take. Plan ahead accordingly.

$Mailboxes = Get-Mailbox -ResultSize Unlimited

Now we have all the mailboxes, we need to go through each mailbox and get the inbox rules for that mailbox. We start a for each loop of the mailboxes.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 

}

Next, we will need to grab the inbox rules for that mailbox. We do this with the Get-InboxRule commandlet and we feed it the mailbox alias.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
}

Normally a mailbox has more than one rule. Thus, we need to make another for each loop for the rules inside our main foreach loop.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
    
    }
}

Afterward, we need to pull the data out of the rules and make it useful. The amount of output is large, breaking it down and making it useful is important. That’s the whole goal of this. We want to find out who has forwarders and we want to know if those forwarders are forwarding out to someone else. I want to break it up as well so I can look at all the forwarders and just the ones with email addresses.

Gathering Information

Firstly, we need to ask the question, Are we forwarding to someone as an email or an attachment? The properties we want to look at are, forwardto and forwardasattachmentto. If either of these are not null, then we want to look at that information. This allows us to Find Forwarding Rules.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
        
        }
    }
}

Now we are looking at a rule object that has a forwarder of some sort. It’s time to let the end user know. Next, we will create a PowerShell Custom Object. Almost every get command I have come across has produced one of these objects.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) { 
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{

            }
        }
    }
}

The object is ready for us. It’s time to fill it in with useful information. We need the mailbox name, the rule name, the rule’s id, if it’s enabled, and finally the forwarder information. The forwarder information is broken up into two. The “ForwardTo” and the “ForwardAsAttachmentTo”. The first forwards the email to a person. The second wraps up the email into an attachment and sends it to the person. We need to see both.

These items are arrays of email addresses and references. If the forwarder points to an external email address it will contain the @ symbol like most email addresses do. If the forwarder points to an internal address like bob in accounting, then it will not have an @ symbol unless told otherwise. This is useful. We can use a where object to pull out the lines with an @ symbol.

$ForwarderRules = foreach ($Mailbox in $Mailboxes) {
    $rules = Get-InboxRule -mailbox $Mailbox.Alias
    foreach ($rule in $rules) {
        if (($null -ne $rule.ForwardTo) -or ($null -ne $rule.ForwardAsAttachmentTo)) {
            [pscustomobject][ordered]@{
                Username = $Mailbox.Alias
                Rule = $Rule.name
                ID = $Rule.RuleIdentity
                Enabled = $rule.enabled
                ForwardTo = $rule.ForwardTo | where-object {$_ -like "*@*"}
                ForwardAsAttachmentTo = $rule.ForwardAsAttachmentTo | where-object {$_ -like "*@*"}
            }
        }
    }
}

Sorting the Sorted Information

Now it’s time to sort the sorted information. First why? Why not add it to the loop above? Two reasons. First is the time it takes to process. Second, I want to run $ForwarderRules to get information and I want to run the next line of code to see the more focused information. I like having options. Now we will take the forwarder rules we created and filter out the nulls of the forwarders. Finally, we want to display the information.

$ats = $ForwarderRules | where-object {($null -ne $_.ForwardTo) -or ($null -ne $_.ForwardAsAttachmentTo)}
$ats

Finally, you have all the email addresses and rules that have a forwarder that forwards to a real email address. You can run through each one and audit them for security.

Future Reading

Images created with Mid Journey AI

Get-Credentials to Plain Text

Get-Credentials to Plain Text

Sometimes I like to go back and see where I can improve my scripts. A few weeks ago I showed you the Unifi Controller script. Where it pulls the client information from each site. Well, I wanted to improve upon this script. A problem I see with this script is the plain text password. The unifi controller I have to test only accepts plain text, but I don’t want to have a password inside my PowerShell history. Today I want to show you how to convert Get-Credentials to Plain Text.

Get-Credentials as a Parameter

We first need to remove the Username and Password from the parameters and then create a secure credential parameter. By default, we want this to be a mandatory parameter. The type of parameter we will be using is a System Management Automation PScredential. This is what the parameter will look like.

[parameter( 
            HelpMessage = "Unifi Username and Password", 
            Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential

Convert Get-Credentials to Plain Text

Now it’s time to Covert the PSCredentials back to plain text. Inside our PSCredentials lives the username and password we gave the system. Thus we pull the secure string password from the PSCredentials.

$SecurePassword = $Credential.Password

Next, we are going to use the Marshal Class. This class is designed to bridge the gap between managed and unmanaged programming models. The method we will be using is the Secure String to BSTR. This method Allocates an unmanaged binary string and copies the content of a managed secure string object into it. Basically, we find where it lives in memory and combine it. For more information.

 $CodedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)

Aftward, we Take this coded data and push it into the following method. The PtrToStringAuto. This method allocates a managed string and copies all the characters up to the first null. This copy is from the string stored in unmanaged memory. This is why we need the Coded Password to tell us where the password lives in memory. Basically, I’m taking the coded password and making it human-readable.

$Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($CodedPassword)

The Script Rebuilt

Now we have a way to decrpt the get credential command, it’s time to dive back into the script. Here is the rebuilt script.

function Get-UnifiSitesClients {
        param (
            [parameter(
                ValueFromPipeline = $True,
                ValueFromPipelineByPropertyName = $True,    
                HelpMessage = "Unifi Username and Password", 
                Mandatory = $true)][alias('IP')][ipaddress]$IPaddress,
            [parameter( 
                HelpMessage = "Unifi Username and Password", 
                Mandatory = $true)][alias('UserNamePassword')][System.Management.Automation.PSCredential]$Credential,
            [parameter(
                HelpMessage = "Port Number", 
                Mandatory = $true)][alias('Port')][init]$portNumber
        )
        $SecurePassword = $Credential.Password
        $CodedPassword = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SecurePassword)
        $Password = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($CodedPassword)
        $Username = $Credential.UserName

        $uri = "https://$($IPaddress):$portNumber/api/login"
        $headers = @{'Accept' = 'application/json' }
        $params = @{
            'username' = $username;
            'password' = $Password;
        }
        $body = $params | ConvertTo-Json
     
        $response = Invoke-RestMethod -Uri $uri `
            -Body $body `
            -Method Post `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -SessionVariable s
        $uri = "https://$($IPaddress):$portNumber/api/self/sites"
        $sites = Invoke-RestMethod -Uri $uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s
        $Return = Foreach ($Site in $sites.data) {
            $Uri = "https://$($IPaddress):$portNumber/api/s/$($Site.name)/stat/sta"
            $Clients = (Invoke-RestMethod -Uri $Uri `
                -Method Get `
                -ContentType 'application/json' `
                -Headers $headers `
                -SkipCertificateCheck `
                -Websession $s).data
            Foreach ($Client in $Clients) {
                [pscustomobject][ordered]@{
                    Site = $Site.name
                    SiteDescritption = $Site.desc
                    OUI = $client.OUI
                    MacAddress = $client.mac 
                    IPAddress = $Client.IP
                    SwitchMac = $client.sw_mac
                    SwitchPort = $client.sw_port
                    WireRate = $client.wired_rate_mbps
                }
            }
        }
        $return
    }

Image created with mid-journey

LAPS Password With PowerShell

LAPS Password With PowerShell

A few of my clients use something called LAPS. Laps change the local administrator password on a computer and then store the information inside Active Directory. Since I don’t dive deep into this client’s computers often, I needed something to quickly type the first letter of the computer in question to pull up the LAPS password. Basically, I needed a list of computer names from the command itself. This is fully possible with Dynamic Parameters. So, today we will be grabbing the LAPS password with PowerShell.

Where Does LAPS Password live?

Most companies that set up LAPS do so with Active Directory. By default, Active Directory saves the password into an attribute called “ms-Mcs-AdmPwd” and LAPS also stores the expiration date in “ms-Mcs-AdmPwdExpirationTime” Thus, all you have to do is call a get-adcomputer command and pull out the information.

Get-Adcomputer -filter {name -like $Computer} -properties name,ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime | select-object name,ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime 

Now the “ms-Mcs-AdmPwdExpirationTime” is unique and needs to be parsed into something more readable. We can use the m method called [datetime] to do this.

Get-Adcomputer -filter {name -like $Computer} -properties name,ms-Mcs-AdmPwd,ms-Mcs-AdmPwdExpirationTime | select-object Name, @{l = "AdminPassword"; e = { $_."ms-Mcs-AdmPwd" } }, @{l = "AdminPasswordExpireTime"; e = { [datetime]::FromFileTime(($_."ms-Mcs-AdmPwdExpirationTime")) } }

There you have it, That’s how you get the LAPS password, But I want to take this one step further. I don’t know all the computer names. I want that information at my fingertips while I type out the command. So, I want to type something like Get-LAPS -ComputerName <here populate a name where I can tab> and bamn, it gives it to me when I hit enter. That’s where we will dive into dynamic parameters next.

Adding Dynamic Computer Name Parameters

In a previous article, we went over how to make a dynamic parameter. I want to help refresh memories by doing a single parameter and show you how it can be done with this function.

The first thing we need to do is create our form. This form allows us to use the dynamic parameters and gives us spots to pull data. This is more of a road map than anything else, but it is required for dynamics.

function Get-LapsPassword {
    [cmdletbinding()]
    Param()
    DynamicParam {}
    Begin {}
    Process {}
    End {}
}

The first part of our Dynamics is we want to name the parameter template. From there, we want to create a new object. This object will be the system collections object model for collections. AKA system attributes. Then we want to make an attribute object to add to that object later.

Building Out Objects

$paramTemplate = 'ComputerName' 
$AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute

The ParameterAttribute will be where we add the flags like mandatory and position. We add those by dropping them directly into the ParamterAttribute object. A fun little fact, you can tab through and see what other items are available for this object. Things like the help message, the value from the pipeline, and more are hidden here. Today we only care about the mandatory and position.

$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1

After we build out our parameter Attribute object, we need to add it to the Attribute Collection we made at the start. We do this by using the “.add()” function of objects.

$AttributeCollection.Add($ParameterAttribute)

Now we need to create another object. This will be the Runtime Parameter Directory. Basically, what they will be looking through. This is a system management automation object called runtime defined parameter directory. Say that 10 times fast…

More Objects

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

Now we need to make our Validate Set. We will create an array of devices using the Get-adcomputer command. Here we will push (Get-adcomputer -filter {enabled -eq “true”}).name into a variable. Now we will have a list of active computers. Notice that we filter out all other information by using the “.name” call.

$ParameterValidateSet = (Get-ADComputer -Filter { enabled -eq "true" -and OperatingSystem -Like '*Windows*' -and OperatingSystem -notlike "*Server*" }).name

Next, we need to create another object. This object is the system management automation validate set attribute object. We can feed this object our Parameter Validate Set.

$ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ParameterValidateSet)

Afterward, it’s time to feed the Validate Set attribute to the attribute collection from the beginning. We can accomplish this by using the “.add()” method.

$AttributeCollection.Add($ValidateSetAttribute)

Next, it’s time to bring our Attribute collection into the command line. It’s time to make the run-time parameter. Once again, a new object. This time it’s the Run time Defined Parameter object. Like the last object, we can place our data directly into it. We will want the parameter’s name, the type, a string in this case, and the validate set.

$RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($paramTemplate, [string], $AttributeCollection)

Afterward, we take the above parameter and place it into our directory with the “.add()” method. We need the parameter Template and the Run time Parameter.

$RuntimeParameterDictionary.Add($paramTemplate, $RuntimeParameter) 

Finally, in the dynamic parameter block, we return our directory.

return $RuntimeParameterDictionary

Beginning

We are almost done. It’s time to bring the dynamic parameter into the function and make it useable. We do this in the beginning section. We shove the PSBoundParameters of our template name into a variable.

$MemberName = $PSBoundParameters[$paramTemplate]

Then from there, we call the $memberName in our Get-adcomputer command.

The Script

It’s that time, it’s time to put it all together, so you can copy and past it into your toolbox. It’s time To Grab LAPS Password With PowerShell.

function Get-LapsPassword {
    [cmdletbinding()]
    Param()
    DynamicParam {
        # Need dynamic parameters for Template, Storage, Project Type
        # Set the dynamic parameters' name
        $paramTemplate = 'ComputerName' 
        # 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
        $ParameterAttribute.Position = 1
        # 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
        $ParameterValidateSet = (Get-ADComputer -Filter { enabled -eq "true" }).name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($ParameterValidateSet)
        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute) 
        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($paramTemplate, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($paramTemplate, $RuntimeParameter) 
        return $RuntimeParameterDictionary
    } # end DynamicParam
    BEGIN {
        $MemberName = $PSBoundParameters[$paramTemplate]
    } # end BEGIN
    Process {
        $ComputerInfo = Get-ADComputer -Filter { name -like $MemberName } -Properties * 
    }
    End {
        $ComputerInfo | select-object Name, @{l = "AdminPassword"; e = { $_."ms-Mcs-AdmPwd" } }, @{l = "AdminPasswordExpireTime"; e = { [datetime]::FromFileTime(($_."ms-Mcs-AdmPwdExpirationTime")) } }
    }
}

Additional Reading

Get Client Information From your Unifi Controller

Get Client Information From your Unifi Controller

Today I would like to go over how to get client information from your Unifi Controller. We will first connect to our Unifi Controller with Powershell using the Unifi API. Then from there we will pull each site and create a list of different sites. Finally, we will use the site information to pull the client information from the controller.

Powershell and API the Unifi Controller

The first step is to create a connection with our PowerShell. We will need a few items:

  • User Account that Has at least read access
  • The IP address of our controller
  • The API port.
  • An Unifi Controller that is configured correctly. (Most are now)

Ask your system administrator to create an account with read access. Check your configuration for the IP addresses and the Port numbers. By default, the API port is 8443. Now we have those pieces of information, its time to discuss what is needed.

Connecting to the API

To connect to the Unifi controller API, you need a Cookie. This is not like how we connected to the Graph API in our previous blog. To get this cookie, we will need to execute a post. The API URL is the IP address followed by the port number, API and login. Very standard.

$uri = "https://$($IPAddress):8443/api/login"

We will need to create a header that accepts jsons. To do this we simply say accept equal application/json.

$headers = @{'Accept' = 'application/json' }

Now we will create our Parameters for the body. Since this is going to be a Post command, we have to have a body. This is where we will place our Username and Password. We are going to build out a Hashtable and then convert that into a json since the API is expecting a json. We will use the convertto-json command to accomplish this step.

$params = @{
        'username' = $username;
        'password' = $Password;
}
$body = $params | ConvertTo-Json

Now we have our Body built out, it’s time to create a session. We will use the invoke-restmethod command to do this. We feed the URI we made a few steps back. The body will be the body we just built out. The content type will be ‘application/json’. Our headers will be the header we made a few steps back. We will skip the certificate check unless we are using a cert. Most of the time we are not. Finally, the secret sauce. We want a session. Thus, we use the Session Variable and give it a session name. Let’s go with S for the session.

$response = Invoke-RestMethod -Uri $uri `
        -Body $body `
        -Method Post `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -SessionVariable s

Grabbing Sites

Now we have our Session cookie, its’ time to grab some information. We will use the same headers but this time we will use the Session Name, S. The URL for sites is /api/Self/Sites.

$uri = "https://$($IPaddress):8443/api/self/sites"

This URL will get the sites from your Unifi Controller. Once again we will use the Invoke-RestMethod to pull the data down. This time we are using a get method. The same Content type and headers will be used. Along with the skip certificate check. The Session Variable is going to be the Web Session and we will use the $S this time.

$sites = Invoke-RestMethod -Uri $uri `
        -Method Get `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -Websession $s

The Sites variable will contain two items. a meta and a data variable. Everything we will need is inside the Data variable. To access it we can use the $sites.data to see all the data.

Here is an example of what the data will look like for the sites.

_id          : 5a604378614b1b0c6c3ef9a0
desc         : A descriptoin
name         : Ziipk5tAk
anonymous_id : 25aa6d32-a165-c1d5-73a0-ace01b433c14
role         : admin

Get Client Information from your Unifi Controller

Now that we have the sites, we can go one at a time and pull all the clients from that site. We will do this using a Foreach loop. There is something I want to point out first. Remember, the data that is returned is two variables, two arrays. The first one is Meta. Meta is the metadata of the session itself. It’s something we don’t need. We do need the Data. For our foreach loop, we will pull directly from the data array. We want to push everything in this loop into a variable. This way we can parse out useful data, sometime to much data is pointless.

$Clients = Foreach ($Site in $Sites.Data) {
    #Do something
}

Without using the $Sites.Data we will have to pull the data from the command itself. This can cause issues later if you want to do more complex things.

The URL we will be using is the /api/s/{site name}/stat/sta. We will be replacing the Site name with our $site.name variable and pushing all that into another Uri.

$Uri = "https://$($IPaddress):8443/api/s/$($Site.name)/stat/sta"

Then we will execute another invoke-restmethod. Same as before, we use the same headers, content type, and web session. The only difference is we wrap up the command inside preferences. This way we can pull the data directly while the command executes. Less code that way.

(Invoke-RestMethod -Uri $Uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s).data

Each time the command runs, it will release the data that we need and that will all drop into the $Clients variable. From here, we pull the information we want. The information that the client’s produce includes, the match addresses, times, IP addresses, possible names, IDs and more. So, it’s up to you at this point to pick and choose what you want.

The Script

The final script is different because I wanted to add site names and data to each output. But here is how I built it out. I hope you enjoy it.

function Get-UnifiSitesClients {
    param (
        [string]$IPaddress,
        [string]$username,
        [string]$portNumber,
        [string]$Password
    )
    
    $uri = "https://$($IPaddress):$portNumber/api/login"
    $headers = @{'Accept' = 'application/json' }
    $params = @{
        'username' = $username;
        'password' = $Password;
    }
    $body = $params | ConvertTo-Json

    $response = Invoke-RestMethod -Uri $uri `
        -Body $body `
        -Method Post `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -SessionVariable s
    $uri = "https://$($IPaddress):$portNumber/api/self/sites"
    $sites = Invoke-RestMethod -Uri $uri `
        -Method Get `
        -ContentType 'application/json' `
        -Headers $headers `
        -SkipCertificateCheck `
        -Websession $s
    $Return = Foreach ($Site in $sites.data) {
        $Uri = "https://$($IPaddress):$portNumber/api/s/$($Site.name)/stat/sta"
        $Clients = (Invoke-RestMethod -Uri $Uri `
            -Method Get `
            -ContentType 'application/json' `
            -Headers $headers `
            -SkipCertificateCheck `
            -Websession $s).data
        Foreach ($Client in $Clients) {
            [pscustomobject][ordered]@{
                Site = $Site.name
                SiteDescritption = $Site.desc
                OUI = $client.OUI
                MacAddress = $client.mac 
                IPAddress = $Client.IP
                SwitchMac = $client.sw_mac
                SwitchPort = $client.sw_port
                WireRate = $client.wired_rate_mbps
            }
        }
    }
    $return 
}

Additional Resource