Create-IntuneRemediation.ps1
|
<#PSScriptInfo
.VERSION 1.0.3 .GUID 3b2966d1-96be-4da2-99fe-485984dd14a4 .AUTHOR Jeff Gilbert .COMPANYNAME .COPYRIGHT .TAGS Intune .LICENSEURI .PROJECTURI .ICONURI .EXTERNALMODULEDEPENDENCIES .REQUIREDSCRIPTS .EXTERNALSCRIPTDEPENDENCIES .RELEASENOTES Version 1.0: Original published version. Version 1.0.2: Fixed minor syntax issues and made default name for remediation log the name of the workspace. Version 1.0.3: Updated default detection script output to remediation detection output that is displayed in Intune admin center. #> <# .SYNOPSIS Automates the creation of Intune remediation script pairs. .DESCRIPTION This script creates a Visual Studio Code workspace for Intune remediation scripts. It sets up the necessary folders, tasks, and scripts for creating and managing detection and remediation scripts. .PARAMETER outDir The output directory where the workspace and files will be created. Default is the location the IntuneRemediations folder in the My Documents directory. .PARAMETER workSpaceName The name of the workspace to be created. Default is "MyRemediation". .EXAMPLE .\IntuneRemediationSetup.ps1 -outDir "IntuneRemediations" -workSpaceName "MyRemediation" Creates a workspace named "MyRemediation" in the "Documents\IntuneRemediations" directory. .NOTES For best results, run this script in a PowerShell terminal with administrative privileges. Ensure that the required tools and dependencies are installed and available in the system PATH. A scratch directory will be created in the specified output directory for storing test files. #> param ( [Parameter(Mandatory=$false)] [string]$workSpaceName = "MyRemediation", [Parameter(Mandatory=$false)] [string]$outDir = "IntuneRemediations" ) $myDocs = [Environment]::GetFolderPath("MyDocuments") $outDir = Join-Path $myDocs $outDir $path = Join-Path $outDir $workSpaceName if (-not (Test-Path -Path $path)) { New-Item -Path $path -ItemType Directory | Out-Null } else { $rand = Get-Random -Minimum 1 -Maximum 100 $workSpaceName = $workSpaceName + "-" + $rand $path = Join-Path $outDir $workSpaceName } # Create the scripts and scratch folders New-Item -ItemType Directory -Path $path\scripts | Out-Null New-Item -ItemType Directory -Path $path\scratch | Out-Null # Define the folders to include in the workspace $folders = @( @{ path = $path } ) # Define workspace settings (optional) $settings = @{ "editor.tabSize" = 4 "files.exclude" = @{ "*.code-workspace" = $true ".vscode" = $true ".git" = $true } } $tasks = @{ version = "2.0.0" tasks = @( @{ label = "Generate example file" type = "shell" command = "./.vscode/makeExample.ps1" problemMatcher = "[]" } ) } # Create the workspace JSON structure $workspace = @{ folders = $folders settings = $settings tasks = $tasks } # Convert the workspace structure to JSON $workspaceJson = $workspace | ConvertTo-Json -Depth 10 -Compress # Write the JSON to the .code-workspace file Write-Output "Creating $workSpaceName workspace file..." $workspaceFilePath = Join-Path $path "$workSpaceName.code-workspace" Set-Content -Path $workspaceFilePath -Value $workspaceJson -Encoding UTF8 | Out-Null Write-Host " $workSpaceName.code-workspace created at $outDir" # Put shortcut in outdir $WshShell = New-Object -ComObject WScript.Shell $Shortcut = $WshShell.CreateShortcut("$outDir\$workSpaceName.lnk") $Shortcut.TargetPath = "$workspaceFilePath" $Shortcut.Save() $taskFolderPath = Join-Path $path ".vscode" # Ensure the .vscode folder exists if (-not (Test-Path -Path $taskFolderPath)) { New-Item -ItemType Directory -Path $taskFolderPath | Out-Null $hide = Get-Item $taskFolderPath -Force $hide.attributes = 'Hidden' } # -------------------------------------------- Create the detection.ps1 file ------------------------------------------- $detectionScript = @" <# .SYNOPSIS .DESCRIPTION .PARAMETER .EXAMPLE .NOTES #> #-------------------------------------------------------- Functions --------------------------------------------------------- #------------------------------------------------------- Begin Script ------------------------------------------------------- <# No logging performed by default for detection scripts, as they do not make changes to the device. Use write-host or write-output to display messages in the Intune portal's remediation device status (Pre/Post-remediation detection output column). #> try { # Your script logic goes here. `$someCondition = 'xyz' # This is just a placeholder for something you're checking for in your detection logic #----- Test if a specific condition is met ----- if (`$someCondition -eq 'xyz') { Write-Host "No issues detected." ; Exit 0 # This shows up as device status in the remediation detection output column. }Else { Write-Host "Remediation necessary." ; Exit 1 # This shows up as device status in the remediation detection output column. } } catch { # Do this if a terminating exception happens Write-Error "An error occurred: `$_" } Exit "@ $detectionScript | Out-File -FilePath $path\scripts\$workSpaceName"_detection.ps1" -Encoding UTF8 -Force Write-Output " Default detection script created" $logFileName = $workspacename + "_remediation.log" $remediationScript = @" <# .SYNOPSIS .DESCRIPTION .EXAMPLE .OUTPUTS .NOTES Author: Version: Requirements: Logging: #> # ------------------------------ Configuration & Logging ------------------------------ # IME diagnostics directory and transcript path `$logDir = Join-Path -Path `$env:ProgramData -ChildPath 'Microsoft\IntuneManagementExtension\Logs' # This path is pulled when Intune collects diagnostics. `$logFile = Join-Path -Path `$logDir -ChildPath '$logFileName' # Default log name is the name of this remediation workspace. #-------------------------------------- Functions ------------------------------------- function Log { [CmdletBinding()] Param( [Parameter(Mandatory = `$true, Position = 0)] [ValidateNotNullOrEmpty()] [string] `$Message, [Parameter(Mandatory = `$false, Position = 1)] [string] `$Component = 'Remediation', [Parameter(Mandatory = `$false, Position = 2)] [ValidateSet(1, 2, 3)] [int] `$Type = 1 ) # Capture once to avoid drift between Time/Date `$now = Get-Date `$Time = `$now.ToString('HH:mm:ss.ffffff', [System.Globalization.CultureInfo]::InvariantCulture) `$Date = `$now.ToString('MM-dd-yyyy', [System.Globalization.CultureInfo]::InvariantCulture) # Escape message for CMTrace payload (XML-like). SecurityElement.Escape handles &, <, >, " (quotes) safely `$escapedMessage = [System.Security.SecurityElement]::Escape(`$Message) # Identify if it was a function that called Log and make it the component; default is "Remediation" `$scriptFullPath = `$PSCommandPath ; `$scriptName = Split-Path -Path `$scriptFullPath -Leaf `$caller = (Get-PSCallStack)[1].Command if (-not (`$caller -eq `$scriptName)){ `$Component = `$caller } # Build CMTrace entry (keep empty attributes as per CMTrace spec) `$logLine = ('<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">' `` -f `$escapedMessage, `$Time, `$Date, `$Component, `$Type) try { # Write the log entry `$dir = Split-Path -Path `$LogFile -Parent # Ensure directory exists if (`$dir -and -not (Test-Path -LiteralPath `$dir)) { New-Item -ItemType Directory -Path `$dir -Force | Out-Null } Add-Content -LiteralPath `$LogFile -Value `$logLine -Encoding UTF8 } catch { # If writing fails write error Write-Error -Message ("Failed to write log to '{0}': {1}" -f `$LogFile, `$_.Exception.Message) return } # Write all log lines to console for interactive testing/transcript capture switch (`$Type) { 1 { Write-Host `$Message -ForegroundColor Gray } 2 { Write-Host `$Message -ForegroundColor Yellow } 3 { Write-Host `$Message -ForegroundColor Red } default { Write-Host `$Message } } } # --------------------------------------- Main ---------------------------------------- `$ErrorActionPreference = 'Stop' # Ensure non-terminating errors become terminating for try/catch try { # Your script logic goes here } catch { Log "An error occurred: `$_" } Exit "@ $remediationScript | Out-File -FilePath $path\scripts\$workSpaceName"_remediation.ps1" -Encoding UTF8 -Force Write-Output " Default remediation script created" #--------------------------------------------- Create Example File -------------------------------------------- $makeApp = @" `$makeExample = @" # detection.ps1 # Detect if Notepad++ is installed ```$AppName = "Notepad++" ```$Installed = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { ```$_.DisplayName -like "*```$AppName*" } if (```$Installed) { Write-Output "```$AppName is installed." exit 0 # Detection: compliant } else { Write-Output "```$AppName is NOT installed." exit 1 # Detection: non-compliant } # remediation.ps1 # Install Notepad++ if not present ```$AppName = "Notepad++" ```$Installed = Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object { ```$_.DisplayName -like "*```$AppName*" } if (-not ```$Installed) { ```$InstallerUrl = "https://github.com/notepad-plus-plus/notepad-plus-plus/releases/latest/download/npp.8.6.7.Installer.x64.exe" ```$InstallerPath = "```$env:TEMP\npp_installer.exe" Invoke-WebRequest -Uri ```$InstallerUrl -OutFile ```$InstallerPath Start-Process -FilePath ```$InstallerPath -ArgumentList "/S" -Wait Remove-Item ```$InstallerPath -Force } `"@ `$makeExample | Out-File -FilePath "$path\scripts\examples.ps1" -Encoding UTF8 -Force "@ $makeApp | Out-File -FilePath "$taskFolderPath\makeExample.ps1" -Encoding UTF8 -Force Write-Output " Generate example file task created" # --------------------------------------------------- Finish up ------------------------------------------------------------- Write-Output "Intune remediation scripts VS Code workspace creation completed" Write-Output `n $openWorkspace = Read-Host "Do you want to open the workspace in VS Code now? [Y or N]" if ($openWorkspace -eq "Y" -or $openWorkspace -eq "y") { try { #invoke-item $workspaceFilePath -ErrorAction SilentlyContinue code $workspaceFilePath #-NoNewWindow -ErrorAction SilentlyContinue Write-Output "Workspace opened in VS Code." Exit } catch { Write-Host "Unable to open VS Code. Please check if VS Code is installed." # Uncomment the line below to see the error details # Write-Output "Error: $_" } $portable = Read-Host "Do you want to use VSCode Portable to open the workspace? [Y or N]" if ($portable -eq "Y" -or $portable -eq "y") { try { #If VS Code portable is already installed, use it. $vsCodePortablePath = "$outDir\.VSCodePortable\Code.exe" if (Test-Path $vsCodePortablePath){ Start-Process -FilePath $vsCodePortablePath -ArgumentList """$workspaceFilePath""" Write-Output "Workspace opened in VS Code Portable." Exit } } catch { #VS Code portable is NOT installed $vsCodePortablePath = "$outDir\.VSCodePortable\Code.exe" $vscodePath = Join-Path $outDir ".VSCodePortable" if (-not (Test-Path -Path $vscodePath)) { New-Item -ItemType Directory -Path $vscodePath | Out-Null } # Find processor architecture for download link https://code.visualstudio.com/download $architecture = (Get-WMIObject -Class Win32_Processor).Architecture if ($architecture -eq 9) { #x64 $vscodeZip = "https://code.visualstudio.com/sha/download?build=stable&os=win32-x64-archive" } elseif ($architecture -eq 5) { #arm64 $vscodeZip = "https://code.visualstudio.com/sha/download?build=stable&os=win32-arm64-archive" } else { Write-Host "Unsupported architecture: $architecture" -ForegroundColor Red } # Download VS Code portable .zip $vscodeFile = "VSCodePortable.zip" $vscodeOutFile = Join-Path $vscodePath $vscodeFile Write-Output " Downloading and extracting VS Code Portable..." Invoke-WebRequest -Uri $vscodeZip -OutFile $vscodeOutFile -UseBasicParsing if (-not (Test-Path -Path $vscodeOutFile)) { Write-Host "Failed to download VS Code Portable. Please check your internet connection." -ForegroundColor Red Write-Host "Direct download link is $vscodeZip" -ForegroundColor }else { # Extract the downloaded .zip file Expand-Archive -Path $vscodeOutFile -DestinationPath $vscodePath -Force # Delete .zip file after extraction Remove-Item -Path $vscodeOutFile -Force Write-Output "VS Code Portable downloaded to $vscodePath" # Open workspace? $openWorkspace = Read-Host "Do you want to open the workspace in VS Code now? [Y or N]" if ($openWorkspace -eq "Y" -or $openWorkspace -eq "y") { $vscode = Join-Path -Path $vscodePath -ChildPath "\Code.exe" start-process $vscode -ArgumentList """$workspaceFilePath""" -NoNewWindow -ErrorAction SilentlyContinue Write-Output "Workspace opened in VS Code." Write-Output "You can also open the workspace later by double-clicking the $Shortcut created in $outDir" Write-Output `n Start-Sleep -seconds 10 Exit } } } } } else { Write-Output "You chose not to open the workspace." `n Write-Output "You can open the workspace later by double-clicking the $workSpaceName workspace shortcut created in $outDir" `n Write-Output "Good-bye." `n } Exit |