feat: Add Mark of the Web (MotW) bypass scripts for RMM deployment#11
feat: Add Mark of the Web (MotW) bypass scripts for RMM deployment#11aybouzaglou merged 4 commits intomainfrom
Conversation
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.
Summary of ChangesHello @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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| $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" |
There was a problem hiding this comment.
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" }
| "Boolean" { | ||
| $envValue -eq 'true' -or $envValue -eq '1' -or $envValue -eq 'yes' | ||
| } |
There was a problem hiding this comment.
|
|
||
| try { | ||
| $streams = Get-Item -Path $FilePath -Stream * -ErrorAction SilentlyContinue | ||
| return ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) -ne $null |
There was a problem hiding this comment.
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'
| catch { | ||
| $results.Failed++ | ||
| } |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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.
| Required: | |
| Optional (primary operation selector, defaults applied if unset): |
| $useRecurse = ($Mode -eq "AllUserFiles") | ||
|
|
There was a problem hiding this comment.
The variable useRecurse is assigned on line 337 but never used. It should be removed to improve code maintainability.
| $useRecurse = ($Mode -eq "AllUserFiles") |
|
|
||
| try { | ||
| $streams = Get-Item -Path $FilePath -Stream * -ErrorAction SilentlyContinue | ||
| return ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) -ne $null |
There was a problem hiding this comment.
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.
| return ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) -ne $null | |
| return $null -ne ($streams | Where-Object { $_.Stream -eq 'Zone.Identifier' }) |
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| } | ||
| } | ||
| catch { | ||
| $results.Failed++ |
There was a problem hiding this comment.
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.
| $results.Failed++ | |
| $results.Failed++ | |
| Write-RMMLog "Failed to unblock file $($file.FullName): $($_.Exception.Message)" -Level Warning |
| $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 | ||
|
|
There was a problem hiding this comment.
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.
| $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 |
There was a problem hiding this comment.
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.
| $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 | |
| } | |
| } |
- 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
Add two scripts to address File Explorer preview issues for internet-downloaded files:
to prevent Windows from tagging new downloads with MotW
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.