How to Automate Microsoft Safety Scanner Using a PowerShell Script

In today’s security-conscious IT landscape, ensuring systems remain free from malware and other threats is a top priority for IT professionals and Managed Service Providers (MSPs). While several tools are available to accomplish this, automating threat detection and response can significantly reduce the time and effort required to maintain a secure environment.

One such tool that can be integrated into an automated workflow is the Microsoft Safety Scanner (MSERT). In this post, we’ll explore a PowerShell script that automates the downloading, running, and reporting of MSERT, making it easier for IT professionals to keep their systems safe.

Background

Microsoft Safety Scanner is a free, on-demand scanning tool designed to detect and remove malware from Windows systems. It is updated frequently and is meant to be used in environments where the latest security definitions are required but where an always-on solution may not be feasible.

The script we’ll discuss in this post streamlines the process of using MSERT by automating its download, execution, and result handling. This is particularly useful in environments where regular scans are necessary but manual intervention is impractical.

For IT professionals and MSPs, the ability to automate this process reduces the risk of human error, ensures consistency in how scans are run, and frees up valuable time for other tasks. This script is a powerful tool in maintaining a secure IT environment with minimal effort.

The Script:

#Requires -Version 5.1

<#
.SYNOPSIS
    Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field.
.DESCRIPTION
    Run the Microsoft Safety Scanner, collect the results, and optionally save the results to a multiline custom field.
.EXAMPLE
    (No Parameters)
    
    Downloading MSERT from https://go.microsoft.com/fwlink/?LinkId=212732
    Waiting for 3 seconds.
    Download Attempt 1
    Download Successful!
    Initiating Scan
    Exit Code: 7
    [Critical] Infections found!

    ---------------------------------------------------------------------------------------
    Microsoft Safety Scanner v1.405, (build 1.405.445.0)
    Started On Thu Feb 22 13:33:34 2024

    Engine: 1.1.24010.10
    Signatures: 1.405.445.0
    MpGear: 1.1.16330.1
    Run Mode: Scan Run in Quiet Mode

    Quick Scan Results:
    -------------------
    Threat Detected: Virus:DOS/EICAR_Test_File, not removed.
        Action: NoAction, Result: 0x00000000
            file://C:\Windows\system32\eicarcom2.zip->eicar_com.zip->eicar.com
                SigSeq: 0x00000555DC2DDDB0
            file://C:\Windows\system32\eicar.com
                SigSeq: 0x00000555DC2DDDB0
            file://C:\Windows\eicar.com
                SigSeq: 0x00000555DC2DDDB0
            containerfile://C:\Windows\system32\eicarcom2.zip

    Results Summary:
    ----------------
    Found Virus:DOS/EICAR_Test_File, not removed.
    Successfully Submitted MAPS Report
    Successfully Submitted Heartbeat Report
    Microsoft Safety Scanner Finished On Thu Feb 22 13:35:58 2024


    Return code: 7 (0x7)

PARAMETER: -ScanType "Full"
    Specifies the type of scan to perform. "Full" for a complete disk scan, or "Quick" for a scan of common exploit locations.

PARAMETER: -Timeout "ReplaceMeWithANumber"
    Sets a time limit for the scan in minutes. If the scan exceeds this duration, it is canceled, and an error is output. Replace "ReplaceMeWithANumber" with the desired time limit in minutes.

PARAMETER: -CustomField "ReplaceWithNameOfCustomField"
    Specifies the name of the multiline custom field where scan results are optionally saved. Enter the field name to enable this feature.
.OUTPUTS
    None
.NOTES
    Minimum OS Architecture Supported: Windows 10, Server 2016
    Release Notes: Initial Release
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://ninjastage2.wpengine.com/terms-of-use.
    Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms. 
    Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party. 
    Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider. 
    Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations. 
    Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks. 
    Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script. 
    EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
#>

[CmdletBinding()]
param (
    [Parameter()]
    [String]$ScanType = "Quick",
    [Parameter()]
    [Int]$Timeout = 30,
    [Parameter()]
    [String]$CustomField,
    [Parameter()]
    [String]$DownloadURL = "https://go.microsoft.com/fwlink/?LinkId=212732"
)

begin {
    # Set parameters using dynamic script variables.
    if($env:scanType -and $env:scanType -notlike "null"){ $ScanType = $env:scanType }
    if($env:scanTimeoutInMinutes -and $env:scanTimeoutInMinutes -notlike "null"){ $Timeout = $env:scanTimeoutInMinutes }
    if($env:customFieldName -and $env:customFieldName -notlike "null"){ $CustomField = $env:customFieldName }

    # If a timeout is specified, check that it's in the valid range.
    if($Timeout -lt 1 -or $Timeout -ge 120){
        Write-Host "[Error] Timeout must be greater than or equal to 1 minute and less than 120 minutes."
        exit 1
    }

    # If we're not given a scan type, error out.
    if(-not $ScanType){
        Write-Host "[Error] Please select a scan type (Quick or Full)."
        exit 1
    }

    # Check that the scan type is valid.
    switch($ScanType){
        "Quick" { Write-Verbose "Quick Scan Selected!"}
        "Full" { Write-Verbose "Full Scan Selected!" }
        default { 
            Write-Host "[Error] Invalid scan type selected!"
            exit 1
        }
    } 

    # Checks for local administrator rights.
    function Test-IsElevated {
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }

    # Utility function for downloading files.
    function Invoke-Download {
        param(
            [Parameter()]
            [String]$URL,
            [Parameter()]
            [String]$Path,
            [Parameter()]
            [int]$Attempts = 3,
            [Parameter()]
            [Switch]$SkipSleep
        )

        $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType')
        if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) {
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12
        }
        elseif ( $SupportedTLSversions -contains 'Tls12' ) {
            [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
        }
        else {
            # Not everything requires TLS 1.2, but we'll try anyway.
            Write-Warning "TLS 1.2 and or TLS 1.3 are not supported on this system. This download may fail!"
            if ($PSVersionTable.PSVersion.Major -lt 3) {
                Write-Warning "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2."
            }
        }

        $i = 1
        While ($i -le $Attempts) {
            # Some cloud services have rate-limiting
            if (-not ($SkipSleep)) {
                $SleepTime = Get-Random -Minimum 3 -Maximum 15
                Write-Host "Waiting for $SleepTime seconds."
                Start-Sleep -Seconds $SleepTime
            }
        
            if ($i -ne 1) { Write-Host "" }
            Write-Host "Download Attempt $i"

            $PreviousProgressPreference = $ProgressPreference
            $ProgressPreference = 'SilentlyContinue'
            try {
                # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly
                if ($PSVersionTable.PSVersion.Major -lt 4) {
                    # Downloads the file
                    $WebClient = New-Object System.Net.WebClient
                    $WebClient.DownloadFile($URL, $Path)
                }
                else {
                    # Standard options
                    $WebRequestArgs = @{
                        Uri                = $URL
                        OutFile            = $Path
                        MaximumRedirection = 10
                        UseBasicParsing    = $true
                    }

                    # Downloads the file
                    Invoke-WebRequest @WebRequestArgs
                }

                $ProgressPreference = $PreviousProgressPreference
                $File = Test-Path -Path $Path -ErrorAction SilentlyContinue
            }
            catch {
                Write-Warning "An error has occurred while downloading!"
                Write-Warning $_.Exception.Message

                if (Test-Path -Path $Path -ErrorAction SilentlyContinue) {
                    Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue
                }

                $File = $False
            }

            if ($File) {
                $i = $Attempts
            }
            else {
                Write-Warning "File failed to download."
                Write-Host ""
            }

            $i++
        }

        if (-not (Test-Path -Path $Path)) {
            [PSCustomObject]@{
                ExitCode = 1
            }
        }
        else {
            [PSCustomObject]@{
                ExitCode = 0
            }
        }
    }

    # Utility function to help set custom fields
    function Set-NinjaProperty {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            $Value,
            [Parameter()]
            [String]$DocumentName
        )
    
        $Characters = $Value | Measure-Object -Character | Select-Object -ExpandProperty Characters
        if($Characters -ge 10000){
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded, value is greater than 10,000 characters.")
        }
        
        # If we're requested to set the field value for a Ninja document we'll specify it here.
        $DocumentationParams = @{}
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
        
        # This is a list of valid fields that can be set. If no type is given, it will be assumed that the input doesn't need to be changed.
        $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"
        if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type! Please check here for valid types. https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" }
        
        # The field below requires additional information to be set
        $NeedsOptions = "Dropdown"
        if ($DocumentName) {
            if ($NeedsOptions -contains $Type) {
                # We'll redirect the error output to the success stream to make it easier to error out if nothing was found or something else went wrong.
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
        
        # If an error is received it will have an exception property, the function will exit with that error information.
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
        
        # The below type's require values not typically given in order to be set. The below code will convert whatever we're given into a format ninjarmm-cli supports.
        switch ($Type) {
            "Checkbox" {
                # While it's highly likely we were given a value like "True" or a boolean datatype it's better to be safe than sorry.
                $NinjaValue = [System.Convert]::ToBoolean($Value)
            }
            "Date or Date Time" {
                # Ninjarmm-cli expects the  Date-Time to be in Unix Epoch time so we'll convert it here.
                $Date = (Get-Date $Value).ToUniversalTime()
                $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
                $NinjaValue = $TimeSpan.TotalSeconds
            }
            "Dropdown" {
                # Ninjarmm-cli is expecting the guid of the option we're trying to select. So we'll match up the value we were given with a guid.
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID
        
                if (-not $Selection) {
                    throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown")
                }
        
                $NinjaValue = $Selection
            }
            default {
                # All the other types shouldn't require additional work on the input.
                $NinjaValue = $Value
            }
        }
        
        # We'll need to set the field differently depending on if its a field in a Ninja Document or not.
        if ($DocumentName) {
            $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
        }
        else {
            $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1
        }
        
        if ($CustomField.Exception) {
            throw $CustomField
        }
    }
    
    $ExitCode = 0

    # If the log file already exists remove it.
    if(Test-Path -Path "$env:SYSTEMROOT\debug\msert.log"){
        Remove-Item -Path "$env:SYSTEMROOT\debug\msert.log" -Force -ErrorAction SilentlyContinue
    }
}
process {
    # Error out if we don't have local admin permissions.
    if (-not (Test-IsElevated)) {
        Write-Host "[Error] Access Denied. Please run with Administrator privileges."
        exit 1
    }

    # Download MSERT.
    Write-Host "Downloading MSERT from $DownloadURL"
    $MSERTPath = "$env:TEMP\MSERT.exe"
    $Download = Invoke-Download -Path $MSERTPath -URL $DownloadURL
    if($Download.ExitCode -ne 0){
        Write-Host "[Error] Failed to download MSERT please check that $DownloadURL is reachable!"
        exit 1
    }

    Write-Host "Download Successful!"

    # Start the MSERT Scan with the parameters given.
    Write-Host "Initiating Scan"
    $Arguments = New-Object System.Collections.Generic.List[string]
    if($ScanType -eq "Full"){
        $Arguments.Add("/F")
    }
    $Arguments.Add("/Q")
    $Arguments.Add("/N")

    try{
        # Run it with our specified timeout.
        $TimeoutInSeconds = $Timeout * 60
        $MSERTProcess = Start-Process -FilePath $MSERTPath -ArgumentList $Arguments -NoNewWindow -PassThru
        $MSERTProcess | Wait-Process -Timeout $TimeoutInSeconds -ErrorAction Stop
    }catch{
        Write-Host "[Alert] The Microsoft Safety Scanner exceeded the specified timeout of $Timeout minutes, and the script is now terminating."
        $MSERTProcess | Stop-Process -Force
        $TimedOut = $True
        $ExitCode = 1
    }
    Write-Host "Exit Code: $($MSERTProcess.ExitCode)"

    # If the report is missing, something has clearly gone wrong.
    if(-not (Test-Path -Path $env:SYSTEMROOT\debug\msert.log)){
        Write-Host "[Error] The report from MSERT.exe is missing?"
        exit 1
    }

    # Get the contents of the MSERT log and error out if it's blank.
    $Report = Get-Content -Path "$env:SYSTEMROOT\debug\msert.log"
    if(-not $Report){
        Write-Host "[Error] The report from MSERT.exe is empty?"
        exit 1
    }

    # If threats are detected, send out the alert.
    $Report | ForEach-Object {
        if($_ -match "No infection found"){
            $NoInfectionFoundTextPresent = $True
        }

        if($_ -match "Threat Detected" ){
            $ThreatDetectedTextPresent = $True
        }
    }

    
    if(($ThreatDetectedTextPresent -or -not $NoInfectionFoundTextPresent) -and -not $TimedOut){
        Write-Host "[Critical] Infections found!"
    }elseif($ExitCode -ne 1 -and -not $TimedOut){
        Write-Host "[Success] Scan has completed no infections detected."
    }

    # Save to a custom field upon request.
    if($CustomField){
        try {
            Write-Host "Attempting to set Custom Field '$CustomField'."
            Set-NinjaProperty -Name $CustomField -Value ($Report | Out-String)
            Write-Host "Successfully set Custom Field '$CustomField'!"
        }
        catch {
            if($_.Exception.Message){
                Write-Host "[Error] $($_.Exception.Message)"
            }

            if($_.Message){
                Write-Host "[Error] $($_.Message)"
            }

            $ExitCode = 1
        }
    }

    # Send out the report to the activity log.
    $Report | Write-Host

    # Remove the old log file.
    if(Test-Path -Path "$env:SYSTEMROOT\debug\msert.log"){
        Remove-Item -Path "$env:SYSTEMROOT\debug\msert.log" -Force -ErrorAction SilentlyContinue
    }

    # Exit.
    exit $ExitCode
}
end {
    
    
     
}

 

Access over 300+ scripts in the NinjaOne Dojo

Get Access

Detailed Breakdown

The PowerShell script provided is designed to automate several key tasks related to running the Microsoft Safety Scanner:

  1. Parameter Handling: The script begins by defining parameters for the type of scan (Quick or Full), a timeout period, and an optional custom field where results can be saved. These parameters are set with defaults, but can also be overridden by environment variables, allowing for flexible usage across different scenarios.
  2. Environment Setup: Before proceeding, the script checks if the user has the necessary administrative privileges. Without these, the script will terminate, ensuring that only authorized personnel can execute potentially disruptive scans.
  3. File Download: One of the script’s primary functions is downloading the latest version of MSERT from Microsoft’s servers. This is handled by the Invoke-Download function, which supports TLS 1.2 and 1.3 for secure connections. The function attempts to download the file multiple times to account for any network issues or rate limiting by the server.
  4. Running the Scan: Once downloaded, the script initiates the scan using the specified parameters. It can perform a quick scan of common exploit locations or a full scan of the entire disk, depending on the user’s choice. The scan is run in quiet mode to minimize interruptions.
  5. Handling Results: After the scan completes, the script processes the log file generated by MSERT. It checks for any detected threats and prints the results to the console. If specified, it also saves the results to a custom field within NinjaOne, which can be used for further analysis or reporting.
  6. Cleanup: Finally, the script removes the downloaded MSERT executable and the log file to clean up the system, ensuring no unnecessary files are left behind.

Potential Use Cases

Imagine a scenario where an MSP is responsible for the security of hundreds of endpoints across multiple clients. Manually running malware scans on each machine would be time-consuming and inefficient.

By deploying this script across all endpoints, the MSP can ensure that each system is regularly scanned for threats, with results automatically reported back to their management console. If a threat is detected, the MSP can quickly respond, minimizing the potential damage and maintaining a secure environment for their clients.

Comparisons

The approach used in this script contrasts with traditional, manual methods of running the Microsoft Safety Scanner. Normally, an IT professional would need to download MSERT, run it manually, and then review the results—steps that are prone to oversight and inconsistency.

By automating the process with PowerShell, the script ensures that scans are performed uniformly and regularly, reducing the chance of human error and ensuring that security protocols are followed consistently.

Compared to other automated solutions, such as full-fledged antivirus software with real-time protection, this script offers a lightweight, on-demand alternative that can be integrated into broader security practices. It’s particularly useful in environments where running full antivirus software on every machine may not be feasible or necessary.

FAQs

1. What happens if the script times out during a scan?
If the scan exceeds the specified timeout period, the script terminates the process and outputs an alert. This prevents the scan from running indefinitely and potentially impacting system performance.

2. Can the script be used on older versions of Windows?
The script requires at least Windows 10 or Server 2016. Older versions of Windows may not support some of the features used in the script, such as TLS 1.2/1.3 or certain PowerShell cmdlets.

3. How does the script handle network issues during the download?
The Invoke-Download function includes multiple attempts to download the MSERT executable. If the download fails after several attempts, the script will output an error and terminate.

4. Is it safe to run this script in a production environment?
Yes, the script is designed with safety in mind, including checks for administrative privileges and careful handling of potential errors. However, it’s always recommended to test scripts in a controlled environment before deploying them broadly.

Implications

The results of this script can have significant implications for IT security. By automating malware scans, IT teams can ensure that systems are regularly checked for threats, reducing the risk of undetected infections. This proactive approach to security can help prevent data breaches and other security incidents, which can have severe consequences for businesses, including financial loss and damage to reputation.

Recommendations

When using this script, consider the following best practices:

  • Test the script in a lab environment before deploying it across all endpoints to ensure it works as expected.
  • Schedule regular scans using Windows Task Scheduler or another automation tool to ensure that systems are continually monitored.
  • Monitor the results closely and set up alerts for when threats are detected so that you can respond quickly.

Final Thoughts

This PowerShell script offers a powerful way to automate the use of Microsoft Safety Scanner, providing IT professionals and MSPs with a reliable tool to maintain system security. By integrating it into broader security practices, users can ensure that their environments remain protected against malware threats with minimal manual intervention.

Add a Comment

Your email address will not be published. Required fields are marked *

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service deliver tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like

×

See NinjaOne in action!

By submitting this form, I accept NinjaOne's privacy policy.

NinjaOne Terms & Conditions

By clicking the “I Accept” button below, you indicate your acceptance of the following legal terms as well as our Terms of Use:

  • Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms.
  • Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party.
  • Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library belonging to or under the control of any other software provider.
  • Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations.
  • Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks.
  • Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script.
  • EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).