Add Checks to GFI MAX using powershell

(Updated and superseded by a new version.)

GFI MAX is a great remote monitoring system that we use with our clients. It is quite easy to set up, but one feature is still missing: Automatic detection of changes on a monitored device. To work around this I have developed an approach based on powershell scripts.

This article is written for technicians that are intimately familiar with GFI MAX. My approach is based on using a powershell script to modify the agents configuration files directly and then restarting the agent service to have the modified files loaded. This is a shotgun approach that may create a lot of extra work for you if you aren’t very careful. I will show you how to do it, but I will take no responsibility for the damage and wasted hours it may cause you.

Updated

I have learned a lot about automating MAXfocus (GFI MAX has changed name) since this post. You should begin with this article instead. The link to the script in this article has been updated to point to a new version.

How To Add Checks to GFI MAX using Powershell

In this How To I will try to show you how you can create a powershell script that will detect Windows services you need to monitor on a server. To get started a script need to locate where the configuration files are. The approach I prefer is to get the location of the GFI MAX executable through WMI. On top I setup the script to receive a few options as parameters. If you run the script without parameters it will do nothing. If you use “-WinServiceCheck All” it will try to figure out wich services you want monitored and fail the script if they aren’t. If you do “-Apply -WinServiceCheck All” it will even add the missing checks. Be careful if you wish to use “-Replace”. It will delete any existing Windows Service Checks before the script adds any new services it can find. That may be fine for a one-off running of the script, but as I will show later you may want to run this script as a Daily Script Check. You do not want to use “-Replace” with a script check!

I have published a complete script (GFIMAX_Autoconfig.ps1) on GitHub. WordPress does some crazy things with my poewrshell code at times, so you may want to look at the original if you cannot get my code to work.

Settings

## SETTINGS
# A few settings are handled as parameters 
param ( 
    [switch]$Apply = $false, # -Apply will write new checks to configfiles and reload [email protected]
    [switch]$Replace = $false, # Automated task only: -Replace will dump any existing checks if -Apply is used
    [string]$WinServiceCheck = 'None' # 'All' or 'Default'. 
)

## SETUP ENVIRONMENT
# Find 'Advanced Monitoring Agent' service and use path to locate files
$gfimaxagent = Get-WmiObject Win32_Service | Where-Object { $_.Name -eq 'Advanced Monitoring Agent' }
$gfimaxexe = $gfimaxagent.PathName
$gfimaxpath = Split-Path $gfimaxagent.PathName.Replace("'",'') -Parent
$gfimaxpath = $gfimaxpath + /
$247file = $gfimaxpath + '247_Config.xml'
$DSCfile = $gfimaxpath + 'DSC_Config.xml'
$AgentFile = $gfimaxpath + 'agentconfig.xml'
$DeviceFile = $gfimaxpath + 'Config.xml'
$IniFile = $gfimaxpath + 'settings.ini'
$ConfigChanged = $false
$settingsChanged = $false

# We need an array of hashes to remember which checks to add
$New247Checks = @()

Is is supereasy to query WMI from powershell. The first line gets us a powershell object of type Win32_service with all the properties of the actual Windows service filled in. If you have an GFI MAX running on your workstation you may copy/paste this code to a powershell prompt. Enter “$gfimaxagent” on the interaktive prompt + hit enter. You will see all the properties of a windows service listed.

I also set up a few useful variables for later. It is easier to explain them when I use them later on.

The only config file we need for working with Windows Service Checks are the 247_Config.xml. Powershell is a very powerful XML handler. All we need to do is to read the file with Get-Content into a variable we declare as type XML. The ELSE clause is just to set up an empty configuration file if the 247_Config file does not exist yet.

Important: We need to make sure the GFI MAX agent knows a configuration file has been modified when it starts. You do this by setting an attribute named “modified” to 1 on every element you modify. As the Document Element is the parent of all check elements, you need to mark it as modified, too. Remember, for now you are just working with your in memory copy of the configuration file. You do not modify the actual config file until you write it to disk. That is why you mark it as modified straight away. No need to wait. It also has the added functionality of making sure the Document Element is treated as a proper XmlNode by powershell. A completely empty Document Element (It would look like this: <checks/>) is just an XmlElement. If you tried to add checks to it powershell would cast an exception. When you set an attribute straight away you do not need to handle this situation later in your script.

Read configuration of checks

# Read configuration of checks
If (Test-Path $247file)
{
    [xml]$247_Config = Get-Content $247file
    $247_Config.DocumentElement.SetAttribute('modified','1')
}
Else
{
    $247_config = New-Object -TypeName XML
    $decl = $247_Config.CreateXmlDeclaration('1.0', 'ISO-8859-1', $null)
    $rootNode = $247_Config.CreateElement('checks')
    $result = $rootNode.SetAttribute('modified', '1')
    $result = $247_Config.InsertBefore($decl, $247_Config.DocumentElement)
    $result = $247_Config.AppendChild($rootNode)
    $uid = 1
}

Do you see the UID variable I initiate in the ELSE clause? It is very important. All checks need an unique ID. It is very easy to mess this up when you try to modify a configuration file programmatically. I have messed it up several times, and the results are very frustrating. In the dashboard you get checks with wrong names and wrong content as the dashboard side code try to make sense of the garbage you have unintentionally forced the agent to upload. In the beginning I did not have any verification code for UID at all. I only parsed the INI file and picked up the NEXTCHECKUID from it. That is not enough. You soon get into trouble with this as you have to add the script itself to an agent from the dashboard. And the script needs a UID, too. You cannot know for sure that the agent have had time to update the INI file correctly before your scripts reads it and begins messing with configuration files of a running process – a process that starts the script, no less.

So make sure you include something like this in your code. When I show you new functions later on you should add them below this one in your code.

Various Functions

## VARIOUS FUNCTIONS
# Return an array of values from an array of XML Object
function Get-GFIMAXChecks ($xmlArray, $property)
{
    $returnArray = @()
    foreach ($element in $xmlArray)
    {
        If ($element.$property -is [System.Xml.XmlElement])
        {
            $returnArray += $element.$property.InnerText
        }
        else
        {
            $returnArray += $element.$property
        }
    }
    If ($returnArray) {
        Return $returnArray
    }
}
# Get next available UID from INI file
# $uid = [int]$settingsContent['GENERAL']['NEXTCHECKUID']
# Ini file cannot be trusted if script checks are being used
$MaxUid = get-gfimaxchecks @($($247_Config.checks.ChildNodes | select uid), $($DSC_Config.checks.ChildNodes| select uid)) 'uid' | measure -Maximum
$uid = $MaxUid.Maximum + 1

Powershell sure is powerful! That code just parsed a complete XML subtree, located all uid attributes, read their value and picked the highest one. You may put the function on top in your script file. It is very useful, because it will return the value of a specific property even if the value is encoded as CDATA. Some values in the configuration file is encoded as CDATA, others are not. Use this function and you do not have to know which it is.

Which Windows services do we monitor?

First you need to get the name of all Windows Services that are already monitored. Like I said earlier, powershell handles XML very well. Then you check which Windows service option you have passed the script, “All” or “Default”. The distinction is important, as the whole point is to filter out any services that we do not care about or that may cause failed checks that do not require an action. Any Windows server has serveral services configured to start automatically, but will stop after a short time when it is clear that it does not have a job to do. In this case I have already filtered out Google Update and Adobe update service. You may know of more services you do not want included.

A list of services you do not want to monitor

$DoNotMonitorServices = @( # Services you do not wish to monitor, regardless
    'gpupdate', # Google Update Service. Does not always run.
    'AdobeARMservice' # Another service you may not want to monitor
    )

Default

When you install GFI MAX agent manually you may have noticed that it will suggest a few Windows services to monitor in the configuration wizard. GFI MAX has included a list of services to include by default (if they exist) in a special configuration file “services.ini”. If you choose the “Default” option any service will be included only if it is listed in this file. To do this we first need to parse the .ini file. I use a function to do that. I found the function on TechNet, but I modified it to use ordered list. That way the original order of the ini-file will be kept if we have to modify it and write it back. Put it on top of your script. You will see how to use the output in a moment.

Get-IniContent

# Downloaded from 
# http://blogs.technet.com/b/heyscriptingguy/archive/2011/08/20/use-powershell-to-work-with-any-ini-file.aspx
# modified to use ordered list by me
function Get-IniContent ($filePath)
{
    $ini = New-Object System.Collections.Specialized.OrderedDictionary
    switch -regex -file $FilePath
    {
        '^[(.+)]' # Section
        {
            $section = $matches[1]
            $ini[$section] = New-Object System.Collections.Specialized.OrderedDictionary
            $CommentCount = 0
        }
        '^(;.*)$' # Comment
        {
            $value = $matches[1]
            $CommentCount = $CommentCount + 1
            $name = 'Comment' + $CommentCount
            $ini[$section][$name] = $value
        } 
        '(.+?)s*=(.*)' # Key
        {
            $name,$value = $matches[1..2]
            $ini[$section][$name] = $value
        }
    }
    return $ini
}

Read the configuration files:

Read ini-files

# Read ini-files
$settingsContent = Get-IniContent($IniFile)
$servicesContent = Get-IniContent($gfimaxpath + 'services.ini')

All

In this case I am trying to detect services that we do not know the name of. Plenty of services that are important have custom names that cannot be predicted. Named SQL server instances and Axapta services (Microsoft Dynamics) to name but two. These two are perfect examples, because this is business critical services that we really would like to get alerted if they stopped. The approach I use is to split my filter in two:

  • Any services where the executable is located below the Windows directory ($env:systemroot). In this case I filter against the services.ini file. I am putting my faith in the GFI MAX team for all system services.

  • Any services where the executable is located outside the Windows directory. In this case I only filter against the $DoNotMonitorServices list I have created myself.

In this way I hope to include any important software added to a server without getting spammed by failed Windows Service Checks for irrelevant services. Try it on a few servers. It actually works quite well.

The main part

Now for the code that does the actual work locating installed services and filter them against monitored services and deciding whether we want to monitor them or not. Any new services are added to an array of named hashes. It is easier to add any XML in a batch later on.

WinServiceCheck

## WINSERVICECHECK

# First extract all servicenames from current configuration
$MonitoredServices = @()
$MonitoredServices = Get-GFIMAXChecks $247_Config.checks.winservicecheck 'servicekeyname'

If ('All', 'Default' -contains $WinServiceCheck)
{
    If ($Replace)
    {
        Foreach ($xmlCheck in $247_config.checks.WinServiceCheck) 
        {
            $null = $247_Config.checks.RemoveChild($xmlCheck) 
        }
        $MonitoredServices = @()
    }
    # An array to store names of services to monitor
    $ServicesToAdd = @()
    $ServicesToMonitor = @()

    ## SERVICES TO MONITOR
    If ($WinServiceCheck -eq 'Default') # Only add services that are listed in services.ini
    { 

        # Get all currently installed services with autostart enabled from WMI
        $autorunsvc = Get-WmiObject Win32_Service |  
        Where-Object { $_.StartMode -eq 'Auto' } | select Displayname,Name

        Foreach ($service in $autorunsvc) 
        {
            If ($servicesContent['SERVICES'][$service.Name] -eq '1')
            {
                $ServicesToMonitor += $service.Name
            }
        }
    } 
    Else
    { 
        # Add all services configured to autostart if pathname is outside %SYSTEMROOT%
        # if the service is currently running
        $autorunsvc = Get-WmiObject Win32_Service | 
        Where-Object { $_.StartMode -eq 'Auto' -and $_.PathName -notmatch ($env:systemroot -replace '"', '') -and $_.State -eq 'Running'} | select Displayname,Name 
        Foreach ($service in $autorunsvc) 
        {
            If ($DoNotMonitorServices -notcontains $service.Name)
            {
                $ServicesToMonitor += $service.Name
            }
        }

        # Add all services located in %SYSTEMROOT% only if listed in services.ini
        $autorunsvc = Get-WmiObject Win32_Service |
        Where-Object { $_.StartMode -eq 'Auto' -and $_.PathName -match ($env:systemroot -replace '"', '') } | select Displayname,Name
        Foreach ($service in $autorunsvc) 
        {
            If ($servicesContent['SERVICES'][$service.Name] -eq '1')
            {
                $ServicesToMonitor += $service.Name
            }
        }
    }

    ## SERVICES TO ADD
    Foreach ($service in $ServicesToMonitor)
    {
        If (!($MonitoredServices -contains $service))
        {
            $ServicesToAdd += $service
        }
    }

    # Get a complete list of all Displaynames of services
    $autorunsvc = Get-WmiObject Win32_Service | select Displayname,Name
    Foreach ($NewService in $ServicesToAdd)
    {
        $New247Checks += @{ 'checktype' = 'WinServiceCheck'; 
                         'servicename' = ($autorunsvc | Where-Object { $_.Name -eq $NewService }).DisplayName;
                         'servicekeyname' = $NewService;
                         'failcount' = 1; # How many consecutive failures before check fails
                         'startpendingok' = 0; # Is Startpending OK, 1 0 Yes, 0 = No
                         'restart' = 1; # Restart = 1 (Restart any stopped service as default)
                         'consecutiverestartcount' = 2; # ConsecutiveRestartCount = 2 (Fail if service doesnt run after 2 tries)
                         'cumulativerestartcount' = '4|24' } # Cumulative Restart Count = 4 in 24 hours
    }
}

Add new checks as XML

If your code has done its job you should have an array of Windows Service Checks to add to the configuration file. Let us do just that. You’ll notice that I start by checking if there are any elements in the $New247Checks array. I do that by checking if element zero exist. If it does there are at least one check that needs to be added.

Add Named Hashes as XML checks

If($New247Checks[0]) 
{
    Foreach ($Check in $New247Checks)
    {
        $xmlCheck = $247_Config.CreateElement($Check.get_Item('checktype'))
        $xmlCheck.SetAttribute('modified', '1')
        $xmlCheck.SetAttribute('uid', $uid)
        $uid++ # Increase unique ID identifier to keep it unique

        Foreach ($property in $Check.Keys)
        {
            If ($property -ne 'checktype') 
            {
                $xmlProperty = $247_Config.CreateElement($property)
                $propertyValue = $Check.get_Item($property)
                If ([bool]($propertyValue -as [int]) -or $propertyValue -eq '0') # Is this a number?
                { # If its a number we just dump it in there
                    $xmlProperty.set_InnerText($propertyValue)
                }
                Else
                { # If it is text we encode it in CDATA
                    $rs = $xmlProperty.AppendChild($247_Config.CreateCDataSection($propertyValue))
                }
                # Add Property to Check element
                $rs = $xmlCheck.AppendChild($xmlProperty)
            }
        }
        # Add Check to file in check section
        $rs = $247_Config.checks.AppendChild($xmlCheck)

    }
    $247_Config.checks.SetAttribute('modified', '1')
    $ConfigChanged = $true
}

I will be the first to admit that reading and searching XML using powershell is a heck of a lot easier than having to create new XML code. This is why I opted to add any new checks to an array of named hashes first and then loop through the array at the end to convert it into valid XML for our GFI MAX agent to read. I also make double sure that our config file knows it is modified. That is redundant code as I did that when I read the file. But sometimes scripts get modified plenty of times during the time we use them, so better safe than sorry.

You may notice that I know change a variable I created right on top of this article. At this point it is clear that your configuration has changed and you will need to deal with that in a way. Probably by writing some configuration files to disk and restarting a service. This is the dangerous part. Lets get started!

Updating configuration files and restarting an agent

The logic I have made you follow so far has created a situation where your script knows that you have changed a configuration file. Now it has to decide what to do with it. Should it write changes to disk or only report them to the dashboard? You make that decision up front by supplying an “-Apply” parameter or not. At this point you will have to decide how you intend to run this script.

Run script as an Automated Task

This script is perfect for an automated task. Any automated task may run on any schedule you prefer. You may start it manually on occasion, run it daily, weekly or montly. A really crazy person might run it at the event of a Windows Service Check failing using the “-Replace” option. Then the script would remove any checks for a service that do no longer exist.

But (and this is unfortunately a big but) Automated tasks can only be added one by one on a single device for servers. Only workstations have site Automated Tasks. I my case we have several hundred servers we monitor. I cannot add Automated tasks to all of them manually. So I do some crazy things to make the script work as a Daily script check.

Run script as a Daily Script Check

I am not sure if you understand straight away why it is a bad idea to let a script running as a Daily Safety Check restart the monitoring agent? Then let me point out that the script will suddenly and without warning kill the agent which is in the middle of collecting Daily Safety Check data and uploading them to GFI. You will not get any DSC data this day. So we will have to add code to make sure Daily Safety Checks get run immediately upon restart of the agent. You will have to add a function to write .ini files back to disk, a section to check if the script is now running for the second time in a row because the agent has restarted and a section to make sure Daily Safety Checks get rerun at least once whenever the configuration files has changed.

Ok, so now I have established that running the script as a check is a bad idea. At least if you intend to let it modify any checks. Then why do it? Because you can add “Checks like this” to as many devices as you like in one go! That is one powerful “why” when you have several hundred servers to monitor! So let me explain how you can do it.

First a write .ini file function

TechNet is great! I only needed to make sure the .ini file is overwritten everytime. Add it to the top of the script as always.

# Downloaded from 
# http://blogs.technet.com/b/heyscriptingguy/archive/2011/08/20/use-powershell-to-work-with-any-ini-file.aspx
# Modified to force overwrite by me
function Out-IniFile($InputObject, $FilePath)
{
    $outFile = New-Item -ItemType file -Path $Filepath -Force
    foreach ($i in $InputObject.keys)
    {
        if ('Hashtable','OrderedDictionary' -notcontains $($InputObject[$i].GetType().Name))
        {
            #No Sections
            Add-Content -Path $outFile -Value '$i=$($InputObject[$i])'
        } else {
            #Sections
            Add-Content -Path $outFile -Value '[$i]'
            Foreach ($j in ($InputObject[$i].keys | Sort-Object))
            {
                if ($j -match '^Comment[d]+') {
                    Add-Content -Path $outFile -Value '$($InputObject[$i][$j])'
                } else {
                    Add-Content -Path $outFile -Value '$j=$($InputObject[$i][$j])' 
                }

            }
            Add-Content -Path $outFile -Value ''
        }
    }
}

Check if the script has already been run today

You will have to get this right. If you do not your script will cause an endless loop of the agent starting, then the script runs, kills the agent, the agent starts, runs the script and gets killed again. Since you are smarter than me this will not happen to you. Because you will include this (or run your script as an automated task)

Check current time against last runtime

# First of all, check if it is safe to make any changes
If ($Apply)
{
    # Make sure a failure to aquire settings correctly will disable changes
    $Apply = $false
    If ($settingsContent['DAILYSAFETYCHECK']['RUNTIME']) # This setting must exist
    {
        $lastRuntime = $settingsContent['DAILYSAFETYCHECK']['RUNTIME']
        [int]$currenttime = $((Get-Date).touniversaltime() | get-date -UFormat %s) -replace ',','.' # Handle decimal comma 
        $timeSinceLastRun = $currenttime - $lastRuntime
        If($lastRuntime -eq 0 -or $timeSinceLastRun -gt 360)
        {
            # If we have never been run or it is at least 6 minutes ago
            # enable changes again
            $Apply = $true
        }
    }
    If (!($Apply))
    {
        Write-Host 'CHANGES APPLIED - Verifying changes:'
    }
}

Unless the last runtime of DSC is at least 6 minutes ago, the script will not make any changes. Why 6 minutes? Debugging. You can easily set the interval higher – and you probably should, but as I expect you to test your script on a single server before you add it to many more you must be able to let the script be rerun within a reasonable time. At least we avoid an endless loop this way.

Prettify output

One last function to add to make sure a reporting only session will be readable in the dashboard. I promise I will give you access to a complete script at the end. You may notice that this function is able to handle many more checks than Windows Service checks. So does the complete script. Be careful with it.

Make missing checks readable

# Small function to give missing checks output some structure
function Format-Output($CheckTable)
{
    $Result = @()
    Foreach ($CheckItem in $CheckTable)
    {
        Switch ($CheckItem.checktype)
        {
            {'DriveSpaceCheck','DiskSpaceChange' -contains $_ }
                { $Result += $CheckItem.checktype + '  ' + $CheckItem.driveletter }
            'WinServicecheck'
                { $Result += $CheckItem.checktype + ' ' + $CheckItem.servicename }
            'PerfCounterCheck'
            { Switch ($CheckItem.type)
                {
                    '1' { $Result += $CheckItem.checktype + ' Processor Queue Length'}
                    '2' { $Result += $CheckItem.checktype + ' Average CPU Usage'}
                    '3' { $Result += $CheckItem.checktype + ' Memory Usage'}
                    '4' { $Result += $CheckItem.checktype + ' Network Interface ' + $CheckItem.instance}
                    '5' { $Result += $CheckItem.checktype + ' Physical Disk ' + $CheckItem.instance}
                }
            }
            {'PingCheck','AVUpdateCheck','BackupCheck','FileSizeCheck' -contains $_ }
                { $Result += $CheckItem.checktype + ' ' + $CheckItem.name }
            default
                { $Result += $CheckItem.checktype }

        }

    }
    $Result
}

Write changes to disk

Now you will:

  • Check if there are any changes you wish to make ($ConfigChanged = $true)

  • Check if changes should be applied ($Apply = $true)

  • If Apply = $true; stop agent, update ini files, update xml files, start agent

  • If Apply = $false; report any changes you wish to make, but complain you are not allowed to

The code that writes to disk

If($ConfigChanged)
{ 
    If ($Apply)
    {
        # Update last runtime to prevent changes too often
        [int]$currenttime = $(get-date -UFormat %s) -replace ',','.' # Handle decimal comma 
        $settingsContent['DAILYSAFETYCHECK']['RUNTIME'] = $currenttime

        # Clear lastcheckday to make DSC run immediately
        $settingsContent['DAILYSAFETYCHECK']['LASTCHECKDAY'] = '0'

        # Save updated NEXTCHECKUID
        $settingsContent['GENERAL']['NEXTCHECKUID'] = $uid

        # Stop agent before writing new config files
        Stop-Service $gfimaxagent.Name

        # Save all config files
        $247_Config.Save($247file)
        $DSC_Config.Save($DSCfile)
        Out-IniFile $settingsContent $IniFile

        # Start monitoring agent again
        Start-Service $gfimaxagent.Name

        # Write output to Dashboard
        Write-Host 'CHANGES APPLIED:'
        If ($New247Checks) 
        {
            Write-Host 'Added the following 24/7 checks to configuration file:'
            Format-Output $New247Checks 
        }
        If ($NewDSCChecks) 
        {
            Write-Host 'Added the following Daily Safety checks to configuration file:'
            Format-Output $NewDSCChecks 
        }
        If ($settingsChanged) { Write-host 'Updated INI-file with updated settings.'}
        exit 1001 # Internal status code: Changes has been implemented.
    }
    Else
    {
        Write-Host 'SUGGESTED CHANGES:'
        If ($New247Checks) 
        {
            Write-Host '`n-- 24/7 check(s):'
            Format-Output $New247Checks 
        }
        If ($NewDSCChecks) 
        {
            Write-Host '`n-- Daily Safety check(s):'
            Format-Output $NewDSCChecks 
        }
        If ($settingsChanged) { Write-host '`n-- Update INI-file with default settings.'}
        If ($ReportMode)
        {
            Exit 0 # Needed changes have been reported, but do not fail the check
        }
        Else
        {
            Exit 1000 # Internal status code: Suggested changes, but nothing has been touched
        }
    }
}
Else
{
    # We have nothing to do. This Device has passed the test!
    Write-Host 'CHECKS VERIFIED - Result:'
    If ($Performance)       { Write-Host 'Performance Monitoring checks verified: OK'}
    If ($DriveSpaceCheck)   { Write-Host 'Disk usage monitored on all harddrives: OK'}
    If ($WinServiceCheck)   { Write-Host 'All Windows services are now monitored: OK'}
    If ($DiskSpaceChange)   { Write-Host 'Disk space change harddrives monitored: OK'}
    If ($PingCheck)         { Write-Host 'Pingcheck Router Next Hop check tested: OK'}
    If ($MSSQL)             { Write-Host 'SQL servers are not missing any checks: OK'}
    If ($SMART)             { Write-Host 'Physical Disk Health monitoring tested: OK'}
    If ($Backup)            { Write-Host 'Unmonitored Backup Products not found: OK'}
    If ($Antivirus)         { Write-Host 'Unmonitored Antivirus checks verified: OK'}
    Write-Host 'All checks verified. Nothing has been changed.'
    Exit 0 # SUCCESS
}

Conclusion

It isn’t necessarily easy, but it is definitely possible to make a GFI MAX agent configure itself to better monitor its host device using powershell. I have shown you how to do it for Windows Service Checks. As a conclusion I will post a complete script that does a lot more than just Windows Service checks. Use it with care! It is a very powerful tool if used right, but letting a script modify files and restart processes on servers should be approached with respect. Remember, YOU are responsible for what you use it for. It is not endorsed by GFI MAX in any way, I am just a regular client.

Verify-MAXfocusConfig.ps1 on GitHub