Get Vendor from MAC Address

Here’s a quick snippet to get the IP, MAC address and Vendor using PowerShell in Windows with the native arp -a command and curl. It is based on accessing my MAC lookup tool.

$arpOutput = arp -a

foreach ($line in $arpOutput) {
if ($line -match '(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s+([a-fA-F0-9:-]{17})') {
$ip = $matches[1]
$mac = $matches[2]
$vendor = curl https://techish.net/mac/$mac
Write-Output "IP: $ip, MAC: $mac, Vendor: $vendor"
}
}

Note: You could do a quick ping sweep of the network you’re on so that the arp cache is fresh:

for /l %x in (1,1,254) do @ping -n 1 -w 25 192.168.0.%x | find /I "bytes="

Note: I have setup a script you can use with PowerShell to do a scan and output the data in comma delimited format.

irm https://techish.net/mac/scan | iex

You can save to a CSV using something like this

irm https://techish.net/mac/scan | iex | out-file scan.csv -encoding UTF8

You could also output to clipboard

irm https://techish.net/mac/scan | iex | clip

Tail a file in Windows

This PowerShell one-liner is a convenient way to monitor log files in real-time and quickly spot error messages. The script continuously reads the log and highlights lines that contain the word “error” with a red background, making them easy to identify. It’s a handy tool for troubleshooting or monitoring system activities, especially when dealing with logs generated by tools like DISM.

gc .\dism.log -wait |foreach-object { if ($_ -match "error"){write-host -foregroundcolor white -BackgroundColor red $_} else {write-host $_}}

Explanation:

  1. gc .\dism.log -wait
    • gc is short for Get-Content, a PowerShell cmdlet that reads the content of a file.
    • .\dism.log specifies the file to read, which is dism.log. This log file is typically generated by the Deployment Imaging Service and Management Tool (DISM), often used for Windows image management.
    • The -wait parameter makes Get-Content continuously monitor the log file in real-time, displaying new content as it is written to the file. This is especially useful for live monitoring of logs.
  2. | foreach-object
    • The | symbol (pipeline) sends the output of the Get-Content cmdlet to the next part of the command.
    • foreach-object is a loop that processes each line of the log file one by one as it is being read.
  3. if ($_ -match "error")
    • $_ represents the current line of the log file being processed in the loop.
    • -match "error" checks if the current line contains the word “error” (case-insensitive by default in PowerShell). This is the key part that identifies lines with errors in the log.
  4. write-host -foregroundcolor white -BackgroundColor red $_
    • If the line contains the word “error,” this part of the command prints it to the console with a white foreground (text color) and a red background for visibility, signaling an error.
    • $_ again represents the line being processed.
  5. else { write-host $_ }
    • If the line doesn’t contain “error,” it simply prints the line normally, without any special formatting.

Example:

Suppose you are monitoring the dism.log file and a line like this is written to the log:

2024-10-11 14:55:12, Error DISM DISM.EXE: Failed to load the provider

In the console, this line will be printed in white text with a red background, making it easy to spot among other log entries.

Recover media files from iMazing backup

Awhile back I used a piece of software (iMazing) to backup an old iPhone of mine. The raw backup when extracted is a bunch of files without extensions in many subdirs. I needed a way to extract actual images from the backup folder and copy them to an extracted folder giving them the correct file extension and ensuring I kept the date created / modified in tact.

Here’s a PowerShell script that will recursively go through a specified folder and copy any files that are images or videos to an extracted folder location.

# Define the magic numbers for various image and video types
$fileTypes = @{
jpg = @(0xFF, 0xD8, 0xFF)
png = @(0x89, 0x50, 0x4E, 0x47)
gif = @(0x47, 0x49, 0x46, 0x38)
bmp = @(0x42, 0x4D)
mp4 = @(0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D)
avi = @(0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x41, 0x56, 0x49, 0x20)
mkv = @(0x1A, 0x45, 0xDF, 0xA3)
mov = @(0x00, 0x00, 0x00, 0x14, 0x66, 0x74, 0x79, 0x70, 0x71, 0x74, 0x20, 0x20)
wmv = @(0x30, 0x26, 0xB2, 0x75, 0x8E, 0x66, 0xCF, 0x11)
}

# Function to get the file header as a byte array
function Get-FileHeader {
param (
[string]$filePath,
[int]$headerLength = 12
)

$fileBytes = [System.IO.File]::ReadAllBytes($filePath)
return $fileBytes[0..($headerLength - 1)]
}

# Function to match the file header with known file types
function Get-FileType {
param (
[byte[]]$fileHeader
)

foreach ($fileType in $fileTypes.Keys) {
$magicNumbers = $fileTypes[$fileType]
$match = $true

for ($i = 0; $i -lt $magicNumbers.Length; $i++) {
if ($fileHeader[$i] -ne $magicNumbers[$i] -and $magicNumbers[$i] -ne 0x00) {
$match = $false
break
}
}

if ($match) {
return $fileType
}
}

return $null
}

# Source and destination directories
$sourceDirectory = "C:\iPhoneBackup"
$destinationDirectory = "D:\Recovery"

# Ensure the destination directory exists
if (-not (Test-Path -Path $destinationDirectory)) {
New-Item -ItemType Directory -Path $destinationDirectory | Out-Null
}

# Get all files in the source directory recursively
$files = Get-ChildItem -Path $sourceDirectory -Recurse -File

foreach ($file in $files) {
$header = Get-FileHeader -filePath $file.FullName
$fileType = Get-FileType -fileHeader $header

if ($fileType) {
# Create the relative path for the destination file
$relativePath = $file.FullName.Substring($sourceDirectory.Length)
$newFileName = [System.IO.Path]::ChangeExtension($relativePath, ".$fileType")
$destinationPath = Join-Path -Path $destinationDirectory -ChildPath $newFileName

# Ensure the destination subdirectory exists
$destinationSubDir = [System.IO.Path]::GetDirectoryName($destinationPath)
if (-not (Test-Path -Path $destinationSubDir)) {
New-Item -ItemType Directory -Path $destinationSubDir | Out-Null
}

# Copy the file to the destination with the new extension
Copy-Item -Path $file.FullName -Destination $destinationPath

# Preserve the original file timestamps
$originalCreationTime = Get-ItemProperty -Path $file.FullName -Name CreationTime
$originalLastWriteTime = Get-ItemProperty -Path $file.FullName -Name LastWriteTime
$originalLastAccessTime = Get-ItemProperty -Path $file.FullName -Name LastAccessTime

Set-ItemProperty -Path $destinationPath -Name CreationTime -Value $originalCreationTime.CreationTime
Set-ItemProperty -Path $destinationPath -Name LastWriteTime -Value $originalLastWriteTime.LastWriteTime
Set-ItemProperty -Path $destinationPath -Name LastAccessTime -Value $originalLastAccessTime.LastAccessTime

Write-Output "Copied $($file.FullName) to $destinationPath"
} else {
Write-Output "$($file.FullName) is not a recognized image or video file"
}
}

Using Powershell to get installed Edge and Chrome extensions

I want to start off with acknowledging that I know, but don’t have the ability, to manage Google Chrome in an organizational setting through Google Cloud which produces nice reports of extensions installed — and this can even be done with M365 Defender. My dilemma is that for some networks I do not have the ability to do this, so I needed an alternative way. This is what I’m developing.

I found an article on Spiceworks that someone created a PowerShell script already while I was trying to find the original article I got the inspiration from. I learned a little about how Chrome extensions use manifest.json and that “name” key doesn’t always, as of Manifest V2, give the actual extension name.

Reading through Chrome.18n documentation on how the manifest is laid out and internationalization structure, I was able to figure out a way to programmatically look things up.

In manifest.json the following keys are required:

default_locale is required. It can be any value, really, but standards are en_US, en, ru, etc.

name is required. It can be the actual name of the extension, but in Manifest V3, it is the string to translate which is preceded by __MSG_ and ends with __ (two underscores). The value between could be anything, for example __MSG_name__ or __MSG_AppName__.

So my thought is to look up the name key, check for __MSG_ and also the default_locale if the name key matches. This then lets me find messages.json which I can use the __MSG_name__ to find the actual extension name.

Disclaimer: This is my script so far; my PowerShell sucks. But it works for me. I do know I need to handle and log errors (unavailable computers, etc.) and not rely on -ErrorAction SilentlyContinue.

October 2024: I just realized this does not capture Developer mode unpacked/packed extensions that may be installed.

# Ensure necessary modules are loaded
Import-Module ActiveDirectory

# Define an array to store the extensions
$extensions = @()

# Define a hash table of browsers/paths
$browsers = @{
    "Edge" = "AppData\Local\Microsoft\Edge\User Data\Default\Extensions"
    "Chrome" = "AppData\Local\Google\Chrome\User Data\Default\Extensions"
}

Clear-Host

# Get a list of AD computers (handling domain and local environments)
try {
    $computers = Get-ADComputer -Filter * -Credential (Get-Credential)
} catch {
    Write-Host "Could not retrieve AD computers. Running on local machine."
    $computers = @([pscustomobject]@{ Name = $env:COMPUTERNAME })
}

foreach ($computer in $computers) {
    $computerName = $computer.Name
    $computerPath = "\\$computerName\c$\Users"

    Write-Host "******************** $computerName **********************"

    # Get a list of users excluding 'Public' and 'Default'
    try {
        $users = Get-ChildItem -Path $computerPath -ErrorAction SilentlyContinue |
                 Where-Object { $_.PSIsContainer -and $_.Name -notmatch '^(Public|Default|All Users|Default User|defaultuser0|WDAGUtilityAccount)$' }
    } catch {
        Write-Host "Failed to access users on $computerName. Skipping."
        continue
    }

    foreach ($user in $users) {
        $userName = $user.Name
        Write-Host "Checking $userName : $computerName`n"

        foreach ($browser in $browsers.Keys) {
            $browserPath = $browsers[$browser]
            $extensionPath = Join-Path -Path $computerPath -ChildPath "$userName\$browserPath"

            # Check if the Extensions folder exists
            if (Test-Path -Path $extensionPath -ErrorAction SilentlyContinue) {
                $manifestFiles = Get-ChildItem -Path $extensionPath -Filter manifest.json -Recurse -ErrorAction SilentlyContinue

                foreach ($manifestFile in $manifestFiles) {
                    $extDir = $manifestFile.DirectoryName
                    $extID = $manifestFile.Directory.Parent.Name
                    $manifestData = Get-Content -Path $manifestFile.FullName | ConvertFrom-Json

                    $extensionName = if ($manifestData.name -like "*__MSG_*") {
                        $msgJSON = Join-Path -Path $extDir -ChildPath "_locales\$($manifestData.default_locale)\messages.json"
                        $extNameKey = $manifestData.name -replace "__MSG_", "" -replace "__", ""
                        (Get-Content -Encoding UTF8 -Path $msgJSON | ConvertFrom-Json).$extNameKey.message
                    } else {
                        $manifestData.name
                    }

                    $extensions += [PSCustomObject]@{
                        ComputerName  = $computerName
                        UserName      = $userName
                        ExtensionName = $extensionName
                        ExtensionID   = $extID
                        Browser       = $browser
                    }
                    Write-Host "[$browser]: $extensionName ($extID)"
                }
                Write-Host "`n"
            }
        }
    }
}

# Export the extensions to a CSV file
$extensions | Export-Csv -Path "browser_extension_report.csv" -Encoding UTF8 -NoTypeInformation

Once it is done, a report should be created as a CSV. Here’s an example. It lists each extension installed for every user per machine scanned.

Managing Extensions in Google Chrome

What I’m working on now is a way to uninstall these after I review installed extensions and determine which ones I want to remove and block from re-installation. This is still a work in progress, here are some of my notes.

This is for removing an app, not an extension. See: Extension and App Types

"C:\Program Files\Google\Chrome\Application\chrome.exe" --profile-directory=Default --uninstall-app-id=EXTENSION_ID;

I’m also seeing some references to registry locations.

HKEY_USERS\Group Policy Objects\Machine\Software\Policies\Google\Chrome\ExtensionInstallForcelist
HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist

Looks like I need to add the extension to the ExtensionInstallForceList and then remove it.

Add ForceInstall

New-ItemProperty -Path "HKLM\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist " -Name "1" -Value EXTENSION_ID

Remove ForceInstall

Note: Need to specify the same value that was added in the Add step. This does not actually remove the extension if it is installed.

Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallForcelist" -Name "EXTENSION_NAME"

Remove (uninstall) the Extension

Could it be as simple as deleting the actual extension folder? sigh

Remove-Item -Path "C:\users\username\appdata\local\google\chrome\user data\default\extension\EXTENSION_ID" -Recurse

Block Extension

Use * for the EXTENSION_NAME and value of 1 to block all extension installations.

Note: -Name is an integer value being placed in the registry. To add another blocked extension, make sure to increment to the another value.

New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallBlocklist" -Name "1" -Value EXTENSION_ID;

Allow Extension

New-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionInstallAllowlist" -Name "1" -Value EXTENSION_ID;

I’ll end up doing this by Group Policy instead of per-computer in Active Directory environments. I’ll block all, and permit only approved extensions.

Group Policy (Google Policy Templates): Computer Configuration > Administrative Templates > Class Administrative Templates > Google > Chrome > Extensions

Analyze RDP Disconnection Logs using PowerShell

The PowerShell script is designed to extract information about Remote Desktop Protocol (RDP) local session manager events from the Windows event logs on a RDS host and save it to a CSV file.

Script: Get events with EventID 40 from Microsoft-Windows-TerminalServices-LocalSessionManager/Operational Event Log

$RDPAuths = Get-WinEvent -LogName 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'-FilterXPath '<QueryList><Query Id="0"><Select>*[System[EventID=40]]</Select></Query></QueryList>'

[xml[]]$xml = $RDPAuths | ForEach-Object { $_.ToXml() }

$EventData = $xml.Event | ForEach-Object {
    [PSCustomObject]@{
        TimeCreated = (Get-Date $_.System.TimeCreated.SystemTime -Format 'yyyy-MM-dd hh:mm:ss K')
        Session     = $_.UserData.EventXML.Session
        Reason      = $_.UserData.EventXML.Reason
        EventID     = $_.System.EventID
        User        = $_.UserData.EventXML.User
        SessionID   = $_.UserData.EventXML.SessionID
        Address     = $_.UserData.EventXML.Address
    }
}

$EventData | Export-Csv -Path c:\rdlog-LSM-Operational.csv -Encoding ASCII

Here is an explanation of how the code works.

  1. First, the script uses the Get-WinEvent cmdlet to retrieve events from the ‘Microsoft-Windows-TerminalServices-LocalSessionManager/Operational’ log. This log contains information about RDP disconnections. The events are stored in the $RDPAuths variable.
  2. The $RDPAuths variable is then piped to the ForEach-Object cmdlet to convert each event to XML format using the ToXml() method. The resulting array of XML objects is stored in the $xml variable.
  3. Next, the script uses the ForEach-Object cmdlet again to iterate over the $xml.Event array. For each event, a new custom object is created using the [PSCustomObject]@{} syntax. This custom object contains the following properties: TimeCreatedSessionReasonEventIDUserSessionID, and Address. These properties are extracted from the event XML data using dot notation.
  4. Finally, the custom objects are piped to the Export-Csv cmdlet, which saves the objects as a CSV file at the specified path (c:\rdlog-LSM-Operational.csv) with ASCII encoding.

This PowerShell script is useful for extracting and analyzing RDP local session manager events, such as monitoring for disconnect reasons. The resulting CSV file provides a simple, convenient way to view and analyze the RDP events.

RDS Session Host Server Disconnect Codes

RDS server client disconnect codeDisconnect reason
0x00000001The disconnection was initiated by an administrative tool on the server in another session.
0x00000002The disconnection was due to a forced logoff initiated by an administrative tool on the server in another session.
0x00000003The idle session limit timer on the server has elapsed.
0x00000004The active session limit timer on the server has elapsed.
0x00000005Another user connected to the server, forcing the disconnection of the current connection.
0x00000006The server ran out of available memory resources.
0x00000007The server denied the connection.
0x00000009The user cannot connect to the server due to insufficient access privileges.
0x0000000A (10)The server does not accept saved user credentials and requires that the user enter their credentials for each connection.
0x0000000B (11)The disconnection was initiated by the user disconnecting his or her session on the server or by an administrative tool on the server.
0x0000000C (12)The disconnection was initiated by the user logging off his or her session on the server.

Extended Disconnect Reason Codes

Reference: https://learn.microsoft.com/en-us/windows/win32/termserv/extendeddisconnectreasoncode

Extended ReasonCode
NoInfo0
APIInitiatedDisconnect1
APIInitiatedLogoff2
ServerIdleTimeout3
ServerLogonTimeout4
ReplacedByOtherConnection5
OutOfMemory6
ServerDeniedConnection7
ServerDeniedConnectionFips8
ServerInsufficientPrivileges9
ServerFreshCredsRequired10
RpcInitiatedDisconnectByUser11
LogoffByUser2
LicenseInternal256
LicenseNoLicenseServer257
LicenseNoLicense258
LicenseErrClientMsg259
LicenseHwidDoesntMatchLicense260
LicenseErrClientLicense261
LicenseCantFinishProtocol262
LicenseClientEndedProtocol263
LicenseErrClientEncryption264
LicenseCantUpgradeLicense265
LicenseNoRemoteConnections266
LicenseCreatingLicStoreAccDenied267
RdpEncInvalidCredentials768
ProtocolRangeStart4096
ProtocolRangeEnd32767