PowerShell Script to Run a Program Once

Mark Berry November 18, 2014

I want to deploy CryptoPrevent Portable using the MaxFocus RMM tool. CryptoPrevent sets up software restriction policies to keep programs from running in certain locations. However, it doesn’t actually install a program or service, so it’s hard to know if it’s already been set up. (Thanks to Jake Paternoster for confirming that in a comment on this post.)

I decided to write a RunProgramOnce PowerShell script that would record the execution of the the program in the registry. If execution succeeds (returns 0), the program version and parameters are written to the registry. If you run the script again with the same program version and parameters, it sees the previous execution and does not re-run the program. However, if the version or parameters change, the program is run again.

There are important notes at the top of the script. A few bear repeating:

  • Provide your company name as the first parameter to the script. This becomes a registry key under HKLM\Software. If your company name contains spaces, enclose it in quotation marks
  • You must run the script with administrative permissions so that it can write to the HKEY_LOCAL_MACHINE hive of the registry.
  • The script in its current form does not handle parameters containing spaces, or parameters enclosed in quotation marks. Basically, all parameters for the program you are executing must be enclosed on one set of quotation marks, passed as the third parameter (-ProgramParams) to the script. The parameters are split on the space character and passed to the program. See the examples in the script.

Running from MaxFocus

The script can be run independently, but it is designed to give a meaningful summary line following by execution details in the MaxFocus dashboard.

To add to the confusion about parameters, MaxFocus requires that any parameters containing spaces be enclosed in three quotation marks (see the March 2, 2011 update in my post on Deploying PowerShell Scripts to GFI MAX.

Here is a sample command line as specified in MaxFocus to set up CryptoPrevent, writing to the MCB Systems registry key:

"""MCB Systems""" """C:\Scripts\Helpers\CryptoPrevent.exe""" """/apply /silent /appdata /appdatadeep /appdatalocal /programdata /userprofile /startup /bin /fakeexts /syskey /cipher /vssadmin"""

To remove the CryptoPrevent settings:

"""MCB Systems""" """C:\Scripts\Helpers\CryptoPrevent.exe""" """/apply /silent /undoall"""

To deploy HitmanPro.Alert (includes a CryptoGuard component):

"""MCB Systems""" """C:\Scripts\Helpers\hmpalert.exe""" """/install"""

Note that this script does not download the program to the client machine—it is assumed that you have a separate means for distributing program executables.

The Script

Copy and paste this as a PowerShell script (e.g. MCB.RunProgramOnce.ps1).

<#
.Synopsis
  Run a batch program with parameters, but only run the same version with the same 
  parameters one time even if script is executed repeatedly.
  
  See list of parameters below.
  
  Program version and parameters are stored in 
    "HKLM:\Software\<RegKeyCompany>\Scripts\<ProgramName>".  
  
  Run this script run with administrative privileges in order to be able to write to HKLM.
  
  If the program executable is not found, or if executing the prrogram returns a 
  non-zero exit code, the script exits with ExitCode 1001 so the failure will
  appear the MaxFocus dashboard. 

  NOTE Enclose ALL program parameters in ONE set of quotation marks and pass that
       as the third parameter to this script.  Program params will be split on spaces
       and passed to the program.  This does NOT currently accommodate program
       parameters enclosed in quotation marks or containing spaces!  

  When run from the MaxFocus agent, script parameters containing spaces must be enclosed
  in TRIPLE quotation marks.  See Examples below.
       
  Copyright (c) 2014 by MCB Systems. All rights reserved.
  Free for personal or commercial use.  May not be sold.
  No warranties.  Use at your own risk.

.Notes 
  Name:       MCB.RunProgramOnce.ps1
  Author:     Mark Berry, MCB Systems
  Created:    11/18/2014
  Last Edit:  11/18/2014
    
  Changes:

.Parameter RegKeyCompany
  The name to substitute in this registry path, usually your company name.
    "HKLM:\Software\<RegKeyCompany>\Scripts\<ProgramName>"
  Default:  "MyITCompany".
.Parameter ProgramPath
  The program to run, with its path.  If in current path, prepend with ".\".
  Default:  "".
.Parameter ProgramParams
  Optional program parameters.
  Default:  "".
.Parameter LogFile
  Path to a log file. Required by MaxFocus script player.  Not used here.
  Default:  "".
 
.Example
  Examples reflect running from MaxFocus agent with triple quotation marks.  
  To run stand-alone, use single quotation marks.
  .\MCB.RunProgramOnce.ps1 """Best IT""" """C:\Scripts\Helpers\CryptoPrevent.exe""" 
    """/apply /silent /appdata /appdatadeep /appdatalocal /programdata /userprofile /startup 
    /bin /fakeexts /syskey /cipher /vssadmin"""
  Run CryptoPrevent add settings but only if this version has not been run before with these parameters.
.Example
  .\MCB.RunProgramOnce.ps1 """Best IT""" """C:\Scripts\Helpers\CryptoPrevent.exe""" "/apply /silent /undoall"""
  Run CryptoPrevent to remove all settings if this settings have not been previously removed.
#>
param(
    [Parameter(Mandatory = $true,
                    Position = 0,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$RegKeyCompany="MyITCompany",

    [Parameter(Mandatory = $true,
                    Position = 1,
                    ValueFromPipelineByPropertyName = $true)]
    [String]$ProgramPath="",

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

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

$ErrorFound = $false
$OutputDetail = "" # write messages to this variable; output at end
$PreviouslyRun = $false
$RunThisTime = $false

# Convert $ProgramParams to an array, splitting on the space character
# This does NOT accommodate parameters enclosed in quotation marks or containing spaces!
$option = [System.StringSplitOptions]::RemoveEmptyEntries
$ProgramParamsArray = $ProgramParams.Split(" ", $option)

# Check that program file exists
if (Test-Path $ProgramPath) { 
  # Get the current program version
  $ProgramVersion = (Get-Item $ProgramPath).VersionInfo.ProductVersion
  
  # Get the basename of the program and initialize RegKey
  $ProgramName = (Get-Item $ProgramPath).Basename
  $RegKey = "HKLM:\Software\$RegKeyCompany`\Scripts\$ProgramName\"
  
  # Check registry for previous program execution
  if (Test-Path $RegKey) {
    $PrevProgramVersion = (Get-ItemProperty -Path $RegKey).Version
    $PrevProgramParams = (Get-ItemProperty -Path $RegKey).Params
    $PrevProgramRanAt= (Get-ItemProperty -Path $RegKey).RanAt
    $OutputDetail += "`n$RegKey exists."
    $OutputDetail += "`nProgram ""$ProgramPath"" version $PrevProgramVersion ran previously with parameter(s) " + `
                     """$PrevProgramParams"" on $PrevProgramRanAt."
    $PreviouslyRun = $true
  } else {
    $OutputDetail += "`n$RegKey does not exist."
    $PrevProgramVersion = ""
    $PrevProgramParams = ""
    $PrevProgramRanAt = ""
  }
   
  if ( ($PrevProgramVersion -eq $ProgramVersion) -and ($PrevProgramParams -eq $ProgramParams) ) {
    $OutputDetail += "`nProgram version and parameters have not changed.  Not re-running."
    # Skipping re-running the program is NOT an error; do not set $ErrorFound
  } else {
    
    $RunThisTime =  $true
    # Run the program with params using the "&" call operator
    if ($PreviouslyRun) {
      $OutputDetail += "`nProgram version or parameters have changed.  Re-running."
    } else {
      $OutputDetail += "`nProgram has not run previously.  Running for first time."
    }
    $OutputDetail += "`nRunning program ""$ProgramPath"" version $ProgramVersion with parameter(s) ""$ProgramParams""."
    # Use echoargs.exe for testing.  See http://edgylogic.com/blog/powershell-and-external-commands-done-right/ .
    #&.\echoargs.exe $ProgramParamsArray 
    $LASTEXITCODE = $null # Clear previous values in case program does not set an ExitCode
    &$ProgramPath $ProgramParamsArray
    $ProgramExitCode = $LASTEXITCODE
    if ($ProgramExitCode -eq $null) { 
      $OutputDetail += "`nProgram did not return an ExitCode.  Assuming successful execution."
    } else {
      $OutputDetail += "`nProgram returned ExitCode $ProgramExitCode`."
    }
    
    if ( ($ProgramExitCode -eq 0) -or ($ProgramExitCode -eq $null) ) {
      # Record successful run in registry
      New-Item -Path $RegKey -Force | Out-Null # create key if it's not already there; discard returned object 
      Set-ItemProperty -Path $RegKey -Name "Version" -Value $ProgramVersion
      Set-ItemProperty -Path $RegKey -Name "Params" -Value $ProgramParams
      Set-ItemProperty -Path $RegKey -Name "RanAt" -Value (Get-Date -Format G)
      $OutputDetail += "`nThis run recorded in registry key $RegKey."
    } else {
      $ErrorFound = $true
      $OutputDetail += "`nNon-zero ExitCode so this run not recorded in registry."
    }
  
  }
} else {
  $OutputDetail += "`nProgram file ""$ProgramPath"" not found."
  $ErrorFound = $true
}

if ($ErrorFound) {
  $SummaryLine = "Error running program ""$ProgramPath""."
  $ExitCode = 1001
} else {
  if ($RunThisTime) {
    $SummaryLine = """$ProgramPath"" ran successfully."
  } else {
    $SummaryLine = """$ProgramPath"" ran previously. Not re-run."
  }
  $ExitCode = 0
}
#Print one-line summary first for display in monitoring dashboard. Abbreviate date/time.
$SummaryLine + " [" + (Get-Date -Format "MM/dd HH:mm") + "]"
# Print detailed output stored as we went through the steps above
"`n" + $OutputDetail
"`n`n======================================================"
"`nLocal Machine Time:  " + (Get-Date -Format G)
"`nExit Code: " + $ExitCode

Exit $ExitCode

Leave a Reply





*