PoshCode Archive  Artifact Content

Artifact b64046563a41ca7aa5d135803c840bcf59ce3b936186602f2397bb8607167b66:

  • File chkhash.ps1 — part of check-in [3d1533e17f] at 2018-06-10 13:16:25 on branch trunk — ChkHash.ps1 – ChkHash.ps1 can create a .XML database of files and their SHA-512 hashes and check files against the database, in order to detect corrupt or hacked files. (user: James Gentile size: 14023)

# encoding: utf-8
# api: powershell
# title: chkhash.ps1
# description: ChkHash.ps1 – ChkHash.ps1 can create a .XML database of files and their SHA-512 hashes and check files against the database, in order to detect corrupt or hacked files.
# version: 0.1
# type: function
# author: James Gentile
# license: CC0
# function: Get-SHA512
# x-poshcode-id: 2876
# x-archived: 2014-12-22T02:45:39
# x-published: 2011-07-28T17:31:00
#
# Run with -h option for help and usage. Options include -u to add new and modified files to a database, -e to exclude directories, -c to create a database, -x to specify a .xml file to create/use. Subdirs are automatically processed.
#
# calculate SHA512 of file.

function Get-SHA512([System.IO.FileInfo] $file = $(throw 'Usage: Get-MD5 [System.IO.FileInfo]'))
{
  	$stream = $null;
  	$cryptoServiceProvider = [System.Security.Cryptography.SHA512CryptoServiceProvider];
  	$hashAlgorithm = new-object $cryptoServiceProvider
  	$stream = $file.OpenRead();
  	$hashByteArray = $hashAlgorithm.ComputeHash($stream);
  	$stream.Close();

  	## We have to be sure that we close the file stream if any exceptions are thrown.

  	trap
  	{
   		if ($stream -ne $null)
    		{
			$stream.Close();
		}
  		break;
	}	

 	foreach ($byte in $hashByteArray) { if ($byte -lt 16) {$result += 0{0:X} -f $byte } else { $result += {0:X} -f $byte }}
	return [string]$result;
}

function noequal ( $first, $second)
{
    if (!($second) -or $second -eq "") {return $true}
    $first=join-path $first "\"
    foreach($s in $second)
    {
        if ($first.tolower().startswith($s.tolower())) {return $false}
    }
    return $true
}

# functions to print overwriting multi-line messages.  Test script will accept a file/filespec/dir and iterate through all files in all subdirs printing a test message + file name to demostrate.
# e.g. PS>.\writefilename.ps1 c:\
# call WriteFileName [string]
# after done writing series of overwriting messages, call WriteFileNameEnd

function WriteFileName ( [string]$writestr )                        # this function prints multiline messages on top of each other, good for iterating through filenames without filling
{                                                                   # the console with a huge wall of text.  Call this function to print each of the filename messages, then call WriteFileNameEnd when done
                                                                    # before printing anything else, so that you are not printing into a long file name with extra characters from it visible.
    if ($Host.Name -match 'ise') 
    { write-host $writestr; return }
                                                                            
    if ($global:wfnlastlen -eq $null) {$global:wfnlastlen=0}              
    $ctop=[console]::cursortop
    $cleft=[console]::cursorleft	

	$oldwritestrlen=$writestr.length
    
    $rem=$null
	$writelines = [math]::divrem($writestr.length+$cleft, [console]::bufferwidth, [ref]$rem)
	#if ($rem -ne 0) {$writelines+=1}

    $cwe = ($writelines-(([console]::bufferheight-1)-$ctop))                                       # calculate where text has scroll back to.
    if ($cwe -gt 0) {$ctop-=($cwe)}
    
	write-host "$writestr" -nonewline    
	$global:wfnoldctop=[console]::cursortop
	$global:wfnoldcleft=[console]::cursorleft
	
	if ($global:wfnlastlen -gt $writestr.length)
	{
		write-host (" " * ($global:wfnlastlen-$writestr.length)) -nonewline                    # this only overwrites previously written text if needed, so no need to compute buffer movement on this
	}
	
	
	$global:wfnlastlen = $oldwritestrlen

    if ($ctop -lt 0) {$ctop=$cleft=0}
	[console]::cursortop=$ctop
	[console]::cursorleft=$cleft
}
function WriteFileNameEnd ( $switch=$true)                                                      # call this function when you are done overwriting messages on top of each other
{                                                                                               # and before printing something else.  Default switch=$true, which prints a newline, $false restores cursor position same line.
    if ($Host.Name -match 'ise') 
    { return }
    if ($global:wfnoldctop -ne $null -and $global:wfnoldcleft -ne $null) 
    {
        [console]::cursortop=$global:wfnoldctop
        [console]::cursorleft=$wfnoldcleft  
        if ($global:wfnoldcleft -ne 0 -and $switch)
        {
            write-host ""
        }
    }    
    $global:wfnoldctop=$null
    $global:wfnlastlen=$null
    $global:wfnoldcleft=$null
}



#   chkhash.ps1 [file(s)/dir #1] [file(s)/dir #2] ... [file(s)/dir #3] [-u] [-h [path of .xml database]]
#   -u updates the XML file database and exits
#   otherwise, all files are checked against the XML file database.
#   -h specifies location of xml hash database


$hashespath=".\hashes.xml"
del variable:\args3 -ea 0
del variable:\args2 -ea 0
del variable:\xfiles -ea 0
del variable:\files -ea 0
del variable:\exclude -ea 0
$args3=@()
$args2=@($args)
$nu = 0
$errs = 0
$fc = 0
$fm = 0
$upd = $false
$create = $false
$n = $null

"ChkHash.ps1 - ChkHash.ps1 can create a .XML database of files and their SHA-512 hashes and check files against the database, "
"in order to detect corrupt or hacked files."
""
".\chkhash.ps1 -h for usage."
""

for($i=0;$i -lt $args2.count; $i++)
{
    if ($args2[$i] -like "-h*")                                             # -help specified?
    {
        "Usage:    .\chkhash.ps1 [-h] [-u] [-c] [-x <file path of hashes .xml database>] [file(s)/dir #1] [file(s)/dir #2] ... [file(s)/dir #n] [-e <Dirs>]"
        "Options:  -h - Help display."
        "          -c - Create hash database. If .xml hash database does not exist, -c will be assumed."
        "          -u - Update changed files and add new files to existing database."
        "          -x - specifies .xml database file path to use. Default is .\hashes.xml"
        "          -e - exclude dirs. Put this after the files/dirs you want to check with SHA512 and needs to be fullpath (e.g. c:\users\bob not ..\bob)."
        ""
        "Examples: PS>.\chkhash.ps1 c:\ d:\ -c -x c:\users\bob\hashes\hashes.xml"
        "             [hash all files on c:\ and d:\ and subdirs, create and store hashes in c:\users\bob\hashes\hashes.xml]"
        "          PS>.\chkhash.ps1 c:\users\alice\pictures\sunset.jpg -u -x c:\users\alice\hashes\pictureshashes.xml]"
        "             [hash c:\users\alice\pictures\sunset.jpg and add or update the hash to c:\users\alice\hashes\picturehashes.xml"
        "          PS>.\chkhash.ps1 c:\users\eve\documents d:\media\movies -x c:\users\eve\hashes\private.xml"
        "             [hash all files in c:\users\eve\documents and d:\media\movies, check against hashes stored in c:\users\eve\hashes\private.xml"
        "              or create it and store hashes there if not present]"
        "          PS>.\chkhash.ps1 c:\users\eve -x c:\users\eve\hashes\private.xml -e c:\users\eve\hashes"
        "             [hash all files in c:\users\eve and subdirs, check hashes against c:\users\eve\hashes\private.xml or store if not present, exclude "
        "              c:\users\eve\hashes directory and subdirs]"
        "          PS>.\chkhash.p1s c:\users\ted\documents\f* d:\data -x d:\hashes.xml -e d:\data\test d:\data\favorites -u"
        "             [hash all files starting with 'f' in c:\users\ted\documents, and all files in d:\data, add or update hashes to"
        "              existing d:\hashes.xml, exclude d:\data\test and d:\data\favorites and subdirs]"
        "          PS>.\chkhash -x c:\users\alice\hashes\hashes.xml"
        "             [Load hashes.xml and check hashes of all files contained within.]"
        ""
        "Note:     files in subdirectories of any specified directory are automatically processed."
        "          if you specify only an -x option, or no option and .\hash.xml exists, only files in the database will be checked."
        exit
    }
    if ($args2[$i] -like "-u*") {$upd=$true;continue}                       # Update and Add new files to database?
    if ($args2[$i] -like "-c*") {$create=$true;continue}                    # Create database specified?
    if ($args2[$i] -like "-x*") 
    {
        $i++                                                                # Get hashes xml database path    
        if ($i -ge $args2.count) 
        {
            write-host "-X specified but no file path of .xml database specified. Exiting."
            exit
        }
        $hashespath=$args2[$i]
        continue
    }
    if ($args2[$i] -like "-e*")                                             # Exclude files, dirs
    {
        while (($i+1) -lt $args2.count)
        {
            $i++
            if ($args2[$i] -like "-*") {break}            
            $exclude+=@(join-path $args2[$i] "\")                           # collect array of excluded directories.            
        }
        continue
    }        
    $args3+=@($args2[$i])                                                   # Add files/dirs
}

if ($args3.count -ne 0) 
{
    # Get list of files and SHA512 hash them.    
 
    "Enumerating files from specified locations..."  
    $files=@(dir $args3 -recurse -ea 0 | ?{$_.mode -notmatch "d"} | ?{noequal $_.directoryname $exclude})              # Get list of files, minus directories and minus files in excluded paths

    if ($create -eq $true -or !(test-path $hashespath))                        # Create database?
    {       
        # Create SHA512 hashes of files and write to new database
        if ($files.count -eq 0) {"No files found. Exiting."; exit}
 
        $files = $files | %{WriteFileName "SHA-512 Hashing: `"$($_.fullname)`" ...";add-member -inputobject $_ -name SHA512 -membertype noteproperty -value $(get-SHA512 $_.fullname) -passthru}
        WriteFileNameEnd
        $files |export-clixml $hashespath    
        "Created $hashespath"
        "$($files.count) file hash(es) saved. Exiting."
        exit
    }
    write-host "Loading file hashes from $hashespath..." -nonewline
    $xfiles=@(import-clixml $hashespath|?{noequal $_.directoryname $exclude})  # Load database    
    if (($xfiles.count -eq 0) -or ($files.count -eq 0)) {"No files in Database or no files specified. Exiting.";Exit}
    
}
else
{
    if (!(test-path $hashespath)) {"No database found or specified, exiting."; exit}
    write-host "Loading file hashes from $hashespath..." -nonewline
    $xfiles=@(import-clixml $hashespath|?{noequal $_.directoryname $exclude}) # Load database and check it
    if ($xfiles.count -eq 0) {"No files specified and no files in Database. Exiting.";Exit}
    $files=$xfiles | %{get-item -ea 0 -literalpath $_.fullname}
}

"Loaded $($xfiles.count) file hash(es)."
    
$hash=@{}
for($x=0;$x -lt $xfiles.count; $x++)                                    # Build dictionary (hash) of filenames and indexes into file array
{
    if ($hash.contains($xfiles[$x].fullname)) {continue}
    $hash.Add($xfiles[$x].fullname,$x)   
}
     
foreach($f in $files)
{
    if ((get-item -ea 0 -literalpath $f.fullname) -eq $null) {continue}              # skip if file no longer exists.
    
    $n=($hash.($f.fullname))
    if ($n -eq $null)
    {    
        $nu++                                           # increment needs/needed updating count
        if ($upd -eq $false) {WriteFileName "Needs to be added: `"$($f.fullname)`"";WriteFileNameEnd;continue}                 # if not updating, then  continue
    
        WriteFileName "SHA-512 Hashing `"$($f.fullname)`" ..."
        
        # Create SHA512 hash of file
        
        $f=$f |%{add-member -inputobject $_ -name SHA512 -membertype noteproperty -value $(get-SHA512 $_.fullname) -passthru -force}
        
        $xfiles+=@($f)                                  # then add file + hash to list
        continue
    }      
    
    WriteFileName "SHA-512 Hashing and checking: `"$($f.fullname)`" ..."
    
    
    $fc++                                               # File checked increment.
    
    if (($upd -eq $false) -and ($f.length -ne $xfiles[$n].length))
    {
        $errs++
        WriteFileName "Size mixmatch: `"$($f.fullname)`""; WriteFileNameEnd
        continue
    }
    
    $f=$f |%{add-member -inputobject $_ -name SHA512 -membertype noteproperty -value $(get-SHA512 $_.fullname) -passthru -force}      
                                                        # Check SHA512 and size for mixmatch.
    if ($xfiles[$n].length -eq $f.length)
    {
        if ($xfiles[$n].SHA512 -eq $f.SHA512)
        {
            $fm++;continue                              # if matched, increment file matches and continue loop  
                                        
        }
    } 
    
    $errs++                                             # increment mixmatches
    if ($upd -eq $true) { $xfiles[$n]=$f; WriteFileName "Updated `"$($f.fullname)`""; WriteFileNameEnd;continue}                                                   
    WriteFileName "SHA-512 and/or size mixmatch found: `"$($f.fullname)`""; WriteFileNameEnd
}

WriteFileNameEnd                                        # restore cursor position after last write string

if ($upd -eq $true)                                     # if database updated
{
    $xfiles|export-clixml $hashespath                   # write xml database
    "Updated Database: $hashespath"
    "$nu file hash(es) added to Database."
    "$errs file hash(es) updated in Database."
    exit
}

"$errs SHA-512 or size mixmatch(es) found."
"$fm file(s) SHA512 and size matched." 
"$fc file(s) checked total."
if ($nu -ne 0) {"$nu file(s) need to be added [run with -u option to Add file hashes to database]."}