Set Up and Download Scripts and Helpers

Mark Berry December 10, 2015

Scripting is great, but if you manage a bunch of computers, how do you get those scripts and their helper programs onto the computers in the first place? I wrote a script for that!

This PowerShell script (SetUpScripts.ps1) is more than just a script to download files. It fulfills four functions:

  1. Set up a Scripts folder on the computer’s system drive, with subfolders for Batch scripts, Helpers, PowerShell scripts, and PowerShell callers (scripts that call the main PowerShell scripts).
  2. Create a system-level environment variable pointing to the new Scripts folder. You can use this later when you need to run a script. Note that after running this script the first time, the machine must be rebooted before this environment variable will be available.
  3. Add the PowerShell scripts folder to the machine-level Path. This makes it easier to run PowerShell scripts manually. Again, reboot required.
  4. Download scripts and helpers from the specified folders of a public web site that you control. Optionally skip downloading if the file hasn’t changed.

So, yeah, this is deploying an entire scripting environment to your managed machines. You can think of it as a “bootstrap” script—deploy just this script from your RMM tool and it will download all other scripts directly to each local computer.

Modify this script whenever you add new scripts.

Set this script to run daily from your RMM tool and it will update each machine with any new or changed scripts.

Include this script in the scripts that you download, and it will update itself. This means if you run it manually, twice, on a local machine, that machine will be up to date. (The first run updates this script; the second run updates any new scripts.)

Deployment Steps

As always, follow these instructions and run this script at your own risk! Contact MCB Systems if you’d like help setting all this up.

1. Create a “secret” area on a web site that you control, e.g. by creating a subfolder with a random name like http://www.yourdomain.com/rJlN6QLyahRnujU3/

Assuming that path will remain static, modify this script to use that as the default for the BaseWebPath parameter. That way, you don’t have to supply the BaseWebPath when you run the script. Change the default here:

SetUpScripts 4

2. Create this directory structure under that path on your web server:

SetUpScripts 1

Note that “callers” is a subdirectory of “powershell”.

3. Upload your scripts and helpers to the respective directories on your web server. Upload this script (SetUpScripts.ps1) to the powershell folder.

For larger files (usually for helpers, which are executables), also create and upload a file containing the MD5 hash of the file. This allows the script to check whether the file has changed so it doesn’t need to re-download it every time. For example, if you deploy NiniteOne.exe to your Helpers directory, also upload NiniteOne.exe.md5 containing the MD5 hash as a text string:

SetUpScripts 2

You can use a product like the bullzip MD5 Calculator to get the MD5 hash.

Note MD5 is no longer considered secure for encrypting passwords, but for simple file comparison in an environment that you control, it’s fine.

4. Modify “STEP 4” of the script to include all the files you want to download. Examples are included in the script’s comments. This script (SetUpScripts.ps1) is not commented out, so it will be downloaded by default.

If you want to download a new version of the file or script no matter what, use OverwriteAlways. To only download if the local file is different from the web version (by comparing MD5 values), use OverwriteIfChanged.

5. Decide what you want to use as the name of the scripts folder and environment variable on each machine.

By default, scripts will be placed in the Scripts folder on the system drive, which is usually C:. So after running the script, you’ll see this on the system drive:

SetUpScripts 3

By default, you can refer to the Scripts folder with the IT_Scripts environment variable. That variable includes the drive name and the trailing slash, so to run a script, you can, for example, refer to %IT_Scripts%Batch\NiniteUpdate.cmd.

6. Upload this script to your RMM tool and set it to run on the desired machines. (Test it on one machine until everything is working!) You can supply the following parameters, or omit all parameters to accept the defaults:

-BaseWebPath
-BaseScriptsFolder
-EnvVarName

Run the script with administrative privileges so it can create the system-wide path and environment variable. Your RMM may run scripts as SYSTEM, which includes administrative privileges.

7. Sometime after the script runs, reboot the target machine so the environment variable and path will be available. If you’re not in a hurry to use those, you can wait until the next “normal” reboot, e.g. after Windows updates are installed. You only have to reboot once, not every time you run the script.

8. Whenever you add or modify a script or helper, upload the new script to your “secret” web path. If you update a helper that uses MD5 comparison, remember to upload a new .md5 file as well. If this is a new script or helper, modify this script to add the download instruction for the new file, then upload this script to both your RMM tool and to your “secret” web path.

The Script

Save this script as SetUpScripts.ps1.

<#
.Synopsis
  Set up a standard script folder with subfolders.  
  Define an environment variable pointing to script folder.
  Download scripts and helpers from a web site.  


  Copyright (c) 2015 by MCB Systems. All rights reserved.
  Free for personal or commerical use.  May not be sold.
  No warranties.  Use at your own risk.


.Notes 
    Name:       MCB.SetUpScripts.ps1
    Author:     Mark Berry, MCB Systems
    Created:    12/10/2015
    Last Edit:  12/10/2015


  Changes:
  12/10/2015 Initial version, adapted from in-house version.


.Parameter BaseWebPath
    The path to the web folder where scripts are stored, for example
    "http://www.yourdomain.com/rJlN6QLyahRnujU3/" .  
    It should have subfolders batch, helpers, powershell, and
    powershell/callers.
  Default:  Your current script web path--fill in param definition below.
.Parameter BaseScriptsFolder
    The base scripts folder to be created on the system drive of each 
    local computer.  If this parameter includes a drive (like "C:\"), it
    will be ignored--the folder is always created on the system drive
    even if it is not C:\.  Subfolders are permitted, but omit leading 
    and trailing backslashes.
  Default:  "Scripts"
.Parameter EnvVarName
    The system-level environment variable name to create on each computer 
    that points to the base script path.  Notes:
    - The computer must be rebooted before the environment variable 
      can be referenced. 
    - The path stored in the environment variable INCLUDES the trailing 
      slash, e.g. "C:\Scripts\".
  Default:  "IT_Scripts"
.Parameter LogFile
    Path to a log file. Required by MaxFocus script player.  Not used here.
  Default:  "".
#>
param(
  [Parameter(Mandatory = $false,
                    Position = 0,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$BaseWebPath="http://www.yourdomain.com/rJlN6QLyahRnujU3/",


  [Parameter(Mandatory = $false,
                    Position = 1,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$BaseScriptsFolder="Scripts",


  [Parameter(Mandatory = $false,
                    Position = 2,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$EnvVarName="IT_Scripts",


  [Parameter(Mandatory = $false,
                    Position = 3,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$LogFile=""
)


[Boolean]$ErrFound = $false


################################################################################
# Functions for downloading and checking files
################################################################################


#Function to get MD5 of a local file
function Get-LocalMD5( `
  [String]$FilePath="", `
  [String]$FileName="") {


  # Adapted from http://learn-powershell.net/2013/03/25/use-powershell-to-calculate-the-hash-of-a-file/
  # and https://gallery.technet.microsoft.com/scriptcenter/Get-Hashes-of-Files-1d85de46 .
  $FileFullPath = $FilePath + $FileName
  $stream = ([IO.StreamReader]$FileFullPath).BaseStream
  $md5Hash = -join ([Security.Cryptography.HashAlgorithm]::Create( "MD5" ).ComputeHash($stream) | 
    ForEach { "{0:x2}" -f $_ })
  $stream.Close()
  $md5Hash.ToUpper()
  

} # end Get-LocalMD5


#Function to download an .md5 version of a remote file, if available, and extract the contents.
function Get-RemoteMD5( `
  [String]$WebPath="", `
  [String]$FilePath="", `
  [String]$FileName="") {


  $FileName += ".md5" # append ".md5", e.g. PsLoggedon.exe.mp5
  try {
    (New-Object System.Net.WebClient).DownloadFile($WebPath + $FileName, $FilePath + $FileName)
    $md5Hash = (Get-Content ($FilePath + $FileName)).Trim() # Trim() in case any trailing whitespace
    Remove-Item ($FilePath + $FileName) # remove the local copy of the downloaded .md5 file
  }
  catch {
    $md5Hash = "None"
  }
  $md5Hash  
} # end Get-RemoteMD5


function DownloadFile( `
  [String]$WebPath, `
  [String]$FilePath, `
  [String]$FileName,
  [String]$Overwrite,
  [ref]$ErrFound) {


  # From http://stackoverflow.com/questions/1973880/download-url-content
  # Supposed to go to current location, but not working in test. So explicitly 
  # specify target location to include $FilePath.


  # All parameters are mandatory.
  # $Overwrite options are NoOverwrite, OverwriteIfChanged (if MD5 hash 
  # available on server), and OverwriteAlways.
  

  if (-not($WebPath))   { throw "You must supply a value for WebPath" }
  if (-not($FilePath))  { throw "You must supply a value for FilePath" }
  if (-not($FileName))  { throw "You must supply a value for FileName" }
  if (-not($Overwrite)) { throw "You must supply a value for Overwrite" }
  if (-not($ErrFound))  { throw "You must supply a value for ErrFound" }
  

  if ($Overwrite -eq "OverwriteIfChanged") {
    if ( Test-Path ($FilePath + $FileName) ) {
      # File already exists.  Check if it has changed.
      $RemoteMD5 = Get-RemoteMD5 $WebPath $FilePath $FileName  # returns "None" if .md5 file does not exist on web
      $LocalMD5 = Get-LocalMD5 $FilePath $FileName
      # Uncomment next line for debugging MD5
      #"OverwriteIfChanged for $FileName`:  RemoteMD5 = $RemoteMD5, LocalMD5 = $LocalMD5"
      if ( $RemoteMD5 -eq $LocalMD5 ) {
        # File exists and is identical to web version, so do not download--exit function now.
        'SKIPPED downloading ' + $FileName + ' from ' + $WebPath + '. OverwriteIfChanged specified. File already exists and is identical to web version.'
        return 
      }
      # File exists but is not identical.  Continue with download.  Also includes the case where no .md5 file exists on web.
      if ( $RemoteMD5 -eq "None" ) {
        "OverwriteIfChanged specified for $FileName but no .md5 file found in web path. Will download and overwrite local file."
      }
    }
    # File does not exist yet.  Continue with download.
  }
  

  if ($Overwrite -eq "NoOverwrite") {
    if ( Test-Path ($FilePath + $FileName) ) {
      # File already exists NoOverwrite specified, so do not download--exit function now.
      'SKIPPED downloading ' + $FileName + ' from ' + $WebPath + '. NoOverwrite specified and file already exists.'
      return 
    }
  }
  

  # At this point, either the file doesn't exist yet, or OverwriteAlways is in effect, or 
  # OverwriteIfChanged was specified and the local file is different from the web version.
  if ( Test-Path ($FilePath + $FileName) ) { 
    # Rename existing file to .old in case download fails
    Rename-Item -Path ($FilePath + $FileName) ($FilePath + $FileName + ".old")
  }
  try {
    (New-Object System.Net.WebClient).DownloadFile($WebPath + $FileName, $FilePath + $FileName)
    'Successfully downloaded ' + $FileName + ' from ' + $WebPath + '.'
    if ( Test-Path ($FilePath + $FileName + ".old") ) { 
      # Successfully downloaded, so remove .old version
      Remove-Item -Path ($FilePath + $FileName + ".old")
    }
  }
  catch {
    'FAILED to download ' + $FileName + ' from ' + $WebPath + '. System message:'
    '   ' + $_.Exception.Message.ToString()
    $ErrFound.Value = $true # passed as [ref] back to caller
    if ( Test-Path ($FilePath + $FileName + ".old") ) { 
      # Download failed, so rename .old version back
      Rename-Item -Path ($FilePath + $FileName + ".old") ($FilePath + $FileName)
    }
  }
} # end DownloadFile


################################################################################
# STEP 1:  Set up Scripts folder and subfolders
################################################################################


$SystemDrive = $env:SystemDrive
# $env.SystemDrive should always exist but just in case...
if ( !(Test-Path $SystemDrive) ) { $SystemDrive = "C:\" }


# Parse $BaseScriptsFolder parameter:  remove drive letter, add backslashes
$colonIndex = $BaseScriptsFolder.IndexOf(":")                   # will be -1 if no colon
$BaseScriptsFolder = $BaseScriptsFolder.Remove(0,$colonIndex+1) # remove up to and including leading colon
$BaseScriptsFolder = "\" + $BaseScriptsFolder + "\"             # enclose in backslashes
$BaseScriptsFolder = $BaseScriptsFolder.Replace("\\","\")       # remove duplicates in case already in backslashes


$ScriptsPath = $SystemDrive + $BaseScriptsFolder
$BatchScriptsPath = $ScriptsPath + "Batch\"
$ScriptHelpersPath = $ScriptsPath + "Helpers\"
$ScriptLogFilesPath = $ScriptsPath + "LogFiles\"
$PSScriptsPath = $ScriptsPath + "PowerShell\"
$PSScriptCallersPath = $PSScriptsPath + "Callers\"


$env:COMPUTERNAME + ':  Setting up scripts in ' + $ScriptsPath


# Create scripts paths.  New-Item creates higher-level directories if needed.
# -Force means don't give error if it already exists.
# (-Force does not delete existing files.)
New-Item $BatchScriptsPath -Type directory -Force | Out-Null # will also create $ScriptsPath
New-Item $ScriptHelpersPath -Type directory -Force | Out-Null
New-Item $ScriptLogFilesPath -Type directory -Force | Out-Null
New-Item $PSScriptCallersPath -Type directory -Force | Out-Null # will also create $ScriptsPath\PowerShell


################################################################################
# STEP 2:  Create environment variable pointing to Scripts folder
################################################################################


# Create/overwrite machine-level environment variable IT_Scripts with a value of
# $ScriptsPath (the Scripts root).  Trap failure (usually due to insufficient permissions).
try {
  [Environment]::SetEnvironmentVariable( $EnvVarName, $ScriptsPath, `
    [System.EnvironmentVariableTarget]::Machine )
  "Successfully created/updated machine-level environment variable ""$EnvVarName""."
}
catch {
  "FAILED to create/update machine-level environment variable ""$EnvVarName"". System message:"
  "   " + $_.Exception.Message.ToString()
  $ErrFound = $true
}


################################################################################
# STEP 3:  Add Scripts folder to Path
################################################################################


# Add $PSScriptsPath to machine-level Path if it's not already there.
# See http://stackoverflow.com/questions/714877/setting-windows-powershell-path-variable
# Use .NET commands to make sure we get current values (see note in 
# http://technet.microsoft.com/en-us/library/ff730964.aspx).
$CurrentMachinePath = [Environment]::GetEnvironmentVariable("Path", `
   [System.EnvironmentVariableTarget]::Machine)
# Note that .Contains method searches for substring. -contains operator does not.  
if ($CurrentMachinePath.Contains($PSScriptsPath)) {
  '"' + $PSScriptsPath + '" already exists in Path, so Path not updated.'
}
else {
  try {
    [Environment]::SetEnvironmentVariable( "Path", $CurrentMachinePath + ";" + `
      $PSScriptsPath, [System.EnvironmentVariableTarget]::Machine )
    'Successfully updated machine-level environment variable "Path".'
  }
  catch {
    'FAILED to update machine-level environment variable "Path". System message:'
    '   ' + $_.Exception.Message.ToString()
    $ErrFound = $true
  }
}


################################################################################
# STEP 4:  Download files
################################################################################


# Remove renamed script - uncomment and change name if you need to remove a previously-downloaded script
# if ( Test-Path ($PSScriptsPath + "SomeScriptNoLongerInUse.ps1") ) {
#   Remove-Item ($PSScriptsPath + "SomeScriptNoLongerInUse.ps1")
# }


# Download the Batch scripts.  Default is to Overwrite any previous versions.
$WebPath = $BaseWebPath + "batch/" # include trailing slash!
#DownloadFile $WebPath $BatchScriptsPath 'NininiteUpdate.cmd' 'OverwriteAlways' ([ref]$ErrFound)
#DownloadFile $WebPath $BatchScriptsPath 'BatchScript2.cmd' 'OverwriteAlways' ([ref]$ErrFound)


# Download the PowerShell scripts.  Default is to Overwrite any previous versions.
$WebPath = $BaseWebPath + "powershell/" # include trailing slash!
DownloadFile $WebPath $PSScriptsPath 'SetUpScripts.ps1' 'OverwriteAlways' ([ref]$ErrFound)
#DownloadFile $WebPath $PSScriptsPath 'PowerShellScript2.ps1' 'OverwriteAlways' ([ref]$ErrFound)


# Download the specified PowerShell Caller scripts.  Default is to Overwrite any previous versions.
$WebPath = $BaseWebPath + "powershell/callers/" # include trailing slash!
#DownloadFile $WebPath $PSScriptCallersPath 'PowerShellCallerScript1.ps1' 'OverwriteAlways' ([ref]$ErrFound)
#DownloadFile $WebPath $PSScriptCallersPath 'PowerShellCallerScript2.ps1' 'OverwriteAlways' ([ref]$ErrFound)


# Download the Helpers
$WebPath = $BaseWebPath + "helpers/" # include trailing slash!
#DownloadFile $WebPath $ScriptHelpersPath 'NiniteOne.exe' 'OverwriteIfChanged' ([ref]$ErrFound) # only download if different from web version 
#DownloadFile $WebPath $ScriptHelpersPath 'Helper2.exe' 'OverwriteIfChanged' ([ref]$ErrFound) # only download if different from web version 


################################################################################
# Wrap-up
################################################################################


# Conclude with local time
'Local Machine Time:  ' + (Get-Date -Format G)


if ($ErrFound) {
  $ExitCode = 1001 # Cause script to report failure in GFI dashboard
}
else {
  $ExitCode = 0
}
"Exit Code: " + $ExitCode
Exit $ExitCode

Help!

This may all seem a bit overwhelming if you’re not familiar with scripting. MCB Systems can help you set this up and/or create custom scripts for your needs. Contact MCB Systems.



1 Comment

  1. Track IP History And Alert On Change | MCB Systems   |  March 31, 2016 at 12:47 pm

    […] is located. (Note the use of the %IT_Scripts% environment variable in the screen shot below—see this post for […]

Leave a Reply





*