Script PowerShell d'installation de Windows pour RMM/MDM

Introduction

Le script PowerShell suivant peut être utilisé pour les déploiements RMM et MDM et gère l'installation/désinstallation de notre agent de point de terminaison MDR Field Effect pour Windows. Le script ci-dessous tire parti de nos appels API pour créer un processus simplifié et automatisé pour les utilisateurs qui tirent parti d'une solution RMM/MDM, ainsi que pour créer les fichiers journaux nécessaires dont notre équipe d'assistance pourrait avoir besoin pour de futurs dépannages ou enquêtes. 


Procédure

Vous aurez besoin d'un jeton de portail MDR valide pour autoriser les appels d'API Field Effect. Pour obtenir de l'aide sur la création de votre jeton, consultez Accès à la documentation de l'API de Field Effect


Commencez par enregistrer le texte ci-dessous sous forme de fichier .ps1 nommé « agent_install_script.ps1 ».

 

Une fois enregistré, utilisez votre agent_install_script.ps1  nouvellement créé dans votre solution RMM ou MDM. 


Étant donné qu'il existe de nombreux outils différents et que la complexité de chaque déploiement varie d'une entreprise à l'autre, le processus d'ajout à votre outil RMM/MDM varie d'une organisation à l'autre.


<#

.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

.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

.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-08: Changed endpoint API lookup to no longer max out at 1000 records.
    2024-09-05: Added deployment check option.
    2025-03-05: Updated zip bundle calls
#>

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

$version="v7.1"
$win_arch = 64
$mdr_agent_installed = $false
$agent_installer_filename = "x" #look it up
$use_license = $false
$client_valid = $false

# 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"

# 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
##    Passed a bool, checks agent status in status.json file.
##    If bool parameter is TRUE, confirms activation by waiting for status to change to ACTIVE
##    Returns success bool.
##
##********************************************************************************************************
function CheckInstall 
{
param (
    [Parameter(Mandatory = $false)][boolean] $_confirm
    )
        
    # Check if mdrAgent is already installed
    $mdr_agent_installed = Test-Path "C:\ProgramData\Field Effect\Covalence\data\status.json"

    if ($mdr_agent_installed) {
        $status_json = Get-Content -Path "C:\ProgramData\Field Effect\Covalence\data\status.json" | ConvertFrom-Json
        WriteLog "Checking status of installed agent " $status_json.AgentInfo.AgentVersion  
        WriteLog $status_json.AgentInfo.StateName  
    } 
    
    if ($_confirm) {
        
        if(-not ($mdr_agent_installed)){ 
            $log_err="Could not find status.json file at C:\ProgramData\Field Effect\Covalence\data\ to validate installation"
            Writelog $log_err
            throw $log_err
        } 

        WriteLog "Polling for active status for installed agent" 
        $i = 1
        while($i -le 40) {
            $status_json = Get-Content -Path "C:\ProgramData\Field Effect\Covalence\data\status.json" | ConvertFrom-Json
            $agent_status = $status_json.AgentInfo.StateName
            if ($agent_status -like "ACTIVE") {
                WriteLog "Installed agent status set to $agent_status"
                $i=99
            } else {
                $i++     
                Start-Sleep -Seconds 10
            }    
        }
        if ($agent_status -notlike "ACTIVE") {
            WriteLog "Could not confirm agent status: $agent_status"
        }
    }

    return $mdr_agent_installed
}

##********************************************************************************************************
##
##    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
   
    # Download zip file    
    $installer_bundle_response = Invoke-RestMethod -Method Post -Uri "https://services.fieldeffect.net/v1/endpoint_installer_bundles" -Body $installer_body -Headers $headers -ContentType application/json
    $zipfile = Join-Path $working_folder $installer_bundle_response.file_name
    $msi = New-Object net.webclient
    $msi.Downloadfile($installer_bundle_response.download_link, $zipfile)
    WriteLog "Downloaded installer archive to $zipfile"

    # Expand the zip file to the installer folder
    Expand-Archive -Path $zipfile -DestinationPath (Join-Path $working_folder $installer_bundle_response.file_name.Substring(0, $installer_bundle_response.file_name.Length - 4)) -Force
    WriteLog "Extracted archive to $working_folder"

    # Delete the zipfile
    Remove-Item $zipfile
    WriteLog "Cleaning up files: Deleted archive"

    # Set final directory of msi installer file
    $script:installer_path = Join-Path $working_folder $installer_bundle_response.file_name.Substring(0, $installer_bundle_response.file_name.Length - 4)
    $script:installer_filename=Get-ChildItem "$script:installer_path" -Filter *.msi
    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")){
        $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 {

    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 Covalence installation found."
        return
    }

    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) {
        try {
            WriteLog "Stopping service: $service..."
            & 'sc.exe' stop $service
        }
        catch {
            WriteLog "Failed to stop $service : $_"
        }
    }

    # Use universal guid to uninstall
    try {
        WriteLog "Running uninstall"
        Start-Process -FilePath 'msiexec.exe' -Wait -ArgumentList '/x', (New-Object -ComObject WindowsInstaller.Installer).RelatedProducts('{d6b2aca4-07e2-4b97-a7f8-d9cb64cb25a8}')[0], '/qn'
        WriteLog "Uninstall... [OK]"
    }
    catch {
        WriteLog "Failed to uninstall the agent"
        throw "Uninstall failed"
    }
}

##********************************************************************************************************
##
##    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"
    }

    #clean up files

    #check status.json
    CheckInstall $true

    #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
        $null= CheckInstall

        $org_id = GetOrgid $use_license $client_valid $org_id

        DownloadInstaller $org_id

        $license_key = GetLicense $use_license

        InstallAgent $license_key

        PostScript
    }
}

main

Cet article a-t-il été utile ?

C'est super !

Merci pour votre commentaire

Désolé ! Nous n'avons pas pu vous être utile

Merci pour votre commentaire

Dites-nous comment nous pouvons améliorer cet article !

Sélectionner au moins l'une des raisons
La vérification CAPTCHA est requise.

Commentaires envoyés

Nous apprécions vos efforts et nous allons corriger l'article