Windows Install PowerShell Script for RMM/MDM

Introduction

The following PowerShell script can be used for RMM and MDM deployments and manages the installation/uninstallation of our Field Effect MDR endpoint agent for Windows. The script below leverages our API calls to create a simplified and automated process for users leveraging and RMM/MDM solution, as well as create the necessary log files our Support team may require for future troubleshooting or investigations. 


Procedure

You will need a valid MDR portal token to authorize the Field Effect API calls. For help creating your token, see Accessing Field Effect's API Documentation


Begin by saving the text below as a .ps1 file named "agent_install_script.ps1".


Once saved, use your newly created agent_install_script.ps1 within your RMM or MDM solution. 


Since there are many different tools available, and the complexity of each deployment will vary between companies, the process for adding this to your RMM/MDM tool will vary between organizations.


<#

.SYNOPSIS

    Installs or uninstalls the MDR agent on a Windows machine.

.DESCRIPTION

    This script manages the installation or uninstallation of the Field Effect MDR Windows agent. 
    It either installs the agent using a provided API token and license key, or client ID or uninstalls it if the 
    `-uninstall` flag is provided.

    The script requires the user to have Windows administrator privileges to successfully install or uninstall the agent.

    **Usage Example:**
    > powershell ./agent_install_script.ps1 -api_token 'your_api_token' -license_key 'your_license_key'
    > powershell ./agent_install_script.ps1 -api_token 'your_api_token' -client 'Disco Time'
    > powershell ./agent_install_script.ps1 -uninstall -api_token 'your_api_token'

.PARAMETER api_token
    The API token used for authentication with the Field Effect portal.
    This token is required to retrieve the agent installer.

.PARAMETER client
    The name or ID of the client/organization.
    This parameter is used to identify which client will receive the license key. If `license_key` is not provided, 
    the client name or ID is used to fetch the correct client details.

.PARAMETER license_key
    The explicit license key for the client organization to be used during the installation process.
    If provided, the script will install the agent using this key.

.PARAMETER uninstall
    A switch that indicates the script should uninstall the MDR agent instead of installing it.
    If this flag is provided, the script will stop any running services and uninstall the agent from the machine.

.PARAMETER debug
    A switch that prints all logs to the command line as well as logging to file.

.EXAMPLE
    # Install the agent using an API token and license key
    .\agent_install_script.ps1 -api_token 'your_api_token' -license_key 'your_license_key'

.EXAMPLE
    # Install the agent using an API token and client name (no license key)
    .\agent_install_script.ps1 -api_token 'your_api_token' -client 'ClientName'

.EXAMPLE
    # Uninstall the agent (uninstall mode)
    .\agent_install_script.ps1 -uninstall -api_token 'your_api_token'

.NOTES
    Author: Client Support
    Date: January 2025

    This script is intended to install or uninstall the MDR agent on a Windows machine, using an api token and either a license key or client information to authenticate with the portal and retrieve the appropriate agent installer.

    Ensure that you have the necessary administrator privileges to perform installation or uninstallation.

    If your execution policy prevents running scripts, you can bypass it by adding `-ExecutionPolicy Bypass` to the PowerShell command:
    > powershell -ExecutionPolicy Bypass ./agent_install_script.ps1

    Ensure that the API token provided is valid and has the necessary permissions.

    For more information about the FieldEffect MDR service, visit the official documentation at https://support.fieldeffect.com

    Changelog:
    2024-08: Changed endpoint API lookup to no longer max out at 1000 records.
    2024-09: Added deployment check option.
    2025-03: Updated zip bundle calls
    2025-07: Sanitized filenames to avoid path problems
             Forced TLS 1.2 for download
    2025-08: Added support to disable agent protection before agent uninstall 
#>

# Optional command line params
param (
  [string]$api_token,
  [string]$client,
  [string]$license_key,
  [switch]$uninstall,
  [switch]$debug
)

$version="v7.1.3"
$win_arch = 64
$mdr_agent_installed = $false
$agent_installer_filename = "x" #look it up
$use_license = $false
$client_valid = $false
$script:StatusFilePath = "C:\ProgramData\Field Effect\Covalence\data\status.json"
# Ensure TLS 1.2 is enabled
[Net.ServicePointManager]::SecurityProtocol = 3072


# Create the "FieldEffect_MDR" log directory if it doesn't exist
$script:installer_filename="x"
$script:installer_path = Join-Path $Env:TMP "FieldEffect_MDR"
$working_folder = $script:installer_path

if (-not (Test-Path $working_folder)) 
{
    New-Item -Path $script:installer_path -Name "FieldEffect_MDR" -ItemType "Directory" | Out-Null
}

# set log file to save in the installing users folder
$debug_log = Join-Path $working_folder "mdrAgentPoShInstaller.proc_$env:computername_$(Get-Date -Format yyyyMMddTHHmmZ).log"

# API token validation
if (-not $api_token -or $api_token.Trim() -eq "") {
    $errors += "API token is required. Use -api_token parameter"
}
# Display errors if any
if ($errors.Count -gt 0) {
    Write-Host "PARAMETER VALIDATION ERRORS:" -ForegroundColor Red
    foreach ($error in $errors) {
        Write-Host "  - $error" -ForegroundColor Red
    }
    Write-Host ""
    Show-Help
    exit 1
}

# WriteLog helper function writes log messaging to log file; 'debug' parameter also outputs logs to screen   
function WriteLog
{
    Param ([string]$_log_msg)
    if ($debug) {
        Write-Host $_log_msg
    }
    $timestamp = (Get-Date).toString("yyyy/MM/dd HH:mm:ss")
    $log_msg = "$timestamp $_log_msg"
    Add-content $debug_log -value $log_msg  
}

if (-not $uninstall) {
    #check API token
    if ($api_token.Length -ne 128) {
        $log_err = "Portal token invalid; token length is $($api_token.Length)"
        WriteLog $log_err
        throw $log_err
    }    
    #set API header
    $headers = @{
        Authorization="Bearer $api_token"
        ContentType="application/json"
    }
}

# Set values for startup parameters if provided
$api_token = if (![string]::IsNullOrEmpty($api_token)) { $api_token } else { $null }
$client = if (![string]::IsNullOrEmpty($client)) { $client } else { $null }
$license_key = if (![string]::IsNullOrEmpty($license_key)) { $license_key } else { $null }

# Set architecture type based on processor
$win_arch = if ($env:PROCESSOR_ARCHITECTURE -eq "X86") { 32 } else { 64 }

##********************************************************************************************************
##
##    FUNCTION CheckInstall
##    Function to check Field Effect MDR agent installation and status
##    
##    PARAMETERS:
##      -WaitForActivation: If specified, waits for agent to reach ACTIVE status
##      -TimeoutMinutes: Maximum time to wait for activation (default: 10 minutes)
##
##    RETURNS:
##      Object with installation details and status
##
##********************************************************************************************************
function CheckInstall {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [switch]$WaitForActivation,
        
        [Parameter(Mandatory = $false)]
        [int]$TimeoutMinutes = 10
    )
    
    # Initialize result object
    $result = [PSCustomObject]@{
        IsInstalled = $false
        AgentVersion = $null
        CurrentStatus = $null
        StatusFileExists = $null
        OrganizationId = $null
        AgentId = $null
        IsActive = $false
        ActivationSuccessful = $false
        ErrorMessage = $null
        TimeoutOccurred = $false
    }
    
    try {
        WriteLog "Checking agent installation status at: $script:StatusFilePath"
        
        # Check if status file exists
        $result.StatusFileExists = Test-Path $script:StatusFilePath
        
        if (-not $result.StatusFileExists) {
            WriteLog "Status file not found - agent not installed or installation incomplete"
            $result.ErrorMessage = "Status file not found at expected location"
            return $result
        }
        
        # Read and parse status file
        try {
            $statusContent = Get-Content -Path $script:StatusFilePath -Raw -ErrorAction Stop
            $statusJson = $statusContent | ConvertFrom-Json -ErrorAction Stop
            
            # Validate JSON structure
            if (-not $statusJson.AgentInfo) {
                throw "Invalid status file structure - AgentInfo section missing"
            }
            
            $result.IsInstalled = $true
            $result.AgentVersion = $statusJson.AgentInfo.AgentVersion
            $result.CurrentStatus = $statusJson.AgentInfo.StateName
            $result.OrganizationId = $statusJson.AgentInfo.CurrentOrgId
            $result.AgentId = $statusJson.AgentInfo.AgentId
            $result.IsActive = ($result.CurrentStatus -eq "ACTIVE")
            
            WriteLog "Agent found - Version: $($result.AgentVersion), Status: $($result.CurrentStatus)"
            
            if ($result.OrganizationId) {
                WriteLog "Organization ID: $($result.OrganizationId)"
            }
            
        }
        catch {
            $result.ErrorMessage = "Failed to read or parse status file: $_"
            WriteLog $result.ErrorMessage
            return $result
        }
        
        # Wait for activation if requested
        if ($WaitForActivation) {
            $result.ActivationSuccessful = Wait-ForAgentActivation -TimeoutMinutes $TimeoutMinutes
            $result.TimeoutOccurred = -not $result.ActivationSuccessful -and $result.IsInstalled
            
            # Update final status after waiting
            try {
                $finalStatus = Get-Content -Path $script:StatusFilePath -Raw | ConvertFrom-Json
                $result.CurrentStatus = $finalStatus.AgentInfo.StateName
                $result.IsActive = ($result.CurrentStatus -eq "ACTIVE")
            }
            catch {
                WriteLog "Warning: Could not read final status after activation wait"
            }
        }
        
        return $result
    }
    catch {
        $result.ErrorMessage = "Unexpected error during installation check: $_"
        WriteLog $result.ErrorMessage
        return $result
    }
}

##********************************************************************************************************
##
##    HELPER FUNCTION Wait-ForAgentActivation
##    Waits for the agent to reach ACTIVE status 
##
##********************************************************************************************************
function Wait-ForAgentActivation {
    [CmdletBinding()]
    param (  
        [Parameter(Mandatory = $true)]
        [int]$TimeoutMinutes
    )
    
    WriteLog "Waiting for agent activation (timeout: $TimeoutMinutes minutes)"
    
    $timeoutSeconds = $TimeoutMinutes * 60
    $pollIntervalSeconds = 10
    $maxAttempts = [Math]::Ceiling($timeoutSeconds / $pollIntervalSeconds)
    $attempt = 1
    
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    
    while ($attempt -le $maxAttempts) {
        try {
            $statusJson = Get-Content -Path $script:StatusFilePath -Raw | ConvertFrom-Json
            $currentStatus = $statusJson.AgentInfo.StateName
            $elapsedMinutes = [Math]::Round($stopwatch.Elapsed.TotalMinutes, 1)
            
            WriteLog "Activation check $attempt/$maxAttempts - Status: $currentStatus (Elapsed: ${elapsedMinutes}m)"
            
            if ($currentStatus -eq "ACTIVE") {
                $stopwatch.Stop()
                WriteLog "Agent successfully activated after ${elapsedMinutes} minutes"
                return $true
            }
            
            # Check for error states that won't resolve
            $errorStates = @("ERROR", "FAILED", "DISABLED")
            if ($currentStatus -in $errorStates) {
                WriteLog "Agent is in error state: $currentStatus - activation unlikely to succeed"
                break
            }
            
            if ($attempt -lt $maxAttempts) {
                Start-Sleep -Seconds $pollIntervalSeconds
            }
            
            $attempt++
        }
        catch {
            WriteLog "Error reading status during activation wait (attempt $attempt): $_"
            if ($attempt -lt $maxAttempts) {
                Start-Sleep -Seconds $pollIntervalSeconds
            }
            $attempt++
        }
    }
    
    $stopwatch.Stop()
    $finalElapsed = [Math]::Round($stopwatch.Elapsed.TotalMinutes, 1)
    WriteLog "Agent activation timeout after ${finalElapsed} minutes - final status may not be ACTIVE"
    return $false
}

##********************************************************************************************************
##
##    FUNCTION PreScript
##    Performs cursory checks on input parameters. If a client name is provided, also validates client
##    name against known clients from portal API.
##    Returns nothing.
##
##********************************************************************************************************
function PreScript 
{
    $org_id="x"

    WriteLog "Verifying setup..."
    Writelog  "Detecting Powershell verison $($PSVersionTable.PSVersion.ToString())"

    #check if running as Admin
    $check_user_priv = [Security.Principal.WindowsIdentity]::GetCurrent()
    $run_as_admin = ([Security.Principal.WindowsPrincipal]($check_user_priv)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
    if (-not $run_as_admin) {
       WriteLog "WARNING- user not detected as having admin permission: $($check_user_priv.Name)"
    }

    #check that client name OR license key is provided
    if (($license_key.length -eq 0) -AND ($client.length -eq 0)){
        $log_err = "At least one of org license key or client name is required and was not provided"
        WriteLog $log_err
        throw $log_err
    }

    #validate license input
    if (($license_key.length -ne 0) -AND ($license_key.length -ne 156)){
        $log_err = "COVALENCE_LICENSE key was not valid. Key length appears to be $($license_key.length)/expected 156; double-check that all characters were copied"
        WriteLog $log_err
        throw $log_err
    } 

    if ($license_key.length -eq 156){
        $use_license=$true
        WriteLog "Detected proper license key: $($license_key.Length)"
        return $org_id, $use_license, $false
    }      

    #validate client name input
    if(($license_key.length -eq 0) -and ($client.Length -ne 0)){
        $client_list = Invoke-RestMethod -Method Get -Uri "https://services.fieldeffect.net/v1/organizations" -Headers $headers
        $client_list = $client_list -creplace '"id"','"new_ID"' | ConvertFrom-JSON

        # Check if the client matches name or client_id
        $client_valid = $false
        foreach ($c in $client_list) {
            if ($client -like $c.name -or $client -like $c.client_id) {
                $client_valid = $true
                $org_id = $c.ID
                WriteLog "Proceeding to find license key for $($c.name)."
                return $org_id, $false, $client_valid
                break  # Exit loop early once a match is found
            }
        }

        # Handle invalid client
        if (-not $client_valid){
            $log_err = "Client name/id is not valid. It must be one of:"
            WriteLog $log_err
            $client_list | ForEach-Object { WriteLog "$($_.name) ($($_.client_id))" }
            throw "Client name/id is not valid"
        }
    }#endif

} #end PreScript

function GetOrgid 
{
    param (
        [Parameter(Mandatory = $true)][boolean] $_use_license,
        [Parameter(Mandatory = $true)][boolean] $_client_valid,
        [Parameter(Mandatory = $true)][string] $_org_id
    )

    $org_id="x"

    ##get ORGID:
        #if license key provided
            #get client organization(s) and use ID of first returned org; the license key accompanying the bundle zip will be ignored 
        #if license key not provided
            #Use client name provided to get client ID from name; the license key from the bundle zip will be used

    if($_use_license){
        #get v1/organizations and use [0] to get orgid of user and download generic bundle using passed in license key
        $generic_client= Invoke-RestMethod -Method Get -Uri "https://services.fieldeffect.net/v1/organizations" -Headers $headers 
        $generic_client = $generic_client -creplace '"id"','"new_ID"' | ConvertFrom-JSON
        $org_id=$generic_client[0].ID
    }

    elseif ($_client_valid){
        #use orgid value already set under pre-script
        $org_id = $_org_id   
    }

    else {
        throw "Client name/id or license key not provided"
    }
  
    return $org_id
}

##********************************************************************************************************
##
##    FUNCTION DownloadInstaller
##    Passed the org_id, downloads the installer bundle from the portal API, unzips the msi and 
##    license files to the temp folder, then deletes the downloaded archive.
##    Returns nothing.
##
##********************************************************************************************************
function DownloadInstaller 
{
    param (
        [Parameter(Mandatory = $true)][string] $_org_id
    )
    WriteLog "Downloading install bundle to $script:installer_path..."

    $installer_body = @{
        bundle_constituents = @(
            @{
                architecture = "$win_arch bit"
                platform = "Windows"
            }
        )
        organization_id = $_org_id
    } | ConvertTo-Json
   
    $bundle_response = Invoke-RestMethod -Method Post -Uri "https://services.fieldeffect.net/v1/endpoint_installer_bundles" -Body $installer_body -Headers $headers -ContentType application/json

    # Define paths
    $zipFileName = $bundle_response.file_name
    $sanitizedName = $zipFileName -replace  '([\[\]\*\?])', ''  ##remove special chars inherited from client org name
    $zipFilePath = Join-Path $working_folder $sanitizedName
    $extractFolderName = [System.IO.Path]::GetFileNameWithoutExtension($sanitizedName)
    $extractPath = Join-Path $working_folder $extractFolderName

    # Remove any leftover files or folders from previous runs
    if (Test-Path $zipFilePath) {
        Remove-Item $zipFilePath -Force
    }
    if (Test-Path $extractPath) {
        Remove-Item $extractPath -Recurse -Force
    }

    # Download the ZIP file 
    try {
        $msi = New-Object System.Net.WebClient
        $msi.DownloadFile($bundle_response.download_link, $zipFilePath)
        $msi.Dispose()
        WriteLog "Downloaded ZIP to $zipFilePath"
    } catch {
        WriteLog "Download failed: $_" 
        throw "Download error: $_"
    }

    # Validate file before extraction
    if (-not (Test-Path $zipFilePath)) {
        throw "ZIP file was not found at $zipFilePath"
    }

    try {
        Expand-Archive -Path $zipFilePath -DestinationPath $extractPath -Force
        WriteLog "Extracted ZIP contents to $extractPath"
    } catch {
        WriteLog "Extraction failed: $_" 
        throw "Failed to extract ZIP file: $_"
    }

    # Set final directory of msi installer file
    $script:installer_path = $extractPath
    $script:installer_filename = Get-ChildItem -Path $extractPath -Filter *.msi | Select-Object -First 1

    if (-not $script:installer_filename) {
        throw "MSI installer not found in extracted contents"
    }
    WriteLog "Installer ready at $script:installer_path\$script:installer_filename"
}

##********************************************************************************************************
##
##    FUNCTION GetLicense
##    Passed a use_license bool, pulls the license_key from the input parameter on TRUE or from the  
##    downloaded installer bundle on FALSE.
##    Returns the license key.
##
##********************************************************************************************************
function GetLicense 
{
    param (
        [Parameter(Mandatory = $true)][boolean] $_use_license
    )
    $mask_length=8

    if($_use_license) {
        #use license_key as typed
        WriteLog "Using provided license key: "
        $license_key = $license_key
        # clean up unused license.txt file
        Remove-Item -Path (Join-Path $script:installer_path "license.txt")
    }

    else {
        $license_key = Get-Content -Path (Join-Path $script:installer_path "license.txt")
        # clean up license.txt file
        WriteLog "Using license key from downloaded bundle: "
        Remove-Item -Path (Join-Path $script:installer_path "license.txt")
    }

    $masked_key = $license_key.Substring(0, $mask_length) + '*' * ($license_key.Length - $mask_length - $mask_length) + $license_key.Substring($license_key.Length - $mask_length)
    WriteLog $masked_key
    
    return $license_key
}

##********************************************************************************************************
##
##    FUNCTION InstallAgent
##    Passed the license key, performs a silent install using msiexec. Msiexec logs are saved in the 
##    working folder.
##    Returns nothing
##
##********************************************************************************************************

function InstallAgent 

{
    param (
        [Parameter(Mandatory = $true)][string] $_license_key
    )
    WriteLog "Installing agent"

    # Construct the msiexec argument list 
    $msiPath = Join-Path $script:installer_path $script:installer_filename
    $logPath = Join-Path $script:installer_path "msiexec_log.txt"
    $arguments = "/i `"$msiPath`" COVALENCE_LICENSE=`"$_license_key`" /qn /l*v `"$logPath`""

    # Run the installation process and wait for completion
    Start-Process msiexec.exe -Wait -ArgumentList $arguments
    WriteLog "msiexec logs written to:"
    WriteLog "         $logpath"
    
    Start-Sleep -Seconds 1
    if (-not (Test-Path "C:\ProgramData\Field Effect\Covalence\data\status.json")){
        WriteLog "Checking installation status"
        Start-Sleep -Seconds 5
        if (-not (Test-Path "C:\ProgramData\Field Effect\Covalence\data\status.json")){
            $log_err="Installation did NOT succeed. No status file found."
            WriteLog $log_err
            throw $log_err
        }
    }
}

##********************************************************************************************************
##
##    FUNCTION UninstallAgent
##    Checks if the "Field Effect MDR" agent is installed via the registry.
##    If found, stops MDR services (Covalence Endpoint Service and CovAgent) and uninstalls via msiexec.
##    Returns nothing
##
##********************************************************************************************************
function UninstallAgent {
    
    #get org id from agent info 
    if (Test-Path $script:StatusFilePath) {
        try {
            $_org_id= (Get-Content $script:StatusFilePath | ConvertFrom-Json).AgentInfo.CurrentOrgId
            $agent_state= (Get-Content $script:StatusFilePath | ConvertFrom-Json).AgentInfo.StateName
            WriteLog "Woking on org id $_org_id"
        }

        catch {
            throw "Failed to read CurrentOrgId: $_"
        }
    } else {
        throw "Status file not found: $script:StatusFilePath"
    }

    try {
        WriteLog "Uninstalling agent"
        
        # Search for the installed agent in the registry
        $agentMSI = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* |
                    Where-Object { $_.DisplayName -like 'Field Effect MDR' }
        
        if (-not $agentMSI) {
            WriteLog "No MDR installation found."
            return
        }

        $hostname = $env:COMPUTERNAME
        WriteLog "Processing hostname: $hostname"

        # Get host information from API
        try {
            $host_list = Invoke-RestMethod -Method Get -Uri "https://services.fieldeffect.net/v1/endpoint_devices?organization_id=$_org_id&hostname=$hostname"  -Headers $headers                                                            
        }
        catch {
            WriteLog "API call failed to get endpoint device data for $hostname; does this host appear in the host list for org $_org_id ?"
        }

        if (-not $host_list.items -or $host_list.items.Count -eq 0) {
            if ( $agent_state -eq "ACTIVE"){
              WriteLog "Endpoint not found in organization but its local status is active. Uninstalling may FAIL if protection is enabled."
              #throw "Cannot disable protection"
            }
            else{
              WriteLog "Endpoint not found in organization. Will try to uninstall without modifying protectection"
            }
        } else {
            $endpoint_id = $host_list.items[0].id
            WriteLog "Found endpoint ID: $endpoint_id"
        
            $bulk_update_body = @{
                agent_protection = "Disabled"
                endpoint_id = $endpoint_id
            } | ConvertTo-Json

            # Check for existing protection override
            try {
                $protect_status = Invoke-RestMethod -Method Get -Uri "https://services.fieldeffect.net/v1/endpoint_feature_overrides?endpoint_id=$endpoint_id"  -Headers $headers
            }
            catch {
                throw "API call failed to get endpoint feature overrides"
            }

            # Update or create protection override
            try {
                if ($protect_status.items -and $protect_status.items.Count -gt 0 -and $protect_status.items[0].id) {
                    # Override exists; update it
                    $override_id = $protect_status.items[0].id
                    Invoke-RestMethod -Method Patch -Uri "https://services.fieldeffect.net/v1/endpoint_feature_overrides/$override_id"  -Headers $headers -Body $bulk_update_body -ContentType "application/json"
                } 
                else {
                    # No override; create one
                    Invoke-RestMethod -Method Post -Uri "https://services.fieldeffect.net/v1/endpoint_feature_overrides"  -Headers $headers -Body $bulk_update_body -ContentType "application/json"
                }
                WriteLog "Endpoint protection for $hostname has been disabled"
                #### add delay to alllow EP protection to trickle from portal to appliance to agent
                # $wait_for_protect = $true
            }
            catch {
                WriteLog "Failed to update protection status: $_"
                throw "API call failed to update endpoint protection"
            }
        }

        WriteLog "$($agentMSI.DisplayName) $($agentMSI.DisplayVersion) is installed."
        WriteLog "Uninstalling $($agentMSI.DisplayName) $($agentMSI.DisplayVersion)"


        #####Stop the services associated with the agent
        $services = @("Covalence Endpoint Service", "CovAgent")

        foreach ($service in $services) {
            for ($attempt = 0; $attempt -le 4; $attempt++) {
                try {
                    $serviceObj = Get-Service -Name $service -ErrorAction SilentlyContinue
            
                    if (-not $serviceObj) {
                        WriteLog "Service $service not found"
                        break
                    }
            
                    if ($serviceObj.Status -ne 'Running') {
                        WriteLog "Service $service is not running"
                        break
                    }
            
                    WriteLog "Attempting to stop service: $service (Attempt $($attempt + 1))..."
                    Stop-Service -Name $service -Force -ErrorAction Stop
                    WriteLog "Service $service stopped successfully"
                    break
                }
                catch {
                    WriteLog "Failed to stop $service on attempt $($attempt + 1): $_"
                    if ($attempt -eq 4) {
                        throw "Failed to stop $service after 5 attempts: $_"
                    }
                    WriteLog "Retrying in 5 minutes..."
                    Start-Sleep -Seconds 300
                }
            }
        }

        $installed_msi = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* |  Where-Object DisplayName -like Field*
        $installed_msi_guid = $installed_msi.PSChildName

        try {
            WriteLog "Running uninstall"
            ##   comment to disable use of universal guid; use $installed_msi_guid instead 
            ##     $uninstallGuid = '{d6b2aca4-07e2-4b97-a7f8-d9cb64cb25a8}'
            ##
            $uninstallGuid = $installed_msi_guid

            $process = Start-Process -FilePath 'msiexec.exe' -Wait -PassThru -ArgumentList '/x', $uninstallGuid, '/qn', '/norestart'
            
            if ($process.ExitCode -eq 0) {
                WriteLog "Uninstall process completed with success code"
            }
            #elseif ($process.ExitCode -eq 1605) {
            #    WriteLog "Product not found - may already be uninstalled"
            #}
            else {
                WriteLog "Uninstall process completed with exit code: $($process.ExitCode)"
            }
        }
        catch {
            WriteLog "Failed to uninstall the agent: $_"
            throw "Uninstall failed"
        }

        # Verify uninstall was successful
        WriteLog "Verifying uninstall success..."
        Start-Sleep -Seconds 3  # Allow time for uninstall to complete
        
        # Check if the application is still in registry
        $remainingInstall = Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* |
                           Where-Object { $_.DisplayName -like 'Field Effect MDR' }
        
        if ($remainingInstall) {
            WriteLog "WARNING: Application still appears in registry after uninstall"
            WriteLog "Remaining entry: $($remainingInstall.DisplayName) $($remainingInstall.DisplayVersion)"
        }
        else {
            WriteLog "Registry verification: Application successfully removed from installed programs"
        }

        # Check if services still exist
        $remainingServices = @()
        foreach ($service in $services) {
            $serviceObj = Get-Service -Name $service -ErrorAction SilentlyContinue
            if ($serviceObj) {
                $remainingServices += $service
                WriteLog "WARNING: Service '$service' still exists after uninstall (Status: $($serviceObj.Status))"
            }
        }
        
        if ($remainingServices.Count -eq 0) {
            WriteLog "Service verification: All agent services successfully removed"
        }

        # Check for common installation directories
        $commonPaths = @(
            "${env:ProgramFiles}\Field Effect",
            "${env:ProgramData}\Field Effect"
        )
        
        $remainingPaths = @()
        foreach ($path in $commonPaths) {
            if (Test-Path $path) {
                $remainingPaths += $path
                WriteLog "WARNING: Installation directory still exists: $path"
            }
        }
        
        if ($remainingPaths.Count -eq 0) {
            WriteLog "Directory verification: All installation directories successfully removed"
        }

        # Overall verification summary
        if (-not $remainingInstall -and $remainingServices.Count -eq 0 -and $remainingPaths.Count -eq 0) {
            WriteLog "UNINSTALL VERIFICATION: SUCCESS - Agent completely removed"
        }
        else {
            $issues = @()
            if ($remainingInstall) { $issues += "registry entry" }
            if ($remainingServices.Count -gt 0) { $issues += "$($remainingServices.Count) service(s)" }
            if ($remainingPaths.Count -gt 0) { $issues += "$($remainingPaths.Count) directory(ies)" }
            
            WriteLog "UNINSTALL VERIFICATION: PARTIAL - Some components remain: $($issues -join ', ')"
            WriteLog "Manual cleanup may be required for complete removal"
        }
    }
    catch {
        WriteLog "UninstallAgent function failed: $_"
        throw
    }
}
##********************************************************************************************************
##
##    FUNCTION PostScript
##    Checks for network access to the agent install Identity Server, calls CheckInstall to verify agent 
##    status, and verifies service is running.
##    Returns nothing
##
##********************************************************************************************************
Function PostScript 
{ 
    #check for access to epid
    $epid = TNC -ComputerName epid.fieldeffect.net -Port 443
    if ([boolean]$epid.TcpTestSucceeded) {
        Writelog "Connection to Identitiy Server looks good"
    } else {
        Writelog "Cannot ping Identity Server"
    }

    # Check installation and wait for activation
    $installCheck = CheckInstall -WaitForActivation -TimeoutMinutes 5
        if ($installCheck.IsInstalled -and $installCheck.IsActive) {
            Write-Host "Agent is installed and active"
        } elseif ($installCheck.TimeoutOccurred) {
            Write-Host "Agent is installed but activation checking timed out with status $($installCheck.CurrentStatus)"
        } else {
            Write-Host "Installation check failed: $($installCheck.ErrorMessage)"
        }

    #check SC query services running
    try {
        $covagent= & 'sc.exe' queryex CovAgent
        $covservice= & 'sc.exe' queryex "Covalence Endpoint Service"
    } catch{write-host "Access to sc.exe denied"}
    if ($covagent -like '*FAILED*' -or $covservice -like '*FAILED*'){
        Writelog "Failed to detect MDR service running after install"
    } else {
        Writelog "Detected MDR services running"
    }
}

#######################################################################################################################
## MAIN   
##
#######################################################################################################################
function main 
{
    if($uninstall) {
        UninstallAgent
    }

    else {
        #run pre-flight checks
        $org_id, $use_license, $client_valid = Prescript 

        #check for exsiting agent
        $installCheck = CheckInstall
            if ($installCheck.IsInstalled) {
                Write-Host "Agent is installed - Version: $($installCheck.AgentVersion), Status: $($installCheck.CurrentStatus)"
            } else {
                Write-Host "Agent is not installed: $($installCheck.ErrorMessage)"
            }

        $org_id = GetOrgid $use_license $client_valid $org_id

        DownloadInstaller $org_id

        $license_key = GetLicense $use_license

        InstallAgent $license_key

        PostScript
    }
}

main

Was this article helpful?

That’s Great!

Thank you for your feedback

Sorry! We couldn't be helpful

Thank you for your feedback

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article