O365 Mobile Devices

O365 Mobile Devices

I needed to grab all the mobile devices that had emails attached to them not too long ago. I could have gone through the GUI, and spent a few days trying my best to document everything, or I could use Powershell to pull the O365 Mobile Devices. Can you guess which one I did? Yep, PowerShell is a way of thinking nowadays in the system admin world.

The Script

Connect-ExchangeOnline
$Mailboxes = get-mailbox -all
$Devices = foreach ($Mail in $Mailboxes) {
    $MobileDevices = Get-MobileDevice -Mailbox $mail.userprincipalname
    foreach ($Device in $MobileDevices) {
        $Stats = Get-MobileDeviceStatistics -Identity $Device.Identity
        [pscustomobject]@{
            User = $mail.userprincipalname
            FriendlyName                  = $Device.FriendlyName
            name                          = $Device.name
            Identity                      = $Device.Identity
            DeviceID                      = $Device.DeviceId
            DeviceOS                      = $Device.DeviceOS
            DeviceType                    = $Device.DeviceType
            DeviceModel                   = $Device.DeviceModel 
            ClientType                    = $Stats.ClientType
            FirstSyncTime                 = $Device.FirstSyncTime
            WhenChanged                   = $Device.WhenChanged
            LastPolicyUpdateTime          = $Stats.LastPolicyUpdateTime
            LastSyncAttemptTime           = $Stats.LastSyncAttemptTime
            LastSuccessSync               = $stats.LastSuccessSync
            DeviceAccessState             = $Device.DeviceAccessState
            DeviceAccessStateReason       = $Device.DeviceAccessStateReason
            IsValid                       = $Device.IsValid
            Status                        = $Stats.Status
            DevicePolicyApplied           = $stats.DevicePolicyApplied
            DevicePolicyApplicationStatus = $stats.DevicePolicyApplicationStatus
        }
    }
}
$Devices

The Break Down

This script is pretty straightforward. We first connect to exchange online. Currently, this script is designed to work with MFA. Then we grab all the mailboxes from O365.

Connect-ExchangeOnline
$Mailboxes = get-mailbox -all

Next, we loop through these mailboxes. We drop all the information get gather from this loop into a variable. I called my devices. Once again, nothing great or grand.

$Devices = foreach ($Mail in $Mailboxes) {}

For each mailbox we have, we pull all of the devices from that mailbox. Since a single mailbox can have more than one device, we loop through these devices. For each loop, we want to grab that device’s stats to gain more insights.

$MobileDevices = Get-MobileDevice -Mailbox $mail.userprincipalname
    foreach ($Device in $MobileDevices) {
        $Stats = Get-MobileDeviceStatistics -Identity $Device.Identity

Finally, from there we gather the information into a single object. These commands produce a lot of information. It’s best to parse it down some. That’s what I did with the next part. That’s how I am able to get all the O365 Mobile Devices using PowerShell.

Additional Reading

Set Telemetry with Powershell

Set Telemetry with Powershell

Windows 10 and 11 have a unique feature called telemetry. This feature allows Microsoft to track your device. For security reasons, it’s best to disable this, however, for a home level, this feature is useful. So, let’s Set Telemetry with Powershell.

Registry Keys and Services

There are 4 registry keys that can do this.

  • HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection\AllowTelemetry
  • HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection\AllowDeviceNameInTelemetry
  • HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection\AllowTelemetry
  • HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Policies\DataCollection\AllowTelemetry

Along with the registry keys you will need to work with a service called “Diagtrack“. This service while active, can track your computer. To disable telemetry, we must disable diagtrack from the startup and we can do that with Powershell.

Disable Telemetry

Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" -Name "AllowTelemetry" -Type DWord -Value 0
Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" -Name "AllowDeviceNameInTelemetry" -Type DWord -Value 0
Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" -Name "AllowTelemetry" -Type DWord -Value 0
Set-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Policies\DataCollection" -Name "AllowTelemetry" -Type DWord -Value 0

Get-Service -Name "DiagTrack" | Stop-Service -NoWait -Force
Get-Service -Name "DiagTrack" | set-service -StartupType Disabled

Here we are adding different values. We are setting our 4 registry keys to 0. Then we stop the diagtrack server. Then we set the startup to disabled, This way the service can’t be restarted.

Doing this ensures that the service doesn’t come back to life and if it does, it has no idea what to do.

Enable Telemetry

Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" -Name "AllowTelemetry"
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\DataCollection" -Name "AllowDeviceNameInTelemetry"
Remove-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" -Name "AllowTelemetry"
Remove-ItemProperty -Path "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Policies\DataCollection" -Name "AllowTelemetry"

Get-Service -Name "DiagTrack" | set-service -StartupType Manual
Get-Service -Name "DiagTrack" | start-service

To enable telemetry, we are simply doing the opposite. We remove the registry keys. Then we enable the services. Finally, we start the service.

There you have it, We have Set telemetry with Powershell

Additional Reading

Exchange Mailbox Rules

Exchange Mailbox Rules

At one of my previous Jobs, I had to write a script to help me understand rules. Well, I pulled that script out of the vault to do the same thing recently. The following script will allow you to select one user or multiple users to grab their rules. Or you can just forget the flag and get all of the users at once. The idea of the script is to get basic information from the rule, not all the information as it’s not all useful. So let’s look at the script then we will do a breakdown. Here comes Exchange Mailbox Rules through PowerShell.

The script

function Get-MailboxInboxRules {
    [cmdletbinding()]
    param (
        [string[]]$UserPrincipalName,
        [switch]$StayConnected
    )
    begin {
        #Begining tests if the module is installed, loaded, and connected. Correcting each one at each level. 

        #Installs required modules
        Write-Verbose "Installing required modules"
        if (!(Get-InstallEdModule ExchangeOnlineManagement)) { 
            try {
                Install-Module ExchangeOnlineManagement
            }catch {
                Write-Error $_
                exit
            } 
        }
 
        Write-Verbose "Checking and importing required modules"
        # Starts importanting required modules
        if (!(Get-Command Connect-ExchangeOnline)) { 
            try {
                Import-Module ExchangeOnlineManagement
            } catch {
                Write-Error $_
                exit
            }
        }

        #Tests if Exchange Online is connected, If not, we trigger a connection
        if (!(Get-PSSession | Where-Object { $_.Name -match 'ExchangeOnline' -and $_.Availability -eq 'Available' })) { 
            try {
                Connect-ExchangeOnline}
            catch {
                Write-Error $_
                exit
            } 
        }
        if (!(Get-PSSession | Where-Object { $_.Name -match 'ExchangeOnline' -and $_.Availability -eq 'Available' })) {
            Write-Error "Connection failed"
            exit
        }
    }
    Process {
        $mailboxes = @()
        if (!($PSBoundParameters.ContainsKey("UserPrincipalName"))) {
            $Mailboxes = Get-mailbox
        }
        else {
            foreach ($Username in $UserPrincipalName) {
                try {
                    $Mailboxes += get-mailbox $username -ErrorAction SilentlyContinue
                }
                catch {
                    Write-Warning "$username has no mailbox"
                }
            }
        }

        #It has been proven to speed up the script if we drop the output into a return value
        $RulesReturn = Foreach ($Mail in $Mailboxes) {
            
            Write-Verbose "Scanning: $($Mail.UserPrincipalName)"
            #We are now trying to catch the rule in question. 
            #if the mailbox has no rules, which will be presented as an error, then we null out the rules. 
            try {
                $Rules = Get-InboxRule -Mailbox $Mail.Userprincipalname
            }
            catch {
                $Rules = $null  
            }

            #Now that the rules either have value or null, we can test. 
            if ($null -ne $Rules) {
                #Write-Host "$($mail.userprincipalname)"

                #Now we know we have rules, it's time to start working with those rules
                foreach ($Rule in $Rules) {

                    #From my testing I discover that some rules will be null but the rule itself isn't null. 
                    #Thus we need to test if this is the case. 
                    if ($null -ne $rule) {

                        #Now we have confirmed the rule is not empty, we need to test the form
                        #This is because some rules are based on subjects and such and not from people. 
                        if ($null -ne $rule.from) {
                            
                            #since form is not empty we split the string and we get the from information. 
                            $froms = $Rule.from.split('[')[0]
                        }
                        else {

                            #if it is, we just blank string the value. 
                            $froms = ""
                        }

                        #Next we want the description to be on a single line. this way we can export to a csv. 
                        if ($null -ne $rule.description) {

                            #This is programmed for the standard 5.1 powershell. Join-String was introduced in 6.2
                            #to combat this, we create a null return
                            #Then we  split the description rule. 
                            #Then we for each that list and push it back into the return.
                            
                            $dereturn = $Null
                            $rule.description.split("`n") | foreach-object { $dereturn = "$dereturn $($_.trim())" }
                            $description = $dereturn.trim()
                        }
                        else {
                            $description = ""
                        }
                        #Next we create our ps object with items we need for basic level audits. 
                        [pscustomobject]@{
                            Username    = $Mail.Userprincipalname
                            Identity    = $Rule.Identity
                            Enabled     = $Rule.Enabled
                            Name        = $Rule.Name
                            from        = $froms
                            Description = $description
                        } 
                    }
                }
            }
        }
    }
    end {

        #At the end we return
        $RulesReturn

        #Then we disconnect if the user didn't say stay connected. 
        if (!($StayConnected)) { Disconnect-ExchangeOnline -Confirm:$false }
    }
}

The Breakdown

Before we begin, I want to point out, at this point, there is no graph API calls for exchange. Not yet, its promised, but as of right now, it’s not reality yet. Security is a big thing to think about with scripts like these. MFA is a big piece of security. We always want some mfa going. This is why you will not see credentials in the parameters.

Parameters

Our parameters are simple. We have a list of strings of user principal names and we have a switch to determine if we need to stay connected. If we leave the user principal name blank, then we are targeting every mailbox. It may take some time. The stay connected is designed to keep exchange connected. This way once you have the exchange mailbox rules, you can run other commands. You can also leave this blank, if you do, it disconnects from exchange. Good security thinking here.

param (
        [string[]]$UserPrincipalName,
        [switch]$StayConnected
)

Begin

Next is our begin. We are doing some house keeping here. First, We need to know if the exchange online management is installed. if it’s not, we then install it. Next, test if the exchange online management is imported. if not, we import the module. Finally we test if we have an exchange online connection, if not, we use connect-exchange. We use the connect-exchangeonline because it will do the MFA for us. Which is nice.

To test if the module is installed, we use the Get-InstalledModule. This command searches your modules and lets you know if it is installed. Older Powershells do not know this command. If the powershell is not elevated, then this code will error out and exits with the exit code.

Write-Verbose "Installing required modules"
if (!(Get-InstallEdModule ExchangeOnlineManagement)) { 
        try {
            Install-Module ExchangeOnlineManagement
        }catch {
            Write-Error $_
            exit
        } 
}

If we haven’t exited out yet, we then see if the module is loaded by using the get-command command. We are looking for the command Connect-ExchangeOnline. If the command exists, we will continue on, if not, we will use the import-module command and import the exchangeonlinemanagement module. Of course, if we run into an error, we exit with the error.

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

Finally, we are going to pull all the PowerShell Sessions currently on this computer. This is where we will gain the ability to pull Exchange Mailbox Rules. The first command is get-pssession. We search the output for a session with the name of ExchangeOnline that is available. If we don’t find one, we connect to exchange using the connect-exchangeonline. Next, if connection errors out, we present the error and disconnect. Now, if the connection is successful, we test once more for a connection. If it’s just not there, we give up and say bye.

#Tests if Exchange Online is connected, If not, we trigger a connection
        if (!(Get-PSSession | Where-Object { $_.Name -match 'ExchangeOnline' -and $_.Availability -eq 'Available' })) { 
            try {
                Connect-ExchangeOnline}
            catch {
                Write-Error $_
                exit
            } 
        }
        if (!(Get-PSSession | Where-Object { $_.Name -match 'ExchangeOnline' -and $_.Availability -eq 'Available' })) {
            Write-Error "Connection failed"
            exit
        }

Process – Exchange Mailbox Rules

Check Mailboxes

Now we are connected to exchange online, it’s time to start grabbing the needed information. Before we start, we need to establish an array for possible mailboxes. We will call these mailboxes to make life easy.

$mailboxes = @()

Now, whether or not the user used the “UserPrincipalName” parameter or not, we are ready. The next step is to test if we used the parameter and grab those mailboxes. We do this by using the $PSBoundParameters variable. Searching the Contains key allows you to see what parameters are being passed to the script. in our case we will be searching for the user principal name. If there is no UPN, we just grab all the mailboxes and dump it into the $mailboxes variable we made.

Now, if we did use the upn, we will loop through the UPN and grab each mailbox accordingly. if there is an issue, we let the user know that that username was an issue. This will give us the mailbox information we need. We do this separately because it causes fewer errors. Best way to describe it is using this method is the bouncer. It bounces issues out the door before we start looking at the inbox rules.

$mailboxes = @()
        if (!($PSBoundParameters.ContainsKey("UserPrincipalName"))) {
            $Mailboxes = Get-mailbox
        }
        else {
            foreach ($Username in $UserPrincipalName) {
                try {
                    $Mailboxes += get-mailbox $username -ErrorAction SilentlyContinue
                }
                catch {
                    Write-Warning "$username has no mailbox"
                }
            }
        }

Exchange Mailbox Rules

The next step is to grab the rules themselves. We start off by making a for each loop. The loop of course is going to drop into a variable. We do this because it has been found that dropping the output of a foreach loop into a variable is a faster method than rebuilding or appending an array.

$RulesReturn = Foreach ($Mail in $Mailboxes) {}

Now, it’s time to hang onto your hats. The rules are tricky as they error out. We see errors because the end user will create a rule, and abandon it. It has nothing to do with your code. It is purely what it is.

The first thing we need to do inside our loop is grab the rules for the mailbox. The command is Get-InboxRule. We will be placing the rules into a variable called Rules. Now here is the catch. This command will produce something. So throw it in a try catch. So if it produces an error, which happens, you can set the Rules to null.

Inbox Rules

try {
    $Rules = Get-InboxRule -Mailbox $Mail.Userprincipalname
} catch {
    $Rules = $null  
}

Next, we test if the Rules are null, if not, we start the loop. Here is another fun part… If the rule is misconfigured, it will present as null from time to time. So we need to test for the null again per rule.

if ($null -ne $Rules) {
    Write-Verbose "$($mail.userprincipalname)"
    foreach ($Rule in $Rules) {
        if ($null -ne $rule) {}
    }
}

Parsing the data

Some rules are based on subject, some rules are based on email. It’s best that we grab useful information. Things like the description, and from are presented in string format with unique structures. However, sometimes those can be null strings and that will explode the results as well. So we have to test them. First we will test the from. if the from is not null, we want to split the from, from the bracket and select the first item. However, if the from is null, we want to give the from a blank string as a psobject doesn’t like null.

if ($null -ne $rule.from) {
    $froms = $Rule.from.split('[')[0]
} else {
    $froms = ""
}

After the From, we need to grab the description. However, I need this description on a single line. As most people are using PowerShell 5, Join-String is not available. Which is sad. So, I built my own join-string. To do this, first create a null return. Then split the Rules description by the enter, `n. Next we do a foreach-object loop, taking that null return value and added itself to itself with a nice trim. Finally, I dropped that information into the description. If the description was null to begin with we drop a blank string into the description variable.

if ($null -ne $rule.description) {
    $dereturn = $Null
    $rule.description.split("`n") | foreach-object { $dereturn = "$dereturn $($_.trim())" }
    $description = $dereturn.trim()
} else {
    $description = ""
}

Bringing it together

Finally, we have all the information we need. It’s time to create the PS Custom Object. Here we will be presenting the username, the ID of the rule, if the rule is enabled, the from, and the description. Since we joined the strings of the description before, this output can be exported to a csv file. Remember at the beginning of the main loop, we are placing all the output of that loop into a variable called Rules Return. Well, this is what will be going into that variable.

[pscustomobject]@{
    Username    = $Mail.Userprincipalname
    Identity    = $Rule.Identity
    Enabled     = $Rule.Enabled
    Name        = $Rule.Name
    from        = $froms
    Description = $description
} 

End

Finally, we reach the end of this script. We are doing two things at the end of this script. First, we are presenting the data. Then we are testing if we need to stay connected. We present the variable Rules Return. Then we check if Stay Connected is true. If it isn’t we disconnect from exchange with a confirmation of false. If you set the flag to stay connected when you executed the code, then this part only shows the rules. No need to disconnect. I always love having this option as exchange online is a pain with multiple MFA layers.

$RulesReturn
if (!($StayConnected)) { Disconnect-ExchangeOnline -Confirm:$false }

Continue Reading:

As always, I like to present some items at the end to encourage you to continue reading.

Remote Wipe a Computer With PowerShell

Remote Wipe a Computer With PowerShell

Did you know you can wipe a computer using code? I didn’t realize this was an option until I needed to do it the other day. Well, I quickly found code to wipe a computer using PowerShell. It was pretty simple as a full Microsoft documentation page is dedicated to the wipe types. You can read about it here. The only problem I ran into was this code needs to be launched as a “System User” instead of a domain admin. This presented a problem if I wanted to use PowerShell for this task. However, psexec could launch scripts as a system user. That was my solution for running a Remote Wipe on a Computer with PowerShell.

The Script

Today we start out with a rough script. This script is designed to give you a rough idea of what I am thinking about and how I was able to do it. You can do additional items to this script like split the here-string and add options. However, most of my items have been from a clean local wipe idea.

function Invoke-RemoteWipeComputer {
    param (
        [parameter(Mandatory = $true)][string[]]$ComputerName
    )
    begin {
        if (!(Test-Path -Path "$env:SystemDrive\Temp")) { New-Item -Path c:\ -Name Temp -ItemType Directory }
        if (!(Test-Path -Path "$env:SystemDrive\Temp\PsExec.exe")) { Invoke-WebRequest -Uri "https://live.sysinternals.com/PsExec.exe" -OutFile "$env:SystemDrive\Temp\PsExec.exe" }
        $WipeScript = @'

            $session = New-CimSession
            $params = New-Object Microsoft.Management.Infrastructure.CimMethodParametersCollection
            $param = [Microsoft.Management.Infrastructure.CimMethodParameter]::Create("param", "", "String", "In")
            $params.Add($param)
            $CimSplat = @{
                Namespace = "root\cimv2\mdm\dmmap"
                ClassName = "MDM_RemoteWipe"
                Filter    = "ParentID='./Vendor/MSFT' and InstanceID='RemoteWipe'"
            }

            try {
                $instance = Get-CimInstance @CimSplat
                $session.InvokeMethod($CimSplat["Namespace"], $instance, "doWipeMethod", $params)
            }
            catch {
                Write-Error $_
                exit 1
            }
'@ 
        $WipeScript > "$env:SystemDrive\Temp\WipeScript.ps1"
    }
    process {
        foreach ($Computer in $ComputerName) {
            if (Test-Connection -ComputerName $Computer -Count 2 -Quiet) {
                Copy-Item "$env:SystemDrive\Temp\WipeScript.ps1" -Destination "\\$Computer\c$\Temp\WipeScript.ps1" -Force
                & "$env:SystemDrive\Temp\PsExec.exe" -s \\$Computer PowerShell -ExecutionPolicy Bypass -File "\\$Computer\c$\Temp\WipeScript.ps1"
            }
        }
    }
    end {}
}

The Breakdown

Let’s break down this script. The first item is always the parameters. In this case, we are making a list of strings with computer names. We will use these later. This script is broken up into a begin and process. Remember, begin, process, end, and final are all simple organization tools. This makes life easier because we are going to need to download the psexec command from sysinternals life site. We also need to build out the main script and have a folder to add it all in.

Begin

The first thing we need to do is test if the c:\temp exists as we are going to download everything into it. This one liner makes life much easier. We test if the path doesn’t exist, then we make it if that is true. The ! mark indicates do the opposite of what is inside the (). Inside the () we are testing for c:\temp or local system drive temp in this case. If that doesn’t exist, we create a new item with a path of the system drive. We create a new item with new item and call it temp making sure it’s a directory flag in the item type.

if (!(Test-Path -Path "$env:SystemDrive\Temp")) { New-Item -Path "$env:SystemDrive\" -Name Temp -ItemType Directory }

Next, we test if the psexec exists and download it accordingly. Once again, we are using the ! test. If the file doesn’t exist, we us invoke-webrequest to reach out to the live sysinternals site and download it to our c:\temp.

if (!(Test-Path -Path "$env:SystemDrive\Temp\PsExec.exe")) { Invoke-WebRequest -Uri "https://live.sysinternals.com/PsExec.exe" -OutFile "$env:SystemDrive\Temp\PsExec.exe" }

Now we have downloaded and made all the required folders, it’s time to write the script. In this case, we are building the script with a here-string. This way, the data is the same no matter what we do. It’s a clone each time and we know what we are getting. Let’s break down the wipe script.

The Wipe Script

As stated before, we are building out the script inside our here-string. This way, it is always the same. I prefer to know what is happening with any script I launch. So, it’s time to break it down.

We start off with a new cim session. This is a blank session, with nothing special about it.

$session = New-CimSession

Next, we need to make some new objects. We need a cim method parameters container. So we do this with a new-object command. Then we add the parameters we need inside this new parameter container. Both of these commands use the Management > infrastructure objects as parents. Finally, we add the parameters to the parameters. yeah that sounds weird, but luckily we just change a s and it makes it much easier to understand.

$params = New-Object Microsoft.Management.Infrastructure.CimMethodParametersCollection
$param = [Microsoft.Management.Infrastructure.CimMethodParameter]::Create("param", "", "String", "In")
$params.Add($param)

Now we have our parameter set for our invoke method, it’s time to create the splat. We need a remote wipe command. Viewing this documentation. We see our remote wipe is located inside the ./Device/Vendor/MSFT/RemoteWipe/. This is part of the MDM cim version 2 instance. So, our namespace will need to reflect that. We have a namespace of root\cimv2\mdm\dmmap. Our class name needs to be the MDM_Remotewipe. Finally, our filter needs to be show the vendor msft and the instanceId, the item we are after, is remoteWipe.

$CimSplat = @{
    Namespace = "root\cimv2\mdm\dmmap"
    ClassName = "MDM_RemoteWipe"
    Filter    = "ParentID='./Vendor/MSFT' and InstanceID='RemoteWipe'"
}

Now we start our try catch. Inside our try, we are call the get-ciminstance with the splat from before. This creates an instances on the computer that is talking directly to the wipe system. Now, we need to use the session we made at the beginning of this here-string. We invoke the method using invoke method. From there, we use the $cimsplat namespace, the instance of the cimsplat, the wipe method, in this case, a local wipe, and the parameters we made at the beginning. The system will trigger a wipe at this point. The catch just exits and sends the error it gets.

Now the here-string is built, we push that string into a PowerShell script on our freshly created temp folder. We use the single > to overwrite anything that is already there. This limits mistakes.

Process

Now it’s time for the process. Here we are going to copy the files and execute it with psexec. We are also going to loop through each computer during this process. The first step is to start a loop with a foreach loop.

foreach ($Computer in $ComputerName) {
#            Do something
}

Up to this point, we have done everything on our computer. We have downloaded the psexec. We have created the required script. The next step starts to affect the end user’s computer. This is where the Remote comes into Remote Wipe a Computer. We are going to use the command test-connection and ping the computer twice.

if (Test-Connection -ComputerName $Computer -Count 2 -Quiet) {
    #It was tested
}

If the computer is present, we attempt to copy the script over. We force the copy to overwrite anything with the same name. Finally, we use the local psexec to trigger powershell as the system. We use the -s for psexec to trigger the file that is on the remote computer that we copied. I want the output on my local screen of the psexec command. This is what we trigger it with the & instead of a start-process. Now, could you use the start process, yes, in fact, it would be quicker since you can set it as a job and put the PC name as the job, but you do lose the output information from psexec. So, if there is a problem, you wouldn’t know.

Copy-Item "$env:SystemDrive\Temp\WipeScript.ps1" -Destination "\\$Computer\c$\Temp\WipeScript.ps1" -Force
& "$env:SystemDrive\Temp\PsExec.exe" -s \\$Computer PowerShell -ExecutionPolicy Bypass -File "\\$Computer\c$\Temp\WipeScript.ps1"

At this point, the computer will take a few seconds, and the reset process would start. After that, everything you will need to do will require a hand on the machine. So have fun and use wisely. Also, the script inside the script can be ran by other products like Continuum, ninja, pdq and more.

Continue Reading

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