For obvious reasons I have been a little more concerned with security lately and so have been modifying my trust behavior on my windows machine when running programs or commands. Somewhere I’ve lost in a forum, someone was building a powershell module(that I will embed below) that could be set like a cron job that would get a file hash for every file in a folder you selected, and keep a history of easily checked file changes, and I think I’m going to have to implement something like that, but this morning I stumbled across a post here at eleven forums that is a game changer for Win11 admins. You can natively add powershell file hashing capability to the context menu of file explorer by,,, gasp adding registry keys. I know I hate touching the registry and was suspicious, so rather than copy the file there for download I copied it into a text editor and read it until I was confident it wasn’t malicious. Just run it as a .reg file with regedit and you’re golden. There are some benefits to Redmond’s involvement with Linux these days. Big thanks to Brink at eleven forums for posting the .reg
Written by
Brink
Administrator
Here’s the reg file.
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT*\shell\hash]
“MUIVerb”=”Hash value”
“SubCommands”=””
; SHA1
[HKEY_CLASSES_ROOT*\shell\hash\shell\01menu]
“MUIVerb”=”SHA1”
[HKEY_CLASSES_ROOT*\shell\hash\shell\01menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm SHA1 | format-list”
; SHA256
[HKEY_CLASSES_ROOT*\shell\hash\shell\02menu]
“MUIVerb”=”SHA256”
[HKEY_CLASSES_ROOT*\shell\hash\shell\02menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm SHA256 | format-list”
; SHA384
[HKEY_CLASSES_ROOT*\shell\hash\shell\03menu]
“MUIVerb”=”SHA384”
[HKEY_CLASSES_ROOT*\shell\hash\shell\03menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm SHA384 | format-list”
; SHA512
[HKEY_CLASSES_ROOT*\shell\hash\shell\04menu]
“MUIVerb”=”SHA512”
[HKEY_CLASSES_ROOT*\shell\hash\shell\04menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm SHA512 | format-list”
; MACTripleDES
[HKEY_CLASSES_ROOT*\shell\hash\shell\05menu]
“MUIVerb”=”MACTripleDES”
[HKEY_CLASSES_ROOT*\shell\hash\shell\05menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm MACTripleDES | format-list”
; MD5
[HKEY_CLASSES_ROOT*\shell\hash\shell\06menu]
“MUIVerb”=”MD5”
[HKEY_CLASSES_ROOT*\shell\hash\shell\06menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm MD5 | format-list”
; RIPEMD160
[HKEY_CLASSES_ROOT*\shell\hash\shell\07menu]
“MUIVerb”=”RIPEMD160”
[HKEY_CLASSES_ROOT*\shell\hash\shell\07menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm RIPEMD160 | format-list”
; Allget-filehash -literalpath ‘%1’ -algorithm RIPEMD160 | format-list
[HKEY_CLASSES_ROOT*\shell\hash\shell\08menu]
“CommandFlags”=dword:00000020
“MUIVerb”=”Show all”
[HKEY_CLASSES_ROOT*\shell\hash\shell\08menu\command]
@=”powershell -noexit get-filehash -literalpath \\”%1\\” -algorithm SHA1 | format-list;get-filehash -literalpath \\”%1\\” -algorithm SHA256 | format-list;get-filehash -literalpath \\”%1\\” -algorithm SHA384 | format-list;get-filehash -literalpath \\”%1\\” -algorithm SHA512 | format-list;get-filehash -literalpath \\”%1\\” -algorithm MACTripleDES | format-list;get-filehash -literalpath \\”%1\\” -algorithm MD5 | format-list;get-filehash -literalpath \\”%1\\” -algorithm RIPEMD160 | format-list”
And the powershell script that I found earlier
function Out-HashFiles
{
<#
.SYNOPSIS
Calculates hashes and saves as file.
.DESCRIPTION
This script recursively searches the ScanPath for files larger than the specified size. Then creates a .md5 or .sha1 file in the same directory and with the same name as the source file. The hash of the source file is stored in inside this .md5/.sha1 file.
.PARAMETER $ScanPath
The path that will be recursively searched for files.
.PARAMETER $Algorithm
Specify whether to use MD5 or SHA1 algorithm to hash. Default is SHA1
No speed or CPU load difference when I tested.
.PARAMETER $LargerThan
Hash files larger than specified size in bytes.
1000 = 1 Thousand = 1KB
1000000 = 1 Million = 1MB
1000000000 = 1 BIllion = 1GB
.PARAMETER $Depth
How deep to recursively search for files
.NOTES
Author: Michael Yamry
.EXAMPLE
PS C:\> Out-HashFiles -ScanPath "C:\test\" -LargerThanFileSize 0
Hash all files in test folder using the default algorithm of SHA1 and recursive depth of 5.
.EXAMPLE
Out-HashFiles -ScanPath "C:\test\" -Algorithm SHA1 -LargerThan 100MB -Depth 5 -Verbose
Hash files larger than 100MB using SHA1 algorithm. Only recursively searches 5 levels deep.
.INPUTS
None
.OUTPUTS
None
#>
[CmdletBinding()]
param(
[Parameter(Position=0, Mandatory=$false)]
[ValidateScript({Test-Path -LiteralPath $_ -PathType Container})]
[System.String]$ScanPath = "\\skynet.local\DFS\Video",
[Parameter(Position=1, Mandatory=$false)]
[ValidateSet("SHA1", "MD5", IgnoreCase = $true)]
[System.String]$Algorithm="SHA1",
[Parameter(Position=2, Mandatory=$false)]
[ValidateNotNull()]
[System.Int32]$LargerThan=100MB,
[Parameter(Position=3, Mandatory=$false)]
[ValidateNotNull()]
[System.Int32]$Depth=5
)
BEGIN
{
Write-Host -Object "`r`n`r`n`r`n`r`n" #Four character returns so progress bar does not cover output
}
PROCESS
{
#Find large files in scanpath
Write-Verbose "Scanning path: $ScanPath"
$LargeFiles = Get-ChildItem -Path $ScanPath -Recurse -Depth $Depth -Exclude "*.md5", "*.sha1", "*.json", "*.nfo", "*.srt", "*.sub" | Select-Object FullName, Name, Length | Where-Object {$_.length -GT $LargerThan}
#Count total number of files it will scan
$FoundFileCount = ($LargeFiles | Measure-Object).Count
Write-Verbose "INFO: $FoundFileCount files larger than $([math]::Round($LargerThan/1MB)) MB found"
#Loop through each file and build custom object with info such as name, length, and if has file exists already
Write-Verbose "Parsing $FoundFileCount files and checking if hash file already exists."
#$i = 0
$ParsedFiles = foreach ($File in $LargeFiles)
{
#Progress bar, when writing progress it takes 4 minutes, when not writing progress it takes 20 seconds
#$i++
#Write-Progress -Activity "Analyzing Files" -status "Working on $i/$FoundFileCount - $($File.Name)" -percentComplete ($i / $FoundFileCount*100)
#Build object
[PSCustomObject]@{
Name = $File.Name
FullName = $File.FullName
HashFilePath = ($File.FullName + ".json")
Length = $File.Length
HashFileExists = $(Test-Path -LiteralPath $($File.FullName + ".json")) #True if hash file exists
}
}
#Select only files not hashed from object
$FilesNotYetHashed = $ParsedFiles | Where-Object {$_.HashFileExists -eq $false}
#Calculate number of files not yet hashed
$NumberOfFilesNotYetHashed = $FilesNotYetHashed | Measure-Object | Select-Object -ExpandProperty Count
Write-Verbose "INFO: $($FoundFileCount - $NumberOfFilesNotYetHashed) files found already hashed"
Write-Verbose "Hashing $NumberOfFilesNotYetHashed files."
#Calculate size of files not hashed
$BytesToHash = $FilesNotYetHashed | Measure-Object -Property Length -Sum | Select-Object -ExpandProperty Sum
Write-Verbose "INFO: $([math]::Round($BytesToHash / 1GB)) GB to hash."
#Cycle through each file not yet hashed and calculate it's hash and store info in file
$BytesHashed = 0
$i = 0
foreach ($Obj in $FilesNotYetHashed)
{
#Progress bar
$i++
Write-Progress -Activity "Hashing files" -status "$([math]::Round($BytesHashed / 1GB)) of $([math]::Round($BytesToHash / 1GB))GB - Working on $i/$NumberOfFilesNotYetHashed - $($Obj.Name)" -percentComplete ($BytesHashed / $BytesToHash*100)
#Compute hash
$FileHash = (Get-FileHash -LiteralPath $Obj.FullName -Algorithm $Algorithm).Hash
#Build output file contents and output to file
$HashTable = @{
$Algorithm = [string]$FileHash
LastScanPassed = $null
LastScanDate = (Get-Date -Format 'yyyy-MM-dd_HH:mm:ss') #set current date so new files don't get scanned checked again soon.
}
$HashTable | ConvertTo-Json | Out-File -LiteralPath $Obj.HashFilePath #force allows overwrite existing read only files #### failes when path has square brackets unless you use literalpath
Write-Output "Stored '$FileHash' $Algorithm hash in '$($Obj.HashFilePath)'"
#Increment number of bytes that have been hashed for progress bar
$BytesHashed = $BytesHashed + $Obj.Length
}
#>
}
END
{
Write-Verbose "Completed hashing $([math]::Round($BytesHashed / 1GB)) GB in $i files"
}
}
Later verify your previously hashed files with:
function Test-HashFiles
{
<#
.SYNOPSIS
Verify file hashes in directory.
.DESCRIPTION
This script searches the ScanPath for .MD5 or .SHA1 hash files. It makes sure the companion file exists. It then hashes the companion file and checks it against the previously stored hash; unless -Skiphash is used
Failed files will be checked each time.
.PARAMETER $ScanPath
The path that will be recursivly searched for files.
.PARAMETER $Algorithm
Specify whether to use MD5 or SHA1 algorithm to hash. Default is SHA1
.NOTES
Author: Michael Yamry
.EXAMPLE
$Results = Test-HashFiles -ScanPath "C:\test\" -SkipCheckDays -1 -Verbose
Verifyes hashes by recursivly searching for SHA1 files in the specified folder. -1 means it will force recheck all even if checked recently.
.EXAMPLE
$Orphaned = (Get-ChildItem -LiteralPath "\\test.local\DFS\Video\" -Recurse -Filter *.json).FullName | foreach {if (-not (Test-Path -LiteralPath $($_ -replace ".json")) ) {$_} }
$Orphaned | Remove-Item
Delete all orphaned hash files
.EXAMPLE
$Currupt = (Get-ChildItem -LiteralPath "\\test.local\DFS\Video" -Recurse -Filter *.json).FullName | foreach { if ( (Get-Content -LiteralPath $_ | ConvertFrom-Json).LastScanPassed -eq $false ) {$_ -replace ".json"} }
Find files that were marked as currupt
#>
[CmdletBinding()]
param (
[Parameter(Position=0, Mandatory=$true)]
[ValidateScript({Test-Path -LiteralPath $_ -PathType Container})]
[System.String]
$ScanPath = "\\test.local\DFS\Video",
#Skip scanning files that were scanned recently
[int]$SkipCheckDays = 60
)
Write-Verbose "Finding all '*.json' has files in '$ScanPath'."
$FoundFiles = Get-ChildItem -Path $ScanPath -Recurse -Filter *.json | Select-Object FullName, Name
#Count total number of files it will parse
$FileCount = ($FoundFiles | Measure-Object).Count
Write-Verbose "Found $FileCount .json files. Parsing through each getting source file properties and JSON contents."
#Loop through each file and build custom object with info such as name, length, and if has file exists already, etc
$ParsedFiles = foreach ($File in $FoundFiles)
{
$JSONContents = Get-Content -LiteralPath $File.FullName | ConvertFrom-Json
<# Example
{
"LastScanPassed": null,
"LastSCanDate": null,
"SHA1": "4C3883069DC34910ADB47DC41D2A837C93C40305"
} #>
#Retrive stored hash from file
if ($JSONContents.SHA1)
{
$Algorithm = "SHA1"
$StoredHash = $JSONContents.SHA1
} else {
if ($JSONContents.MD5)
{
$Algorithm = "MD5"
$StoredHash = $JSONContents.MD5
}
else
{
Write-Warning "Could not find SHA1 or MD5 properties in JSON file: $($HashFile.FullName)"
}
}
#find out if hash should be computed
if (-not $JSONContents.LastScanDate) #is the date emtpy
{
[bool]$ComputeHash = $true
}
else
{
$StoredDate = [datetime]::ParseExact($JSONContents.LastScanDate,'yyyy-MM-dd_HH:mm:ss',$null) #Convert string back to date
$TimeDifference = New-TimeSpan -Start $storedDate -End (Get-Date)
# has it been scanned recently?
if ( $TimeDifference.Days -gt $SkipCheckDays )
{
[bool]$ComputeHash = $true
} else {
[bool]$ComputeHash = $false
#Write-Verbose "[Skipped] '$($OutputObject.SourceFilePath)' File already checked $($TimeDifference.Days) day(s) ago."
}
}
#find out if source file exists and get size
try
{
$SourceFileLength = (Get-Item -LiteralPath $($File.FullName -replace ".json") -ErrorAction Stop).Length
$Exists = $true
}
catch
{
$Exists = $false
}
#make and output object
[PSCustomObject]@{
Name = $File.Name
FullName = $File.FullName
SourceFileName = ($File.Name -replace ".json")
SourceFileFullName = ($File.FullName -replace ".json")
SourceFileExists = $Exists
SourceFileLength = $SourceFileLength
LastScanPassed = $JSONContents.LastScanPassed
LastScanDate = $JSONContents.LastScanDate
Algorithm = $Algorithm
StoredHash = $StoredHash
ComputeHash = $ComputeHash
}
}
#Warn about currupt files
$ParsedFiles | Where-Object {$_.LastScanPassed -eq $false} | foreach { Write-Warning "File found currupt on last scan, will be rechecked: $($_.SourceFileFullName)"; Start-Sleep -Milliseconds 100}
#Warn about orphaned files
$ParsedFiles | Where-Object {$_.SourceFileExists -eq $false} | foreach { Write-Warning "Orphaned JSON file will be skipped: $($_.FullName)"}
#Show number of skipped files scanned recently
$SkippedCount = ($ParsedFiles | Where-Object {$_.ComputeHash -eq $false} | Measure-Object).Count
Write-Verbose "Skipping $SkippedCount files that were scanned in the last $SkipCheckDays day(s)."
#Select files that will be checked this run
$FilesToRun = $ParsedFiles | Where-Object {($_.ComputeHash -eq $true) -and ($_.SourceFileExists -eq $true)} #not filtering files that failed last. failed files will be checked each time.
Remove-Variable ParsedFiles
#Calculate quantity and size of this run
$BytesToHash = $FilesToRun | Measure-Object -Property SourceFileLength -Sum | Select-Object -ExpandProperty Sum
$QuantityFilesToRun = $FilesToRun | Measure-Object | Select-Object -ExpandProperty Count
Write-Verbose "$QuantityFilesToRun files to recompute totaling $([math]::Round($BytesToHash / 1GB)) GB."
$BytesHashed = 0
$i = 0
foreach ($CurrentFile in $FilesToRun)
{
#Progress bar
$i++
Write-Progress -Activity "Recomputing files" -Status "$([math]::Round($BytesHashed / 1GB)) of $([math]::Round($BytesToHash / 1GB))GB - Working on $i/$QuantityFilesToRun - $($CurrentFile.SourceFileName)" -PercentComplete ($BytesHashed / $BytesToHash*100)
#Compute hash of curent file
$TestedFileHash = Get-FileHash -LiteralPath $CurrentFile.SourceFileFullName -Algorithm $CurrentFile.Algorithm | Select-Object -ExpandProperty Hash
#Build object to be put back into JSON file
$Contents = @{}
$Contents.LastScanDate = (Get-Date -Format 'yyyy-MM-dd_HH:mm:ss')
#Compare hashes
if ($TestedFileHash -eq $CurrentFile.StoredHash)
{
Write-Verbose "[Verified] $($CurrentFile.SourceFileFullName)"
$Contents.LastScanPassed = $true
}
else
{
Write-Warning "Stored hash '$StoredHash' does not match current hash '$TestedFileHash' for: $($OutputObject.SourceFilePath)"
$Contents.LastScanPassed = $false
}
#Increment number of bytes that have been hashed for progress bar
$BytesHashed = $BytesHashed + $CurrentFile.SourceFileLength
#Output new JSON file
$Contents.($CurrentFile.Algorithm) = $CurrentFile.StoredHash
$JSONContents = New-Object –TypeName PSObject -Property $Contents
$JSONContents | ConvertTo-Json | Out-File -LiteralPath $CurrentFile.FullName -Force
Remove-Variable -Name JSONContents, Contents
}
Write-Verbose "Completed all operations."
}
I’d recommend using the above functions in a script such as the one below. Set it to run weekly or something using a scheduled task.
Write-Verbose “Running script: $PSCommandPath” -Verbose
Out-HashFiles -ScanPath “\test.local\DFS\Video” -Algorithm SHA1 -LargerThan 100MB -Depth 5 -Verbose
Test-HashFiles -ScanPath “\test.local\DFS\Video” -SkipCheckDays 60 -Verbose
Write-Verbose “End of script: $PSCommandPath” -Verbose
Start-Sleep -Seconds 20