SignatureDownloadCustomTask.ps1

<#PSScriptInfo
 
.VERSION
    1.0
 
.GUID
    55aaa5a7-4b42-4c28-b5d5-ba49a2df0181
 
.AUTHOR
    Windows Defender Team
 
.COMPANYNAME
    Microsoft Corporation
 
.COPYRIGHT
 
.TAGS
    WindowsDefender,SignatureDownload
 
.LICENSEURI
 
.PROJECTURI
 
.ICONURI
 
.EXTERNALMODULEDEPENDENCIES
 
.REQUIREDSCRIPTS
 
.EXTERNALSCRIPTDEPENDENCIES
 
.RELEASENOTES
 
 
#>


<#
 
.DESCRIPTION
   This script allows admins to create scheduled tasks on a Server host that would download the signatures regularly onto a specified share.
    The VMs can then be configured via a policy (not part of this script) to pick up the signatures from the share instead of directly downloading them from ADL or WU.
 
#>
 


<#
.SYNOPSIS
    This script allows admins to create scheduled tasks on a Server host that would download the signatures regularly onto a specified share.
    The VMs can then be configured via a policy (not part of this script) to pick up the signatures from the share instead of directly downloading them from ADL or WU.
.NOTES
    File Name : SignatureDownloadCustomTask.ps1
    Author : Windows Defender Team
    Requires : PowerShell V3
    Note : Messages from this script will be logged to %windir%\Temp\DefenderSignatureDownloadTask.log.
.IMPORTANT-REMARKS
               : -scriptPath. Please make sure that this is a protected path. Otherwise a non-admin could replace the script file and have the task scheduler run it for them.
                              The task does however launch powershell with a flag to enforce running signed script only.
               : -hoursInterval. If this is passed, the task will be created for hour-based intervals instead of day-based intervals.
.EXAMPLES
    To create a task that downloads delta x86 signatures once every 2 days to a directory called D:\Share\Test, and assuming that this scripts resides in C:\Windows\Protected
        SignatureDownloadCustomTask.ps1 -action create -arch x86 -isDelta $true -destDir D:\Share\Test -scriptPath C:\Windows\Protected\SignatureDownloadCustomTask.ps1 -daysInterval 2
    To delete the above task
        SignatureDownloadCustomTask.ps1 -action delete -arch x86 -isDelta $true
    To run the task manually [JFYI, the scheduled task should take care of it anyways]
        SignatureDownloadCustomTask.ps1 -action run -arch x86 -isDelta $true -destDir D:\Share\Test
#>


param
(
    [parameter(Position=0, Mandatory=$true, HelpMessage="Action to do.")]
    [ValidateSet("create","delete","run")] 
    [string]$action,

    [parameter(Position=1, Mandatory=$true, HelpMessage="Architecture of the required signature package.")]
    [ValidateSet("x86","x64","arm")] 
    [string]$arch,

    [parameter(Position=2, Mandatory=$true, HelpMessage="False (0) - Task deals with full signature package, True (1) - Task deals with delta signature package")]
    [ValidateRange($False,$True)]
    [bool]$isDelta,

    [parameter(Mandatory=$false, HelpMessage="The destination directory where the sigs will be downloaded to.")]
    [string]$destDir,

    [parameter(Mandatory=$false, HelpMessage="The full path to where this script file resides.")]
    [string]$scriptPath,

    [parameter(Mandatory=$false, HelpMessage="The frequency desired for this task (in number of days).")]
    [ValidateRange(1, 365)]
    [int]$daysInterval = 1,

    [parameter(Mandatory=$false, HelpMessage="The frequency desired for this task if it has to be run multiple times per day (in number of hours).")]
    [ValidateRange(1, 23)]
    [int]$hoursInterval = 0
)

# Flushes the log file if it is bigger than 100 KB.
Function FlushIfTooBig-LogFile()
{
    [string]$path = Join-Path ($env:windir) "TEMP\DefenderSignatureDownloadTask.log"
    $file = Get-Item $path
    if ($file.Length -gt 100KB)
    {
        '' | Out-File $file
    }
}

# Appends a message to %windir%\Temp\DefenderSignatureDownloadTask.log with the time stamp.
Function Log-Message([string]$message)
{
    Write-Output $message
    [string]$path = Join-Path ($env:windir) "TEMP\DefenderSignatureDownloadTask.log"
    $date = Get-Date
    $date | Out-File $path -Append
    $message | Out-File $path -Append
    '----------End of message----------' | Out-File $path -Append
}

# Downloads file from a given URL.
Function Download-File([string]$url, [string]$targetFile)
{
    [System.Net.WebClient]$webClient = New-Object -TypeName System.Net.WebClient
    [System.Uri]$uri = New-Object -TypeName System.Uri -ArgumentList $url
    $webClient.DownloadFile($uri, $targetFile)
}

# Gets the specified registry value related to signatures.
Function Get-SignatureRegistryValue([string]$name) 
{
    [string]$path
    if ((Test-Path -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft Antimalware') -And ([System.Environment]::OSVersion.Version.Major -lt 10))
    {
        $path = 'HKLM:\SOFTWARE\Microsoft\Microsoft Antimalware\Signature Updates'
    }
    else
    {
        $path = 'HKLM:\SOFTWARE\Microsoft\Windows Defender\Signature Updates'
    }

    $key = Get-Item -LiteralPath $path
    return $key.GetValue($name, $null)
}

# Gets the URL to download AM delta sigs. We use the hosts' AM engine and base sig version for the URL.
Function Get-AmDeltaSigUrl([string]$arch)
{
    [string]$engineVersionValue = Get-SignatureRegistryValue 'EngineVersion'
    [string]$avVersionValue = Get-SignatureRegistryValue 'AVSignatureBaseVersion'
    [string]$asVersionValue = Get-SignatureRegistryValue 'ASSignatureBaseVersion'
    [string]$deltaSigPath = [string]::Format("http://go.microsoft.com/fwlink/?LinkID=121721&clcid=0x409&arch={0}&eng={1}&avdelta={2}&asdelta={3}",
                                             $arch.Trim(),
                                             $engineVersionValue.Trim(),
                                             $avVersionValue.Trim(),
                                             $asVersionValue.Trim())
    return $deltaSigPath
}

# Gets the URL to download AM full sigs.
Function Get-AmFullSigUrl([string]$arch)
{
    [string]$fullSigPath = [string]::Format("http://go.microsoft.com/fwlink/?LinkID=121721&clcid=0x409&arch={0}", $arch.Trim())
    return $fullSigPath
}

# Gets the URL to download NIS sigs.
Function Get-NisSigUrl([string]$arch)
{
    [string]$nisEngineVersionValue = Get-SignatureRegistryValue 'NISEngineVersion'
    [string]$nisSignatureVersionValue = Get-SignatureRegistryValue 'NISSignatureVersion'
    [string]$nisSigPath = [string]::Format("http://go.microsoft.com/fwlink/?LinkID=260974&clcid=0x409&NRI=true&arch=x86&eng={0}&sig={1}",
                                           $arch.Trim(),
                                           $nisEngineVersionValue.Trim(),
                                           $nisSignatureVersionValue.Trim())
    return $nisSigPath   
}

# Downloads AM and NIS sigs.
Function Run-Task([string]$arch, [bool]$isDelta, [string]$destDir)
{
    # Download AM sigs.
    [string]$amSigFileName
    [string]$url
    [string]$packageType
    if ($isDelta)
    {
        $packageType = 'Delta'
        $amSigFileName = 'mpam-d.exe'
        $url = Get-AmDeltaSigUrl $arch
    }
    else
    {
        $packageType = 'Full'
        $amSigFileName = 'mpam-fe.exe'
        $url = Get-AmFullSigUrl $arch
    }

    [string]$fullDestPath = Join-Path $destDir $amSigFileName
    Download-File $url $fullDestPath # Please note that Download-File can throw an error if ADL has no file to offer for the config we pass.
    [string]$message = 'Successfully ran task to download ' + $packageType + ' AM sigs of arch ' + $arch + ' to ' + $fullDestPath
    Log-Message $message

    # Download NIS sigs.
    [string]$nisSigFileName = 'nis_full.exe'
    [string]$fullNisDestPath = Join-Path $destDir $nisSigFileName
    [string]$nisUrl = Get-NisSigUrl $arch
    Download-File $nisUrl $fullNisDestPath
    [string]$nisMessage = 'Successfully ran task to download NIS sigs of arch ' + $arch + ' to ' + $fullNisDestPath
    Log-Message $nisMessage    
}

# Gets the name of the signature download scheduled task, based on arch and package type (delta/full).
Function Get-TaskName([string]$arch, [bool]$isDelta)
{
    [string]$packageType
    if ($isDelta)
    {
        $packageType = 'Delta'
    }
    else
    {
        $packageType = 'Full'
    }

    [string]$taskName = 'Windows Defender Custom Task for ' + $arch + $packageType + ' Signature Download'
    [string]$taskFullName = [string]::Format("\Microsoft\Windows\Windows Defender\{0}", $taskName)

    return $taskFullName
}

# Creates a scheduled task that would download the signatures regularly onto the specified share.
Function Create-Task([string]$scriptPath, [string]$arch, [bool]$isDelta, [string]$destDir, [int]$daysInterval, [int]$hoursInterval)
{
    [string]$schedule
    [int]$interval = 1
    if ($hoursInterval -eq 0)    # if this is not set, then use day-based interval for the scheduled task. Otherwise if it is set, use hour-based interval.
    {
        $schedule = 'DAILY'
        $interval = $daysInterval
    }
    else
    {
        $schedule = 'HOURLY'
        $interval = $hoursInterval
    }

    [string]$scriptCommand = $scriptPath + ' -action run' + ' -arch ' + $arch + ' -isDelta $' + $isDelta + ' -destDir ' + $destDir        
    [string]$schTasksPath = Join-Path ($env:windir) "system32\schtasks.exe"    # Needs to run downlevel as well; hence not using the Win8.1+ specific ScheduleTask* helpers.
    [string]$taskFullName = Get-TaskName $arch $isDelta
    $taskFullName = $taskFullName.Trim()

    [string]$taskProgramArg = 'powershell.exe -Noninteractive -Noprofile -ExecutionPolicy AllSigned -WindowStyle Hidden -NoLogo -Command ' + '\"& ' + $scriptCommand + '\"'
    
    $schTasksProcess = Start-Process -FilePath "$schTasksPath" -ArgumentList "/create /tn ""$taskFullName"" /tr ""$taskProgramArg"" /ru ""NT AUTHORITY\SYSTEM"" /sc ""$schedule"" /MO ""$interval"" /NP /F /RL ""LIMITED""" -Wait -PassThru -NoNewWindow
    if ($schTasksProcess.ExitCode -ne 0)
    {
        throw "Error: Create task failed. Check Event log for more details."
    }

    [string]$message = 'Successfully created task ' + $taskFullName
    Log-Message $message
}

# Deletes specified scheduled task.
Function Delete-Task([string]$arch, [bool]$isDelta)
{
    [string]$schTasksPath = Join-Path ($env:windir) "system32\schtasks.exe"
    [string]$taskFullName = Get-TaskName $arch $isDelta
    $taskFullName = $taskFullName.Trim()

    $schTasksProcess = Start-Process -FilePath "$schTasksPath" -ArgumentList "/delete /tn ""$taskFullName"" /f" -Wait -PassThru -NoNewWindow
    if ($schTasksProcess.ExitCode -ne 0)
    {
        throw "Error: Create task failed. Check Event log for more details."
    }

    [string]$message = 'Successfully deleted task ' + $taskFullName
    Log-Message $message
}

# Main
try
{
    FlushIfTooBig-LogFile

    Log-Message 'Script started.'

    # Some additional parameter validation for specific actions.
    if (($action.ToLower() -eq 'create') -OR ($action.ToLower() -eq 'run'))
    {
        if (-NOT (Test-Path $destDir -PathType 'Container')) 
        { 
           throw "$($destDir) is not a valid folder" 
        }
    }

    if ($action.ToLower() -eq 'create')
    {
        if (-NOT (Test-Path $scriptPath -PathType 'Leaf')) 
        { 
           throw "$($scriptPath) is not a valid file" 
        }
    }

    # Execute specified action.
    if ($action.ToLower() -eq 'create')
    {
        Create-Task $scriptPath $arch $isDelta $destDir $daysInterval $hoursInterval
    }
    elseif ($action.ToLower() -eq 'delete')
    {
        Delete-Task $arch $isDelta
    }
    else # ($action.ToLower() -eq 'run')
    {
        Run-Task $arch $isDelta $destDir
    }

    Log-Message 'Script completed.'
}
catch [System.Exception]
{
    Log-Message $Error
}