Comment lancer une analyse Windows Defender avec PowerShell

La détection des logiciels malveillants et la protection des terminaux sont des piliers essentiels du paysage informatique, en particulier pour les entreprises qui gèrent un réseau distribué de terminaux. Qu’il s’agisse de protéger les appareils contre les ransomwares ou d’assurer la conformité avec les cadres de cybersécurité, l’exécution d’analyses antivirus régulières est une tâche non négociable. L’automatisation de ces analyses au moyen de scripts permet non seulement de réduire les frais généraux, mais aussi d’assurer la cohérence entre les environnements. Cet article de blog se penche sur un script PowerShell complet qui permet aux professionnels de l’informatique de lancer une analyse Windows Defender, de gérer le temps d’exécution et d’enregistrer les résultats, le tout par le biais de l’automatisation.

Contexte

Microsoft Defender, anciennement connu sous le nom de Windows Defender, est intégré à tous les systèmes d’exploitation Windows modernes et constitue une solide base de protection antivirus. Cependant, la gestion et le déclenchement des analyses Defender à grande échelle, sur une flotte d’appareils distants, peuvent s’avérer fastidieux en l’absence d’automatisation. Ce script est destiné aux administrateurs informatiques et aux fournisseurs de services gérés (MSP) qui utilisent des outils tels que NinjaOne pour rationaliser les opérations de sécurité.

Le script PowerShell exploite les capacités de la ligne de commande de Defender et les améliore avec l’analyse du journal des événements, le formatage des résultats et l’intégration optionnelle avec les champs personnalisés WYSIWYG de NinjaOne. Il résout deux problèmes majeurs :

  1. Automatisation – Déclencher des analyses de manière programmatique.
  2. Responsabilité – Saisir les résultats détaillés pour l’audit ou les rapports de tableau de bord.

Le script :

#Requires -Version 5.1

<#
.SYNOPSIS
    Starts a Windows Defender scan.
.DESCRIPTION
    This script starts a Windows Defender scan based on the specified scan type and path. It also handles timeouts and saves the scan results to a Wysiwyg custom field if specified.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.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).

.PARAMETER PathToScan
    The path to scan. This is only required if the scan type is Custom.

.PARAMETER ScanType
    The type of scan to perform. Options are Quick, Full, or Custom. Default is Quick.

.PARAMETER TimeoutInMinutes
    The timeout in minutes for the scan. Default is 120 minutes.

.PARAMETER ForceStopOnTimeout
    If specified, the scan will be forcefully stopped if it exceeds the timeout.

.PARAMETER WysiwygCustomFieldName
    The name of the Wysiwyg custom field to store the scan results.

.EXAMPLE
    -ScanType "Quick"
    ## EXAMPLE OUTPUT WITH pathToScanForCustomScan ##
    [Info] Installed Antivirus: Windows Defender
    [Info] Starting Windows Defender scan:
    [Info] Quick selected, will scan the system drive(C:), and will timeout in 120 minutes.
    [Info] Starting job for a Quick scan on the system drive.
    [Info] Job completed.
    [Info] Scan completed successfully.
    [Info] Scan results:

    Name   : Virus:DOS/EICAR_Test_File
    Action : Quarantine
    Path   : C:\test\eicarcom2.zip
            C:\test\eicarcom2.zip->eicar_com.zip->eicar.com
    Time   : 3/24/2025 10:12:32 AM
    SHA1   : bec1b52d350d721c7e22a6d4bb0a92909893a3ae
    SHA256 : e1105070ba828007508566e28a2b8d4c65d192e9eaf3b7868382b7cae747b397

.EXAMPLE
    -ScanType "Full"
    ## EXAMPLE OUTPUT WITH scanType ##
    [Info] Installed Antivirus: Windows Defender
    [Info] Starting Windows Defender scan:
    [Info] Full selected, will scan the system drive(C:), and will timeout in 120 minutes.
    [Info] Starting job for a Full scan on the system drive.
    [Info] Job completed.
    [Info] Scan completed successfully.
    [Info] Scan results:

    Name   : Virus:DOS/EICAR_Test_File
    Action : Quarantine
    Path   : C:\test\eicarcom2.zip
            C:\test\eicarcom2.zip->eicar_com.zip->eicar.com
    Time   : 3/24/2025 10:12:32 AM
    SHA1   : bec1b52d350d721c7e22a6d4bb0a92909893a3ae
    SHA256 : e1105070ba828007508566e28a2b8d4c65d192e9eaf3b7868382b7cae747b397

.EXAMPLE
    -ScanType "Custom" -PathToScan "C:\test"
    ## EXAMPLE OUTPUT WITH scanType ##
    [Info] Installed Antivirus: Windows Defender
    [Info] Starting Windows Defender scan:
    [Info] Custom selected, will scan the path provided(C:\test), and will timeout in 120 minutes.
    [Info] Starting job for a Custom scan on the system drive.
    [Info] Job completed.
    [Info] Scan completed successfully.
    [Info] Scan results:

    Name   : Virus:DOS/EICAR_Test_File
    Action : Quarantine
    Path   : C:\test\eicarcom2.zip
            C:\test\eicarcom2.zip->eicar_com.zip->eicar.com
    Time   : 3/24/2025 10:12:32 AM
    SHA1   : bec1b52d350d721c7e22a6d4bb0a92909893a3ae
    SHA256 : e1105070ba828007508566e28a2b8d4c65d192e9eaf3b7868382b7cae747b397

.EXAMPLE
    -ForceStopOnTimeout -TimeoutInMinutes 120
    ## EXAMPLE OUTPUT WITH forceStopOnTimeout ##
    [Info] Installed Antivirus: Windows Defender
    [Info] Starting Windows Defender scan:
    [Info] Quick selected, will scan the system drive(C:), and will timeout in 120 minutes.
    [Info] Starting job for a Quick scan on the system drive.
    [Info] Job completed.
    [Error] Scan exceeded the timeout of 120 minutes. Stopping the scan.

.EXAMPLE
    -WysiwygCustomFieldName "WindowsDefenderScanResults"
    ## EXAMPLE OUTPUT WITH wysiwygCustomFieldName ##
    [Info] Installed Antivirus: Windows Defender
    [Info] Starting Windows Defender scan:
    [Info] Quick selected, will scan the system drive(C:), and will timeout in 120 minutes.
    [Info] Starting job for a Quick scan on the system drive.
    [Info] Job completed.
    [Info] Scan completed successfully.
    [Info] Attempting to set Custom Field 'WindowsDefenderScanResults'.
    [Info] Successfully set Custom Field 'WindowsDefenderScanResults'!
    [Info] Scan results:

    Name   : Virus:DOS/EICAR_Test_File
    Action : Quarantine
    Path   : C:\test\eicarcom2.zip
            C:\test\eicarcom2.zip->eicar_com.zip->eicar.com
    Time   : 3/24/2025 10:12:32 AM
    SHA1   : bec1b52d350d721c7e22a6d4bb0a92909893a3ae
    SHA256 : e1105070ba828007508566e28a2b8d4c65d192e9eaf3b7868382b7cae747b397


.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial Release
#>

[CmdletBinding()]
param (
    [string]$PathToScan,
    [string]$ScanType,
    [int]$TimeoutInMinutes,
    [switch]$ForceStopOnTimeout,
    [string]$WysiwygCustomFieldName
)

begin {

    function Set-CustomField {
        [CmdletBinding()]
        Param(
            [Parameter(Mandatory = $True)]
            [String]$Name,
            [Parameter()]
            [String]$Type,
            [Parameter(Mandatory = $True, ValueFromPipeline = $True)]
            $Value,
            [Parameter()]
            [String]$DocumentName,
            [Parameter()]
            [Switch]$Piped
        )
        # Remove the non-breaking space character
        if ($Type -eq "WYSIWYG") {
            $Value = $Value -replace ' ', '&nbsp;'
        }
        
        # Measure the number of characters in the provided value
        $Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters
    
        # Throw an error if the value exceeds the character limit of 200,000 characters
        if ($Piped -and $Characters -ge 200000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.")
        }
    
        if (!$Piped -and $Characters -ge 45000) {
            throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.")
        }
        
        # Initialize a hashtable for additional documentation parameters
        $DocumentationParams = @{}
    
        # If a document name is provided, add it to the documentation parameters
        if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
        
        # Define a list of valid field types
        $ValidFields = "Attachment", "Checkbox", "Date", "Date or Date Time", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine", "MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"
    
        # Warn the user if the provided type is not valid
        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" }
        
        # Define types that require options to be retrieved
        $NeedsOptions = "Dropdown"
    
        # If the property is being set in a document or field and the type needs options, retrieve them
        if ($DocumentName) {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
            }
        }
        else {
            if ($NeedsOptions -contains $Type) {
                $NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
            }
        }
        
        # Throw an error if there was an issue retrieving the property options
        if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
            
        # Process the property value based on its type
        switch ($Type) {
            "Checkbox" {
                # Convert the value to a boolean for Checkbox type
                $NinjaValue = [System.Convert]::ToBoolean($Value)
            }
            "Date or Date Time" {
                # Convert the value to a Unix timestamp for Date or Date Time type
                $Date = (Get-Date $Value).ToUniversalTime()
                $TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
                $NinjaValue = $TimeSpan.TotalSeconds
            }
            "Dropdown" {
                # Convert the dropdown value to its corresponding GUID
                $Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
                $Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID
            
                # Throw an error if the value is not present in the dropdown options
                if (!($Selection)) {
                    throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.")
                }
            
                $NinjaValue = $Selection
            }
            default {
                # For other types, use the value as is
                $NinjaValue = $Value
            }
        }
            
        # Set the property value in the document if a document name is provided
        if ($DocumentName) {
            $CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
        }
        else {
            try {
                # Otherwise, set the standard property value
                if ($Piped) {
                    $CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1
                }
                else {
                    $CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1
                }
            }
            catch {
                Write-Host -Object "[Error] Failed to set custom field."
                throw $_.Exception.Message
            }
        }
            
        # Throw an error if setting the property failed
        if ($CustomField.Exception) {
            throw $CustomField
        }
    }

    function Get-DefenderScanResults {

        $EventTypes = @(
            # Windows Defender event types:
            # https://learn.microsoft.com/en-us/defender-endpoint/troubleshoot-microsoft-defender-antivirus
            [PSCustomObject]@{Id = 1006; Name = "MALWARE_DETECTED" }
            [PSCustomObject]@{Id = 1007; Name = "MALWARE_ACTION_TAKEN" }
            [PSCustomObject]@{Id = 1119; Name = "MALWARE_ACTION_FAILED" }
            [PSCustomObject]@{Id = 1009; Name = "QUARANTINE_RESTORE" }
            [PSCustomObject]@{Id = 1010; Name = "QUARANTINE_RESTORE_FAILED" }
            [PSCustomObject]@{Id = 1011; Name = "QUARANTINE_DELETE" }
            [PSCustomObject]@{Id = 1012; Name = "QUARANTINE_DELETE_FAILED" }
            [PSCustomObject]@{Id = 1013; Name = "MALWARE_HISTORY_DELETE" }
            [PSCustomObject]@{Id = 1014; Name = "MALWARE_HISTORY_DELETE_FAILED" }
            [PSCustomObject]@{Id = 1015; Name = "BEHAVIOR_DETECTED" }
            [PSCustomObject]@{Id = 1116; Name = "STATE_MALWARE_DETECTED" }
            [PSCustomObject]@{Id = 1117; Name = "STATE_MALWARE_ACTION_TAKEN" }
            [PSCustomObject]@{Id = 1118; Name = "STATE_MALWARE_ACTION_FAILED" }
            [PSCustomObject]@{Id = 1119; Name = "STATE_MALWARE_ACTION_CRITICALLY_FAILED" }
        )

        # Get the last scan start event (Event ID 1000)
        $lastScan = Get-WinEvent -FilterHashtable @{
            LogName = 'Microsoft-Windows-Windows Defender/Operational'
            ID      = 1000
        } -ErrorAction SilentlyContinue | Sort-Object TimeCreated -Descending | Select-Object -First 1

        if (-not $lastScan) {
            # Warn if no scan start event is found
            Write-Host "[Warn] No scan start event (ID 1000) found in event logs."
            return
        }

        $scanStartTime = $lastScan.TimeCreated

        # Get detection events (Event ID 1119) that occurred after the scan started
        $detectionEvents = Get-WinEvent -FilterHashtable @{
            LogName = 'Microsoft-Windows-Windows Defender/Operational'
            ID      = $EventTypes.Id
        } -ErrorAction SilentlyContinue | Where-Object { $_.TimeCreated -ge $scanStartTime }

        # Process each event to extract Name, Action, and Path using regex
        $results = foreach ($event in $detectionEvents) {
            $message = $event.Message

            $name = ([regex]::Match($message, "Name:\s*(.+)").Groups[1].Value).Trim()
            $action = ([regex]::Match($message, "Action:\s*(.+)").Groups[1].Value).Trim()
            $path = ([regex]::Match($message, "Path:\s*(.+)").Groups[1].Value).Trim()

            if ($name -and $path) {
                # Create a custom object with the extracted information
                [PSCustomObject]@{
                    Name   = $name
                    Action = "$(
                        if ($action) {
                            $action
                        } else {
                            $(
                                # Get the event name based on the event ID
                                $eventTypes | Where-Object { $_.Id -eq $event.Id } | Select-Object -ExpandProperty Name -First 1
                            ) -replace '_', ' '
                        }
                        # Convert to uppercase using the casing rules of the invariant culture.
                        )".ToUpperInvariant()
                    Path   = $path -replace '[a-z]+:_' -split ';'
                    Time   = $event.TimeCreated
                }
            }
        }
        return $results
    }

    function Get-DetectedThreats {
        [CmdletBinding()]
        Param()

        # Initialize a list to store parsed threat information
        $Threats = Get-DefenderScanResults

        # Initialize lists to store parsed results
        $parsedSDNQuery = [System.Collections.Generic.List[PSCustomObject]]::new()

        # Get the path to the Windows Defender logs
        $MpLogsPath = "$env:ProgramData\Microsoft\Windows Defender\Support"

        # Get the Windows Defender logs
        $MpLogs = Get-ChildItem -Path $MpLogsPath -Filter "MpLog*.log" -File -ErrorAction SilentlyContinue

        # Begin Resource Scan
        $MpLogs | ForEach-Object {
            # Read the file line by line
            $lines = Get-Content -Path $_.FullName

            foreach ($line in $lines) {
                # Parse SDN query events (now includes both SHA1 and SHA256)
                if ($line -match "(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z?) SDN:Issuing SDN query for (\\?.+?) \((\\?.+?)\) \(sha1=(\w{40}), sha2=(\w{64})\)") {
                    $timestamp = ($matches[1] | Get-Date).ToUniversalTime()
                    $filePath = $matches[2]
                    $sha1 = $matches[4]
                    $sha256 = $matches[5]

                    # Store SDN query result
                    $parsedSDNQuery.Add(
                        [PSCustomObject]@{
                            Timestamp = $timestamp
                            Path      = $filePath -replace '\\\\\?\\'
                            SHA1      = $sha1
                            SHA256    = $sha256
                        }
                    )
                }
            }
        }

        # Process each threat to add SHA1 and SHA256 hashes if available
        $Threats | ForEach-Object {
            $ThreatInfo = $_
            # Find the corresponding SHA1 and SHA256 hashes by matching the path
            $SHAs = $parsedSDNQuery | Where-Object { $($_.Path | Split-Path -Leaf) -in $($ThreatInfo.Path | Split-Path -Leaf) } | Select-Object -First 1

            if ($SHAs) {
                $ThreatInfo | Add-Member -MemberType NoteProperty -Name "SHA1" -Value ($SHAs | Select-Object -ExpandProperty SHA1)
                $ThreatInfo | Add-Member -MemberType NoteProperty -Name "SHA256" -Value ($SHAs | Select-Object -ExpandProperty SHA256)
                $ThreatInfo | Add-Member -MemberType NoteProperty -Name "Link" -Value "https://www.virustotal.com/gui/search/$($SHAs | Select-Object -ExpandProperty SHA1)"
            }
            else {
                $ThreatInfo | Add-Member -MemberType NoteProperty -Name "SHA1" -Value "Unavailable"
                $ThreatInfo | Add-Member -MemberType NoteProperty -Name "SHA256" -Value "Unavailable"
                $ThreatInfo | Add-Member -MemberType NoteProperty -Name "Link" -Value ""
            }

            # Output the results
            Write-Output $ThreatInfo
        }
    }

    function ConvertTo-HtmlTable {
        param (
            [Parameter(Mandatory = $true)]
            [System.Collections.Generic.List[Object]]
            $Objects
        )
        $StringBuilder = New-Object System.Text.StringBuilder

        # Create the HTML table header
        $StringBuilder.Append('<table><thead><tr>') | Out-Null
        $($Objects | Select-Object -First 1).PSObject.Properties.Name | ForEach-Object { $StringBuilder.Append("<th>$_</th>") | Out-Null }
        $StringBuilder.Append('</tr></thead><tbody>') | Out-Null

        # Loop through each object and create a table row for each
        $Objects | ForEach-Object {
            $CurrentObject = $_

            $StringBuilder.Append("<tr>") | Out-Null
            $CurrentObject.PSObject.Properties.Name | ForEach-Object {
                $Name = $_
                $StringBuilder.Append("<td>$($CurrentObject.$Name)</td>") | Out-Null
            }
            $StringBuilder.Append('</tr>') | Out-Null
        }
        # Create the HTML table footer
        $StringBuilder.Append('</tbody></table>') | Out-Null

        # Return the HTML table as a string
        return $StringBuilder.ToString()
    }

    # Output the installed antivirus products
    $(
        if ($PSVersionTable.PSVersion.Major -lt 3) {
            Get-WmiObject -Class antivirusproduct -Namespace root\securitycenter2 -ErrorAction SilentlyContinue
        }
        else {
            Get-CimInstance -ClassName antivirusproduct -Namespace root\securitycenter2 -ErrorAction SilentlyContinue
        }
    ) | Select-Object -Property displayName | ForEach-Object {
        Write-Host "[Info] Installed Antivirus: $($_.displayName)"
    }

    # Check if the Windows Defender module is available
    if ($(Get-Command -Module Defender).Count -eq 0) {
        Write-Host "[Error] The Windows Defender is not available. Please ensure that the Windows Defender is installed."
        exit 1
    }

    # Check if Windows Defender is enabled, regardless of the operating system
    $DefenderStatus = Get-MpComputerStatus

    # Check if Windows Defender is enabled
    if ($DefenderStatus.AntivirusEnabled -eq $false -and $DefenderStatus.AntispywareEnabled -eq $false) {
        Write-Host "[Error] Windows Defender is not enabled. Please enable Windows Defender before starting a scan."
        exit 1
    }
    if ($DefenderStatus.AMRunningMode -like "Not running" -or $DefenderStatus.AMRunningMode -like "Disabled") {
        Write-Host "[Error] Windows Defender is disabled or is not running. Please start Windows Defender before starting a scan."
        exit 1
    }

    # Get script variables from environment variables
    if ("$env:pathToScanForCustomScan".Trim()) {
        $PathToScan = "$env:pathToScanForCustomScan".Trim()
    }

    # Check if the path to scan exists
    if ($PathToScan -and -not (Test-Path $PathToScan)) {
        Write-Host "[Error] The path ($PathToScan) does not exist."
        exit 1
    }

    if ($env:scanType) {
        $ScanType = $env:scanType
    }

    if ($ScanType -notlike "Custom" -and $PathToScan) {
        Write-Host "[Error] The path to scan ($PathToScan) can only be used when the scan type is Custom."
        exit 1
    }

    if ($env:timeoutInMinutes) {
        try {
            [int]$value = $env:timeoutInMinutes
        }
        catch {
            Write-Host "[Error] The Timeout In Minutes value ($env:timeoutInMinutes) is not a valid integer. Must be a positive integer and less than ($([int]::MaxValue))."
            exit 1
        }
        if ($value -gt [int]::MaxValue) {
            $TimeoutInMinutes = [int]::MaxValue
        }
        elseif ($env:timeoutInMinutes -lt 1) {
            Write-Host "[Error] The Timeout In Minutes value ($env:timeoutInMinutes) is less than 1. Must be a positive integer and greater than 0."
            exit 1
        }
        else {
            $TimeoutInMinutes = $env:timeoutInMinutes
        }
        
    }
    if ($env:forceStopOnTimeout -like "true") {
        $ForceStopOnTimeout = $true
    }
    if ("$env:wysiwygCustomFieldName".Trim()) {
        $WysiwygCustomFieldName = "$env:wysiwygCustomFieldName".Trim()
    }

    if ($TimeoutInMinutes -gt 2880) {
        Write-Host "[Error] The timeout ($TimeoutInMinutes) exceeds 2880 minutes. The scan will not be performed."
        exit 1
    }
    if ($TimeoutInMinutes -gt 240) {
        Write-Host "[Warn] The timeout ($TimeoutInMinutes) exceeds 240 minutes." -NoNewline
        if ($env:wysiwygCustomFieldName) {
            Write-Host " The scan results may not be saved to the WYSIWYG custom field when the timeout exceeds 240 minutes." -NoNewline
        }
        Write-Host " Scan results may not be returned to the Activity Feed."
        Write-Host "[Info] The scan will still be performed."
    }

}
process {

    # Set the default scan type
    if (-not $ScanType) {
        $ScanType = "Quick"
    }

    # If the scan type is Custom, the path to scan is required
    if ($ScanType -like "Custom" -and -not $PathToScan) {
        Write-Host "[Error] The path to scan is required when the scan type is Custom."
        exit 1
    }

    # Set the default timeout
    if (-not $TimeoutInMinutes) {
        $TimeoutInMinutes = 120
    }

    Write-Host "[Info] Starting Windows Defender scan:"
    if ($ScanType -like "Custom") {
        Write-Host "[Info] $ScanType selected, will scan the path provided($PathToScan), and will timeout in $TimeoutInMinutes minutes."
    }
    else {
        Write-Host "[Info] $ScanType selected, will scan the system drive($env:SystemDrive), and will timeout in $TimeoutInMinutes minutes."
    }

    # Initialize variables to track the scan status
    $ScanFailedToRun = $false
    $ScanFailedToRunMessage = ""

    # Start the scan, wait for it to complete, and get the scan job
    try {
        # Start the scan job
        if ($ScanType -like "Custom") {
            # Start a custom scan with the specified path
            Write-Host "[Info] Starting job for a $ScanType scan on $PathToScan."
            $Job = Start-MpScan -ScanType "$($ScanType)Scan" -ScanPath $PathToScan -AsJob | Wait-Job -Timeout $($TimeoutInMinutes * 60)
            Write-Host "[Info] Job completed."
        }
        else {
            # Start the scan without a specified path
            Write-Host "[Info] Starting job for a $ScanType scan on the system drive."
            $Job = Start-MpScan -ScanType "$($ScanType)Scan" -AsJob | Wait-Job -Timeout $($TimeoutInMinutes * 60)
            Write-Host "[Info] Job completed."
        }
    }
    catch {
        Write-Host "[Error] Failed to start the scan."
        Write-Host "[Error] $($_.Exception.Message)"
        # Set the scan status to failed and save the error message
        $ScanFailedToRunMessage = $_.Exception.Message
        $ScanFailedToRun = $true
    }

    if ($Job.Finished) {
        $ScanTimedOut = $false
    }
    else {
        $ScanTimedOut = $true
        # Stop the scan if it exceeds the timeout, but only if the scan did not fail to run
        if ($ForceStopOnTimeout -and $ScanFailedToRun -eq $false) {
            Write-Host "[Error] Scan exceeded the timeout of $TimeoutInMinutes minutes. Stopping the scan."
            & "$env:ProgramFiles\Windows Defender\MpCmdRun.exe" -Scan -Cancel 2>&1 | Out-Null
        }
    }

    # Initialize variables to track the scan status
    $ErrorScanning = $false
    $ErrorSaving = $false

    # Check the scan status
    switch ($Job.State) {
        "Completed" { Write-Host "[Info] Scan completed successfully." }
        "Failed" { Write-Host "[Error] Scan failed." ; $ErrorScanning = $true }
        "Stopped" { Write-Host "[Info] Scan stopped." }
        Default { Write-Host "[Error] Scan did not complete." }
    }

    # Get the scan results
    $ScanResults = if ($ScanTimedOut) {
        Write-Output "Scan exceeded the timeout of $TimeoutInMinutes minutes."

        Write-Host "[Error] Scan exceeded the timeout of $TimeoutInMinutes minutes."
    }
    elseif ($ErrorScanning) {
        Write-Output "Scan failed."

        Write-Host "[Error] Scan failed."
    }
    elseif ($ScanFailedToRun) {
        Write-Output "Failed to start the scan. Reason: $ScanFailedToRunMessage"

        Write-Host "[Error] Failed to start the scan. Reason: $ScanFailedToRunMessage"
    }
    else {

        # Wait for more events to be logged
        Start-Sleep -Seconds 60

        $Results = Get-DetectedThreats | ForEach-Object {
            [PSCustomObject]@{
                Name          = $_.Name
                Action        = $_.Action
                Path          = $_.Path -join [System.Environment]::NewLine # Join paths with a newline character
                Time          = $_.Time
                SHA1          = $_.SHA1
                SHA256        = $_.SHA256
                "Virus Total" = $_.Link
            }
        }
        if ($Results) {
            Write-Output $Results
        }
        else {
            Write-Output "No threats detected from latest scan."

            Write-Host "[Info] No threats detected from latest scan."
        }
    }

    # Save the scan results to a Wysiwyg custom field
    if ($WysiwygCustomFieldName) {
        # Save the scan results to the Wysiwyg custom field
        try {
            Write-Host "[Info] Attempting to set Custom Field '$WysiwygCustomFieldName'."
            if ($ScanResults -is [String]) {
                Set-CustomField -Name $WysiwygCustomFieldName -Value $ScanResults -Type "WYSIWYG"
            }
            else {
                # Convert the scan results to HTML and set the custom field
                $HtmlTable = ConvertTo-HtmlTable -Objects $(
                    $ScanResults | Select-Object Name, Action, @{
                        # Convert list of paths to an HTML table
                        Name       = "Path"
                        Expression = { $_.Path -split [System.Environment]::NewLine -join "<br>" }
                    }, Time, @{
                        Name       = "SHA1"
                        Expression = {
                            if ($_."Virus Total") {
                                # Create a link to VirusTotal for SHA1
                                "<a href='$($_."Virus Total")' target='_blank' rel='nofollow noopener noreferrer'>$($_.SHA1)&nbsp;&nbsp;<i class='fas fa-arrow-up-right-from-square'></i></a>"
                            }
                            else { "Unavailable" }
                        }
                    }, SHA256
                )
                # Add icons to the table headers and set widths
                $HtmlTable = $HtmlTable -replace '<th>Name', "<th style='width: 10em'><i class='fa-solid fa-file'></i>&nbsp;&nbsp;Name"
                $HtmlTable = $HtmlTable -replace '<th>Path', "<th style='width: 20em'><i class='fa-solid fa-folder'></i>&nbsp;&nbsp;Path"
                $HtmlTable = $HtmlTable -replace '<th>Time', "<th style='width: 19em'><i class='fa-solid fa-clock'></i>&nbsp;&nbsp;Time"
                $HtmlTable = $HtmlTable -replace '<th>Action', "<th style='width: 7em'><i class='fa-solid fa-shield-virus'></i>&nbsp;&nbsp;Action"
                $HtmlTable = $HtmlTable -replace '<th>SHA1', "<th style='width: 19em'>SHA1&nbsp;&nbsp;<i class='fa-solid fa-arrow-up-right-from-square'></i>"
                $HtmlTable = $HtmlTable -replace '<th>SHA256', "<th style='width: 19em'>SHA256"
                $HtmlTable = $HtmlTable -replace "<table>", "<table style='white-space:nowrap;'>"

                # Add a card wrapper around the HTML table
                $HtmlTable = "<div class='card flex-grow-1'>
    <div class='card-title-box'>
        <div class='card-title'><i class='fa-solid fa-book'></i>&nbsp;&nbsp;Windows Defender Scan Results</div>
    </div>
    <div class='card-body' style='white-space: nowrap'>
        $HtmlTable
    </div>
</div>"
                Set-CustomField -Name $WysiwygCustomFieldName -Value $HtmlTable -Type "WYSIWYG" -Piped
            }
            Write-Host "[Info] Successfully set Custom Field '$WysiwygCustomFieldName'!"
        }
        catch {
            Write-Host "[Error] $($_.Exception.Message)"
            $ErrorSaving = $true
        }
    }

    if ($Results) {
        Write-Host "[Info] Scan results:"
        Write-Host ""
        "$($Results | Select-Object -Property Name, Action, Path, Time, SHA1, SHA256 | Format-List | Out-String -Width 4000)".Trim() | Write-Host
        Write-Host ""
    }

    # Exit with an error code if there was an error scanning or the scan timed out
    if ($ErrorScanning -or $ScanTimedOut -or $ScanFailedToRun -or $ErrorSaving) {
        exit 1
    }
    else {
        exit 0
    }
}
end {
    
    
    
}

 

Description détaillée

Le script est structuré avec des fonctions modulaires permettant d’effectuer un cycle de vie complet de l’analyse, de l’initialisation à l’enregistrement des résultats. Voici comment cela fonctionne :

Définitions des paramètres

  • ScanType : Accepte « Quick », « Full », ou « Custom ». Les options Quick et Full analysent le lecteur système ; l’option Custom analyse un chemin d’accès spécifique.
  • PathToScan : Requis si ScanType est « Custom ».
  • TimeoutInMinutes : La valeur par défaut est de 120 minutes. Empêche les analyses de se poursuivre indéfiniment.
  • ForceStopOnTimeout : Optionnel. Annule de force une analyse qui dépasse le délai d’attente.
  • WysiwygCustomFieldName : Optionnel. Enregistre les résultats de l’analyse dans un champ personnalisé NinjaOne.

Fonctions essentielles

  1. Set-CustomField : Valide et met à jour les champs personnalisés de NinjaOne. Particulièrement utile pour documenter les résultats de l’analyse dans un tableau de bord centralisé.
  2. Get-DefenderScanResults : Analyse les journaux d’événements Windows à la recherche d’événements pertinents pour la détection des menaces (par exemple, logiciels malveillants détectés, actions entreprises).
  3. Get-DetectedThreats : Améliore les données des événements avec des hachages SHA1/SHA256 provenant des journaux Defender et inclut des liens VirusTotal pour une investigation plus approfondie.
  4. ConvertTo-HtmlTable : Transforme les résultats en une carte HTML stylisée, parfaite pour les tableaux de bord ou les rapports NinjaOne.

Logique d’exécution de l’analyse

  • Valide les entrées.
  • Vérifie que Defender est installé, activé et en cours d’exécution.
  • Lance l’analyse à l’aide de Start-MpScan avec les paramètres sélectionnés.
  • Surveille l’achèvement, applique les règles de temporisation et, éventuellement, annule les analyses de longue durée.
  • Rassemble les résultats des menaces, les analyse dans un format lisible et les stocke éventuellement dans NinjaOne.

Cas d’utilisation potentiels

Scénario : Une entreprise MSP du secteur de la santé gérant 300 terminaux veut s’assurer que toutes les machines exécutent une analyse hebdomadaire des logiciels malveillants et rapportent les résultats sur leur tableau de bord central.

Solution :

  • Configurez une tâche programmée ou un automatisme NinjaOne pour exécuter ce script tous les vendredis soirs.
  • Spécifiez -ScanType "Full" et un délai de 180 minutes.
  • Utilisez -WysiwygCustomFieldName "Weekly AV Scan" pour enregistrer les résultats dans l’enregistrement de l’actif NinjaOne.

Résultat : Le MSP bénéficie d’une visibilité totale sur l’état de l’antivirus de chaque machine sans vérification manuelle, ce qui lui permet de réagir rapidement en cas de détection d’un logiciel malveillant.

Comparaisons

Comparaison entre l’analyse programmée de Defender et l’analyse manuelle avec l’interface graphique

FonctionScript PowerShellInterface graphique manuelle
ÉvolutivitéHauteFaible
AutomatisationOuiNon
ConnexionDétailléMinime
IntégrationChamps personnalisés NinjaOneAucune

Analyse programmée par script ou par stratégie de groupe

  • La stratégie de groupe est efficace pour les politiques générales, mais elle ne permet pas d’établir des rapports en temps réel ni d’exercer un contrôle granulaire.
  • Ce script offre un contrôle dynamique, un délai d’exécution et un retour d’information exploitable.

Questions fréquentes

Q1 : Que se passe-t-il si Defender n’est pas installé ou ne fonctionne pas ?
Le script vérifie la disponibilité de Defender et se termine par un message d’erreur s’il est absent ou désactivé.

Q2 : Puis-je l’exécuter sur un serveur Windows ?
Oui, il prend en charge Windows Server 2016 et les versions plus récentes.

Q3 : Que se passe-t-il si l’analyse dépasse le délai d’attente ?
Vous pouvez activer l'option -ForceStopOnTimeout pour annuler l’analyse de manière forcée. Un message sera également enregistré.

Q4 : NinjaOne est-il nécessaire ?
L’analyse s’exécute sans NinjaOne. Cependant, l’enregistrement des résultats dans des champs personnalisés nécessite une intégration NinjaOne.

Q5 : Comment puis-je m’assurer que les résultats de l’analyse sont saisis avec précision ?
Le script comprend un Start-Sleep -Seconds 60 pour donner à Defender le temps d’enregistrer les événements avant de les collecter.

Implications

L’exécution régulière de ce script permet aux administrateurs informatiques de maintenir un niveau de sécurité élevé, de détecter les menaces dormantes et de garantir la conformité. En intégrant les résultats de l’analyse dans des tableaux de bord, il favorise la transparence et la responsabilité. Il contribue également à réduire les risques en assurant la traçabilité des menaces détectées, des actions entreprises et des détails au niveau du hachage utiles pour la recherche de menaces.

Recommandations

  • Exécutez le logiciel avec des autorisations élevées pour que Defender puisse fonctionner pleinement.
  • Programmer les analyses en dehors des heures de bureau afin de réduire les perturbations pour les utilisateurs finaux.
  • Utilisez les types d’analyse personnalisés pour les répertoires à haut risque (par exemple, le dossier des téléchargements).
  • Limiter les délais d’attente à des durées réalistes (moins de 240 minutes) pour obtenir des rapports fiables.
  • Combinez les alertes par e-mail ou l’automatisation de la gestion des tickets pour les menaces détectées.

Conclusion

L’automatisation des analyses de Windows Defender avec PowerShell est une étape pratique vers une gestion proactive de la cybersécurité. Ce script illustre la manière dont la combinaison d’outils Windows natifs et de scripts intelligents peut créer des flux de travail évolutifs, transparents et sécurisés.

Pour les utilisateurs de NinjaOne, ce script devient encore plus puissant. En introduisant les résultats des analyses directement dans les champs personnalisés WYSIWYG, les professionnels de l’informatique bénéficient d’une visibilité centralisée, ce qui leur permet de prendre des décisions plus rapides et d’améliorer l’hygiène des terminaux. Que vous soyez un responsable informatique d’entreprise ou un technicien MSP, cette solution apporte une valeur ajoutée mesurable à votre stratégie de sécurité.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service delivery 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.

Catégories :

Vous pourriez aussi aimer