Skip to content

feat: Add Mark of the Web (MotW) bypass scripts for RMM deployment#11

Merged
aybouzaglou merged 4 commits intomainfrom
claude/deploy-motw-policy-fix-eHU4T
Jan 22, 2026
Merged

feat: Add Mark of the Web (MotW) bypass scripts for RMM deployment#11
aybouzaglou merged 4 commits intomainfrom
claude/deploy-motw-policy-fix-eHU4T

Conversation

@aybouzaglou
Copy link
Owner

Add two scripts to address File Explorer preview issues for internet-downloaded files:

  • Deploy-MotW-Policy.ps1: Enables "Do not preserve zone information" policy
    to prevent Windows from tagging new downloads with MotW
  • Unblock-InternetFiles.ps1: Bulk removes MotW from existing downloaded files
    with configurable operation modes (Downloads folder or entire user profile)

Both scripts follow the repository's self-contained architecture pattern with
embedded logging functions, proper error handling, and Datto RMM environment
variable support.

Add two scripts to address File Explorer preview issues for internet-downloaded files:

- Deploy-MotW-Policy.ps1: Enables "Do not preserve zone information" policy
  to prevent Windows from tagging new downloads with MotW
- Unblock-InternetFiles.ps1: Bulk removes MotW from existing downloaded files
  with configurable operation modes (Downloads folder or entire user profile)

Both scripts follow the repository's self-contained architecture pattern with
embedded logging functions, proper error handling, and Datto RMM environment
variable support.
Replace separate Deploy-MotW-Policy.ps1 and Unblock-InternetFiles.ps1 with
unified Deploy-MotW-Fix.ps1 that supports operation selection via environment
variables:

- FixOperation: "PolicyOnly", "UnblockOnly", or "Both"
- UnblockMode: "Downloads" or "AllUserFiles"
- CustomPath: Override target path for unblock
- FileExtensions: Configurable file types to unblock

This allows deploying one component with different configurations instead of
managing two separate scripts.
@gemini-code-assist
Copy link

Summary of Changes

Hello @aybouzaglou, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a robust PowerShell script designed to resolve common File Explorer preview problems stemming from Windows' Mark of the Web security feature. The script offers a dual approach: it can configure a system policy to prevent future MotW tagging on new downloads and/or unblock existing files that have already been tagged. Tailored for Datto RMM environments, it ensures reliable and configurable deployment to manage MotW across user systems effectively.

Highlights

  • New MotW Bypass Script: Introduces a PowerShell script (Deploy-MotW-Fix.ps1) designed to resolve File Explorer preview issues caused by the Mark of the Web (MotW) security feature on internet-downloaded files.
  • Policy Deployment: The script can configure a registry policy to prevent Windows from tagging new downloads with MotW, effectively stopping future issues.
  • Bulk File Unblocking: It provides functionality to bulk remove MotW from existing downloaded files, with options to target specific file types in the Downloads folder or recursively scan the entire user profile.
  • RMM Integration: The script is built for Datto RMM deployment, featuring embedded logging, robust error handling, and support for RMM environment variables for configurable execution.
  • Configurable Operations: Users can choose between 'PolicyOnly', 'UnblockOnly', or 'Both' operations, allowing for flexible deployment strategies (e.g., initial remediation vs. ongoing prevention).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive PowerShell script to manage the Mark of the Web (MotW) by setting a system policy and unblocking existing files. The script is well-structured, with clear separation of concerns into different functions and detailed comment-based help.

My review focuses on improving the script's robustness, diagnostics, and adherence to PowerShell best practices. Key suggestions include:

  • Correcting a bug in parameter handling that causes command-line arguments to be ignored.
  • Enhancing error logging to provide more context when unblocking a file fails.
  • Improving the robustness of boolean environment variable parsing.
  • Minor code cleanup and performance optimizations.

Overall, this is a solid contribution that addresses a common Windows usability issue. The suggested changes will make the script more reliable and easier to troubleshoot.

Comment on lines 415 to 418
$FixOperation = Get-RMMVariable -Name "FixOperation" -Default "Both"
$UnblockMode = Get-RMMVariable -Name "UnblockMode" -Default "Downloads"
$CustomPath = Get-RMMVariable -Name "CustomPath" -Default ""
$FileExtensions = Get-RMMVariable -Name "FileExtensions" -Default "pdf,docx,xlsx,pptx,doc,xls,ppt"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

These lines re-process the configuration from environment variables, overwriting the values that were already populated by the param block. This introduces a bug where any parameters passed directly on the command line (e.g., for local testing) will be ignored.

The param block should be the single source of truth for script parameters. You should remove these redundant assignments and instead check if the parameters are empty and assign default values if needed. This makes the script more robust and behave as expected when used both with environment variables and direct command-line parameters.

if ([string]::IsNullOrWhiteSpace($FixOperation)) { $FixOperation = "Both" }
if ([string]::IsNullOrWhiteSpace($UnblockMode)) { $UnblockMode = "Downloads" }
if ([string]::IsNullOrWhiteSpace($FileExtensions)) { $FileExtensions = "pdf,docx,xlsx,pptx,doc,xls,ppt" }

Comment on lines 136 to 138
"Boolean" {
$envValue -eq 'true' -or $envValue -eq '1' -or $envValue -eq 'yes'
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The boolean conversion logic is case-sensitive, which can lead to unexpected behavior if the environment variable is set to, for example, True or Yes. Using a case-insensitive comparison will make this function more robust.

        "Boolean" {
            @('true', '1', 'yes') -contains $envValue.ToLowerInvariant()
        }


try {
$streams = Get-Item -Path $FilePath -Stream * -ErrorAction SilentlyContinue
return ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) -ne $null

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation uses a pipeline with Where-Object to check for the existence of the 'Zone.Identifier' stream. This can be simplified and made more efficient by using the -contains operator directly on the Stream property of the collection. This avoids the overhead of the pipeline for a simple existence check.

        return $streams.Stream -contains 'Zone.Identifier'

Comment on lines 302 to 304
catch {
$results.Failed++
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The catch block correctly increments the failure count, but it doesn't log any information about which file failed or the reason for the failure. This makes troubleshooting difficult, especially when the script processes a large number of files. Adding a log message here with the file path and exception message would greatly improve diagnostics.

            catch {
                Write-RMMLog "Failed to unblock file '$($file.FullName)': $($_.Exception.Message)" -Level Warning
                $results.Failed++
            }

$totalFailed = 0
$totalSkipped = 0

$useRecurse = ($Mode -eq "AllUserFiles")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The variable $useRecurse is assigned a value based on the $Mode but is never used anywhere else in the function. This unused variable should be removed to improve code clarity and maintainability.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a comprehensive PowerShell script to address Mark of the Web (MotW) issues that prevent File Explorer preview functionality for internet-downloaded files. The script provides configurable operations to either deploy a registry policy that prevents MotW tagging on new downloads, bulk-remove MotW from existing files, or perform both operations.

Changes:

  • Added Deploy-MotW-Fix.ps1 with dual functionality: policy deployment and file unblocking
  • Implemented configurable operation modes (PolicyOnly, UnblockOnly, Both) with validation
  • Follows repository's self-contained architecture pattern with embedded logging and helper functions

Changeable: Yes

.ENVIRONMENT VARIABLES
Required:
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states FixOperation is a required environment variable, but the code provides a default value of "Both" making it optional. This inconsistency between documentation and implementation could confuse users. Either mark it as Optional in the documentation or remove the default value.

Suggested change
Required:
Optional (primary operation selector, defaults applied if unset):

Copilot uses AI. Check for mistakes.
Comment on lines +337 to +338
$useRecurse = ($Mode -eq "AllUserFiles")

Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable useRecurse is assigned on line 337 but never used. It should be removed to improve code maintainability.

Suggested change
$useRecurse = ($Mode -eq "AllUserFiles")

Copilot uses AI. Check for mistakes.

try {
$streams = Get-Item -Path $FilePath -Stream * -ErrorAction SilentlyContinue
return ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) -ne $null
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comparison operator uses '-ne $null' which should follow PowerShell best practices. The preferred pattern is to use '$null -ne' (placing $null on the left side) to avoid potential issues with collections and to make the intent clearer.

Suggested change
return ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) -ne $null
return $null -ne ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' })

Copilot uses AI. Check for mistakes.
Comment on lines +511 to +516
if ($policySuccess -and $unblockSuccess) {
Write-RMMLog ""
Write-RMMLog "All operations completed successfully" -Level Success
Write-RMMLog "File Explorer preview should now work for internet-downloaded files" -Level Info
$exitCode = 0
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a logical issue with the exit code handling. If policySuccess is false but unblockSuccess is true, the code sets exitCode to 1 (line 474). However, later at line 515, if both operations are successful, exitCode is unconditionally set to 0, which contradicts the earlier logic. The condition at line 511 should check if exitCode is still 0 before resetting it, or the final success message and exit code 0 should only be set when there were no prior failures.

Copilot uses AI. Check for mistakes.
}
}
catch {
$results.Failed++
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When an individual file fails to unblock (line 302-304), the error is silently caught and only increments the Failed counter. This makes troubleshooting difficult as there's no indication of which files failed or why. Consider logging at least a warning message with the file path and exception message to help users identify problematic files.

Suggested change
$results.Failed++
$results.Failed++
Write-RMMLog "Failed to unblock file $($file.FullName): $($_.Exception.Message)" -Level Warning

Copilot uses AI. Check for mistakes.
Comment on lines +200 to +223
$RegPath = "HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments"
$RegName = "SaveZoneInformation"
$RegValue = 1

Write-RMMLog "Registry Path: $RegPath" -Level Config
Write-RMMLog "Setting: $RegName = $RegValue (Do not preserve zone information)" -Level Config

# Check current state
$currentExists = Test-Path $RegPath
if ($currentExists) {
$currentValue = (Get-ItemProperty -Path $RegPath -Name $RegName -ErrorAction SilentlyContinue).$RegName
if ($null -ne $currentValue) {
Write-RMMLog "Current policy value: $currentValue" -Level Detect
if ($currentValue -eq $RegValue) {
Write-RMMLog "Policy is already configured correctly" -Level Success
return $true
}
}
}

# Apply policy
Write-RMMLog "Applying MotW bypass policy..." -Level Status
$setResult = Set-MotWPolicy -RegistryPath $RegPath -RegistryName $RegName -RegistryValue $RegValue

Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script configures HKCU:\Software\Microsoft\Windows\CurrentVersion\Policies\Attachments with SaveZoneInformation = 1, which enables the "Do not preserve zone information" policy and effectively disables Mark-of-the-Web tagging for new downloads. With this policy set, files obtained from the internet (e.g., Office documents with macros or other active content) will no longer be tagged as untrusted, reducing or bypassing protections such as Protected View and SmartScreen prompts and making it easier for attackers to execute malicious content via user interaction. Consider limiting this behavior (e.g., via narrower scoping or temporary/opt-in application) rather than globally disabling zone information preservation for all future downloads.

Copilot uses AI. Check for mistakes.
Comment on lines +337 to +347
$useRecurse = ($Mode -eq "AllUserFiles")

if ($Mode -eq "AllUserFiles") {
Write-RMMLog "Scanning entire user profile recursively (this may take several minutes)..." -Level Status

$results = Invoke-UnblockByPattern -TargetPath $TargetPath -Pattern "*" -Recurse $true

$totalFound = $results.Found
$totalUnblocked = $results.Unblocked
$totalFailed = $results.Failed
$totalSkipped = $results.Skipped
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In AllUserFiles mode the unblock logic calls Invoke-UnblockByPattern with Pattern "*" and recursion over the entire user profile, which strips the Zone.Identifier stream from every file that has Mark-of-the-Web, including executables and scripts. This mass unblocking removes Windows' untrusted-origin markings from all user content, so any already-downloaded malware or script payloads will appear as local, trusted files and can be executed with fewer or no security prompts. Consider constraining this mode to a strict allowlist of safe extensions or otherwise limiting its scope so that not all file types across the profile are unblocked.

Suggested change
$useRecurse = ($Mode -eq "AllUserFiles")
if ($Mode -eq "AllUserFiles") {
Write-RMMLog "Scanning entire user profile recursively (this may take several minutes)..." -Level Status
$results = Invoke-UnblockByPattern -TargetPath $TargetPath -Pattern "*" -Recurse $true
$totalFound = $results.Found
$totalUnblocked = $results.Unblocked
$totalFailed = $results.Failed
$totalSkipped = $results.Skipped
if ($Mode -eq "AllUserFiles") {
Write-RMMLog "Scanning entire user profile recursively (this may take several minutes)..." -Level Status
Write-RMMLog "File patterns (AllUserFiles): $($FilePatterns -join ', ')" -Level Config
foreach ($pattern in $FilePatterns) {
$results = Invoke-UnblockByPattern -TargetPath $TargetPath -Pattern $pattern -Recurse $true
$totalFound += $results.Found
$totalUnblocked += $results.Unblocked
$totalFailed += $results.Failed
$totalSkipped += $results.Skipped
if ($results.Found -gt 0) {
Write-RMMLog "AllUserFiles [$pattern] - Found: $($results.Found), Unblocked: $($results.Unblocked)" -Level Info
}
}

Copilot uses AI. Check for mistakes.
- Simplify Test-FileHasMotW using -contains operator instead of pipeline
- Make boolean conversion case-insensitive in Get-RMMVariable
- Fix parameter handling to respect command-line values over env vars
@aybouzaglou aybouzaglou merged commit 0657ee7 into main Jan 22, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants