├── Applocker ├── Create_Applocker_Intune_policy.ps1 └── README.md ├── Apps ├── Audacity_3.5.1_Intune_Win32_Custom_DetectionCheck_Script.ps1 ├── Create_Multiple_IntuneMobileAppAssignments_Out-GridView.ps1 ├── Delete_Multiple_IntuneMobileAppAssignments_Out-GridView.ps1 ├── README.md ├── Show_IntuneMobileAppAssignments_to_selected_AzureADGroup_in_Out-GridView.ps1 ├── install_Audacity_3.5.1-EXE-example.ps1 └── install_LibreOffice_7.0.6-MSI-example.ps1 ├── AutopilotBranding-Yodamiitti └── README.md ├── CVE-2023-24932 ├── BlackLotus-Intune-Remediation-screenshot.png ├── Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-1of3-Check if new 2023 certificate is installed to UEFI db.ps1 ├── Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-2of3-Check if new 2023 certificate is installed to Windows Boot Manager.ps1 ├── Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-3of3-Check if old compromised 2011 certificate is revocaked in UEFI.ps1 └── readme.md ├── Device_Query └── readme.MD ├── Devices ├── Intune_Device_SendCustomNotification.ps1 └── README.md ├── Groups └── README.md ├── Linux ├── Intune_Linux_Custom_Compliance_script.sh ├── Intune_Linux_Custom_Compliance_script_Rules_file.json ├── README.md └── pics │ ├── IntuneAddCustomCompliancePolicySelectFiles.png │ ├── IntuneAddCustomCompliancePolicySettingsPicker.png │ ├── IntuneAddLinuxCustomComplianceScript.png │ ├── Intune_Linux_Compliance_Policy.png │ ├── Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant.png │ ├── Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant_small.png │ ├── Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant.png │ └── Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant_small.png ├── Microsoft_Graph_API ├── AppRegistrations_Secret_Expiry_Report_v1.ps1 ├── AppRegistrations_Secret_Expiry_Report_v2.ps1 ├── AppRegistrations_Secret_Expiry_Report_v3_Using_Certificate.ps1 ├── AppRegistrations_Secret_Expiry_Report_v6_Using_AppSecret_and_Intune_PowershellModule.ps1 ├── Azure_Runbook_using_CredentialsVariable.ps1 ├── CreateApplicationCertificate.ps1 ├── Intune_Devices_Sync_Report_Report_v4_Using_Certificate.ps1 ├── Intune_Devices_Sync_Report_Report_v5_Using_Secret.ps1 ├── Microsoft_Graph_API_urls.ps1 └── README.md ├── Powershell_Commands ├── Intune_Powershell_Commands_Examples.ps1 └── README.md ├── Printing └── README.md ├── README.md ├── Remediations ├── Disable-Builtin-Administrator-account-Detection.ps1 ├── Enable-Builtin-Administrator-account-Detection.ps1 ├── Get-NetworkInformation-Detection.ps1 ├── Get-Windows-SKU-Enterprise_or_Education-Detection.ps1 ├── Get-Windows-licensing-activation-status-Detection.ps1 ├── List_WLAN_profiles_Detection.ps1 ├── Set-TimeZone Win32App Remediation │ ├── 01-Requirements-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1 │ ├── 02-Detection-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1 │ ├── Set-TimeZone-to-FLEStandardTime-Win32-Remediation-Install-script.intunewin │ ├── Win32 App notes.txt │ ├── logo.png │ └── source │ │ └── Set-TimeZone-to-FLEStandardTime-Win32-Remediation-Install-script.ps1 ├── Set-TimeZone-to-FLEStandardTime-Detection.ps1 ├── Set-TimeZone-to-GreenwichStandardTime-Detection.ps1 ├── WindowsUpdateRegistryValuesCheck-Detection.ps1 ├── WindowsUpdateRegistryValuesCheck-Remediate.ps1 └── readme.MD ├── Reports ├── Create-IntuneAppAssignmentsReport.ps1 ├── Create_IntuneConfigurationAssignments_HTML_Report.ps1 ├── Create_IntuneMobileAppAssignments_HTML_Report.ps1 ├── README.md └── pics │ ├── 01-DeviceConfigurationsReport_Overview.png │ ├── 02-DeviceConfigurationsReport_Platform_by_profilename.png │ ├── 03-DeviceConfigurationsReport_Targeted_to_AzureADGroup.png │ ├── IntuneApplicationAssignmentReport.png │ ├── IntuneApplicationAssignmentReportSortByAssignmentGroup.png │ └── README.md └── Troubleshooting ├── README.md ├── Show-IntuneIMELogsInOutGridView.ps1 └── pics ├── 01-Show-IntuneIMELogsInOutGridView.png ├── 02-Show-IntuneIMELogsInOutGridView.png └── README.md /Applocker/Create_Applocker_Intune_policy.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | This script reads Applocker exported XML rules file and creates Intune Applocker configuration profile. 4 | 5 | .DESCRIPTION 6 | This script reads Applocker exported XML rules file and creates Intune Applocker custom oma-uri configuration profile. 7 | 8 | Script makes XML syntax check first. Only Enabled or AuditOnly mode rules are created to Intune. NotConfigured rules are ignored. 9 | 10 | Double check Intune rules configurations before applying to devices. Always test first on test devices before applying policy to production devices. 11 | 12 | Created json files (Intune Applocker configuration profile) and Graph API response are saved in current directory. 13 | 14 | 15 | Script uses Microsoft Intune Powershell management module. 16 | 17 | You can install Intune Powershell module to your user account without Administrator permissions with command: 18 | Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser 19 | 20 | 21 | Author: 22 | Petri.Paavola@yodamiitti.fi 23 | Senior Modern Management Principal 24 | Microsoft MVP - Windows and Devices for IT 25 | 26 | 2023-04-24 27 | 28 | Version 1.1 29 | 30 | https://github.com/petripaavola/Intune/tree/master/Applocker 31 | 32 | .PARAMETER FilePath 33 | Applocker exported rules XML file path 34 | 35 | .PARAMETER WhatIf 36 | Does XML file syntax check and creates rules json file. Filename is like: Applocker Policy - Created 2023-04-24 102623 - WhatIf.json 37 | 38 | .EXAMPLE 39 | .\Create_Applocker_Intune_policy.ps1 .\AppLocker.xml 40 | .EXAMPLE 41 | .\Create_Applocker_Intune_policy.ps1 -Filepath .\AppLocker.xml 42 | .EXAMPLE 43 | .\Create_Applocker_Intune_policy.ps1 -Filepath .\AppLocker.xml -WhatIf 44 | 45 | .INPUTS 46 | .OUTPUTS 47 | Script creates 2 json files. Intune Applocker XML configuration json file and Microsoft Graph API response json file. 48 | 49 | With option -WhatIf script creates Intune Applocker XML configuration json file which ends with -WhatIf.json 50 | .NOTES 51 | .LINK 52 | https://github.com/petripaavola/Intune/tree/master/Applocker 53 | #> 54 | 55 | [CmdletBinding()] 56 | Param( 57 | [Parameter(Mandatory=$True, 58 | HelpMessage = 'Enter Applocker XML file fullpath', 59 | ValueFromPipeline=$true, 60 | ValueFromPipelineByPropertyName=$true)] 61 | [Alias("XMLFilePath")] 62 | [String]$FilePath, 63 | [Parameter(Mandatory=$False, 64 | ValueFromPipeline=$true, 65 | ValueFromPipelineByPropertyName=$true)] 66 | [Switch]$WhatIf 67 | ) 68 | 69 | # Script version 70 | $Version='1.1' 71 | 72 | 73 | # region Functions 74 | Function Validate-XMLSyntax { 75 | Param( 76 | $XMLFilePath 77 | ) 78 | 79 | Try { 80 | [xml]$xmlTest = Get-Content $XMLFilePath -Encoding UTF8 -Raw 81 | return $True 82 | } catch { 83 | Write-Host "Error:`n$($_.Exception.Message)" -ForegroundColor Red 84 | return $False 85 | } 86 | } 87 | 88 | # Create a helper function to format the XmlNode string indents 89 | # 90 | # This function was provided and automatically documented by ChatGPT-4 91 | # Good example of AI usage in coding 92 | function FormatXmlNodeString { 93 | Param( 94 | $node 95 | ) 96 | 97 | $xmlString = $node.OuterXml 98 | 99 | # Add newline before every opening and closing tag (excluding self-closing tags) 100 | $formattedXmlString = $xmlString -replace '(?<=]+>)(?=<[^<>]+>)', "`r`n" -replace '(?<=<[^<>]+>)(?=<[^<>]+>)', "`r`n" 101 | 102 | # Initialize indent level 103 | $indent = 0 104 | 105 | # Process XML string line by line to add proper indentation 106 | $formattedXmlString = ($formattedXmlString -split "`r`n" | ForEach-Object { 107 | # If the line starts with a closing tag, decrease the indent level by 2 108 | if ($_ -match '^$') { 117 | $indent += 2 118 | } 119 | 120 | # Return the formatted line 121 | $line 122 | }) -join "`r`n" 123 | 124 | return $formattedXmlString 125 | } 126 | 127 | # endregion Functions 128 | 129 | # region main 130 | 131 | Write-Host "Import Applocker XML rules and create Applocker policy to Intune. Version $version`n" -ForegroundColor Cyan 132 | 133 | if($WhatIf) { 134 | Write-Host "-WhatIf specified so not making any changes to Intune`n" -ForegroundColor Yellow 135 | } 136 | 137 | Write-Host "Processing file: $FilePath`n" 138 | 139 | # Check that XML file exists 140 | if(-not (Test-Path $FilePath)) { 141 | Write-Host "Could not find XML file: $FilePath" -ForegroundColor Red 142 | Write-Host "Script will exit`n" -ForegroundColor Yellow 143 | Exit 1 144 | } 145 | 146 | # Check XML file syntax 147 | Write-Host "Validate Applocker XML file syntax" 148 | if(Validate-XMLSyntax $FilePath) { 149 | Write-Host "OK`n" -ForegroundColor Green 150 | } Else { 151 | Write-Host "FAILED: XML syntax is NOT valid`n" -ForegroundColor Red 152 | Write-Host "Script will exit`n" -ForegroundColor Yellow 153 | Exit 1 154 | } 155 | 156 | # Read XML file string to variable casted as XML type 157 | [xml]$ApplockerXML = Get-Content $FilePath -Encoding UTF8 -Raw 158 | 159 | Write-Host "Import Applocker configurations from XML file (EXE/MSI/Appx/Script/DLL)" 160 | 161 | # Extract RuleCollections 162 | $exeRuleCollection = $ApplockerXML.AppLockerPolicy.RuleCollection | Where-Object { $_.Type -eq 'Exe' -and (($_.EnforcementMode -eq 'Enabled') -or ($_.EnforcementMode -eq 'AuditOnly'))} 163 | $msiRuleCollection = $ApplockerXML.AppLockerPolicy.RuleCollection | Where-Object { $_.Type -eq 'Msi' -and (($_.EnforcementMode -eq 'Enabled') -or ($_.EnforcementMode -eq 'AuditOnly')) } 164 | $scriptRuleCollection = $ApplockerXML.AppLockerPolicy.RuleCollection | Where-Object { $_.Type -eq 'Script' -and (($_.EnforcementMode -eq 'Enabled') -or ($_.EnforcementMode -eq 'AuditOnly'))} 165 | $dllRuleCollection = $ApplockerXML.AppLockerPolicy.RuleCollection | Where-Object { $_.Type -eq 'Dll' -and (($_.EnforcementMode -eq 'Enabled') -or ($_.EnforcementMode -eq 'AuditOnly'))} 166 | $appxRuleCollection = $ApplockerXML.AppLockerPolicy.RuleCollection | Where-Object { $_.Type -eq 'Appx' -and (($_.EnforcementMode -eq 'Enabled') -or ($_.EnforcementMode -eq 'AuditOnly'))} 167 | 168 | 169 | # Format RuleCollections XML strings 170 | # Add Newlines and indents for human readability 171 | if($exeRuleCollection) { 172 | $exeRuleCollectionXmlString = FormatXmlNodeString $exeRuleCollection 173 | } else { 174 | $exeRuleCollectionXmlString = $null 175 | } 176 | 177 | if($msiRuleCollection) { 178 | $msiRuleCollectionXmlString = FormatXmlNodeString $msiRuleCollection 179 | } else { 180 | $msiRuleCollectionXmlString = $null 181 | } 182 | 183 | if($scriptRuleCollection) { 184 | $scriptRuleCollectionXmlString = FormatXmlNodeString $scriptRuleCollection 185 | } else { 186 | $scriptRuleCollectionXmlString = $null 187 | } 188 | 189 | if($dllRuleCollection) { 190 | $dllRuleCollectionXmlString = FormatXmlNodeString $dllRuleCollection 191 | } else { 192 | $dllRuleCollectionXmlString = $null 193 | } 194 | 195 | if($appxRuleCollection) { 196 | $appxRuleCollectionXmlString = FormatXmlNodeString $appxRuleCollection 197 | } else { 198 | $appxRuleCollectionXmlString = $null 199 | } 200 | 201 | Write-Host "OK`n" -ForegroundColor Green 202 | 203 | <# 204 | # DEBUG 205 | # Output XML strings 206 | Write-Output "Exe RuleCollection XML:`n$exeRuleCollectionXmlString`n" 207 | Write-Output "Msi RuleCollection XML:`n$msiRuleCollectionXmlString`n" 208 | Write-Output "Script RuleCollection XML:`n$scriptRuleCollectionXmlString`n" 209 | Write-Output "Dll RuleCollection XML:`n$dllRuleCollectionXmlString`n" 210 | Write-Output "Appx RuleCollection XML:`n$appxRuleCollectionXmlString`n" 211 | Write-Host "" 212 | #> 213 | 214 | if(-not $WhatIf) { 215 | 216 | # Connect to Intune 217 | $IntunePowershellModule = Import-Module Microsoft.Graph.Intune -PassThru -ErrorAction SilentlyContinue 218 | if (-not $IntunePowershellModule) { 219 | Write-Host "Intune Powershell module not found!`n" -ForegroundColor Red 220 | Write-Host "You can install Intune Powershell module to your user account with command:" 221 | Write-Host "Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser" -ForegroundColor Cyan 222 | Write-Host "`nor you can install machine-wide Intune module with command:`nInstall-Module -Name Microsoft.Graph.Intune`n" 223 | Exit 1 224 | } 225 | 226 | 227 | Write-Host "Connecting to Intune using Powershell Intune-Module" 228 | Update-MSGraphEnvironment -SchemaVersion 'beta' 229 | $Success = $? 230 | 231 | if (-not $Success) { 232 | Write-Host "Failed to update MSGraph Environment schema to Beta!`n" -ForegroundColor Red 233 | Write-Host "Make sure you have installed Intune Powershell module" 234 | 235 | Write-Host "You can install Intune Powershell module to your user account with command:" 236 | Write-Host "Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser" -ForegroundColor Cyan 237 | Write-Host "`nor you can install machine-wide Intune module with command:`nInstall-Module -Name Microsoft.Graph.Intune`n" 238 | Exit 1 239 | } 240 | 241 | $MSGraphEnvironment = Connect-MSGraph 242 | $Success = $? 243 | 244 | if ($Success -and $MSGraphEnvironment) { 245 | $TenantId = $MSGraphEnvironment.tenantId 246 | $AdminUserUPN = $MSGraphEnvironment.upn 247 | 248 | Write-Host "Connected to Microsoft Intune / Microsoft Graph as user:`n$AdminUserUPN`n" 249 | 250 | } else { 251 | Write-Host "Could not connect to MSGraph!" -ForegroundColor Red 252 | Exit 1 253 | } 254 | } else { 255 | Write-Host "Skipping connecting to Intune Graph API management because -WhatIf is specified`n" -ForegroundColor Yellow 256 | } 257 | 258 | ########################################## 259 | # Create Applocker policy to Intune 260 | 261 | $DateTime = Get-Date -Format "yyyy-MM-dd HHmmss" 262 | 263 | $DisplayName = "Applocker Policy - Created $DateTime by $AdminUserUPN" 264 | $Description = "Creation date: $DateTime`nCreated by: $AdminUserUPN`nApplocker rules uploaded with Powershell script v$Version`n`nApplocker rules configured:`n" 265 | $Grouping = Get-Date -Format "yyyyMMddHHmmss" 266 | 267 | # Applocker custom oma-uri policy JSON template 268 | # We will add all rules to this template's omaSettings property if XML found 269 | $BodyTemplate = @" 270 | { 271 | "displayName": "$DisplayName", 272 | "description": "$Description", 273 | "roleScopeTagIds": [ 274 | "0" 275 | ], 276 | "@odata.type": "#microsoft.graph.windows10CustomConfiguration", 277 | "omaSettings": [ 278 | ] 279 | } 280 | "@ 281 | 282 | $Body = $BodyTemplate | ConvertFrom-Json 283 | 284 | # Add rules XML information to Intune Policy creation request body 285 | if($exeRuleCollectionXmlString) { 286 | $ExeEnforcementMode = $exeRuleCollection.EnforcementMode 287 | Write-Host "Found Applocker EXE rules with EnforcementMode $ExeEnforcementMode" 288 | Write-Host "Add EXE rules to Intune policy creation request body" 289 | 290 | $ExeJSON = @" 291 | { 292 | "displayName": "Applocker EXE Rule with EnforcementMode $ExeEnforcementMode", 293 | "description": "", 294 | "omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/EXE$Grouping/EXE/Policy", 295 | "@odata.type": "#microsoft.graph.omaSettingString", 296 | "value": "" 297 | } 298 | "@ 299 | 300 | # Create Powershell object from JSON 301 | $ExeRule = $ExeJSON | ConvertFrom-Json 302 | 303 | # Add rule XML value to object 304 | $ExeRule.value = $exeRuleCollectionXmlString 305 | 306 | # Add EXE Rule to Body object 307 | $Body.omaSettings += $ExeRule 308 | $Success = $? 309 | 310 | if($Success) { 311 | Write-Host "OK`n" -ForegroundColor Green 312 | 313 | # Add rule type and enforcement information to Description" 314 | $Body.Description += "`tEXE with EnforcementMode $ExeEnforcementMode`n" 315 | } else { 316 | Write-Host "Failed" -ForegroundColor Red 317 | Write-Host "Script will exit`n" -ForegroundColor Yellow 318 | Exit 1 319 | } 320 | 321 | } else { 322 | Write-Host "Did NOT find Applocker EXE rules with EnforcementMode Enabled or AuditOnly`n" -ForegroundColor Yellow 323 | } 324 | 325 | 326 | if($msiRuleCollectionXmlString) { 327 | $MsiEnforcementMode = $msiRuleCollection.EnforcementMode 328 | Write-Host "Found Applocker MSI rules with EnforcementMode $MsiEnforcementMode" 329 | Write-Host "Add MSI rules to Intune policy creation request body" 330 | 331 | $MsiJSON = @" 332 | { 333 | "displayName": "Applocker MSI Rule with EnforcementMode $MsiEnforcementMode", 334 | "description": "", 335 | "omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/MSI$Grouping/MSI/Policy", 336 | "@odata.type": "#microsoft.graph.omaSettingString", 337 | "value": "" 338 | } 339 | "@ 340 | 341 | # Create Powershell object from JSON 342 | $MsiRule = $MsiJSON | ConvertFrom-Json 343 | 344 | # Add rule XML value to object 345 | $MsiRule.value = $msiRuleCollectionXmlString 346 | 347 | # Add MSI Rule to Body object 348 | $Body.omaSettings += $MsiRule 349 | $Success = $? 350 | 351 | if($Success) { 352 | Write-Host "OK`n" -ForegroundColor Green 353 | 354 | # Add rule type and enforcement information to Description" 355 | $Body.Description += "`tMSI with EnforcementMode $MsiEnforcementMode`n" 356 | } else { 357 | Write-Host "Failed" -ForegroundColor Red 358 | Write-Host "Script will exit`n" -ForegroundColor Yellow 359 | Exit 1 360 | } 361 | 362 | } else { 363 | Write-Host "Did NOT find Applocker MSI rules with EnforcementMode Enabled or AuditOnly`n" -ForegroundColor Yellow 364 | } 365 | 366 | 367 | if($scriptRuleCollectionXmlString) { 368 | $scriptEnforcementMode = $scriptRuleCollection.EnforcementMode 369 | Write-Host "Found Applocker Script rules with EnforcementMode $scriptEnforcementMode" 370 | Write-Host "Add Script rules to Intune policy creation request body" 371 | 372 | $ScriptJSON = @" 373 | { 374 | "displayName": "Applocker Script Rule with EnforcementMode $scriptEnforcementMode", 375 | "description": "", 376 | "omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Script$Grouping/Script/Policy", 377 | "@odata.type": "#microsoft.graph.omaSettingString", 378 | "value": "" 379 | } 380 | "@ 381 | 382 | # Create Powershell object from JSON 383 | $ScriptRule = $ScriptJSON | ConvertFrom-Json 384 | 385 | # Add rule XML value to object 386 | $ScriptRule.value = $scriptRuleCollectionXmlString 387 | 388 | # Add MSI Rule to Body object 389 | $Body.omaSettings += $ScriptRule 390 | $Success = $? 391 | 392 | if($Success) { 393 | Write-Host "OK`n" -ForegroundColor Green 394 | 395 | # Add rule type and enforcement information to Description" 396 | $Body.Description += "`tScript with EnforcementMode $ScriptEnforcementMode`n" 397 | } else { 398 | Write-Host "Failed" -ForegroundColor Red 399 | Write-Host "Script will exit`n" -ForegroundColor Yellow 400 | Exit 1 401 | } 402 | 403 | } else { 404 | Write-Host "Did NOT find Applocker Script rules with EnforcementMode Enabled or AuditOnly`n" -ForegroundColor Yellow 405 | } 406 | 407 | 408 | if($dllRuleCollectionXmlString) { 409 | $dllEnforcementMode = $dllRuleCollection.EnforcementMode 410 | Write-Host "Found Applocker DLL rules with EnforcementMode $dllEnforcementMode" 411 | Write-Host "Add DLL rules to Intune policy creation request body" 412 | 413 | $DLLJSON = @" 414 | { 415 | "displayName": "Applocker DLL Rule with EnforcementMode $dllEnforcementMode", 416 | "description": "", 417 | "omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/DLL$Grouping/DLL/Policy", 418 | "@odata.type": "#microsoft.graph.omaSettingString", 419 | "value": "" 420 | } 421 | "@ 422 | 423 | # Create Powershell object from JSON 424 | $DLLRule = $DLLJSON | ConvertFrom-Json 425 | 426 | # Add rule XML value to object 427 | $DLLRule.value = $dllRuleCollectionXmlString 428 | 429 | # Add MSI Rule to Body object 430 | $Body.omaSettings += $DLLRule 431 | $Success = $? 432 | 433 | if($Success) { 434 | Write-Host "OK`n" -ForegroundColor Green 435 | 436 | # Add rule type and enforcement information to Description" 437 | $Body.Description += "`tDLL with EnforcementMode $dllEnforcementMode`n" 438 | } else { 439 | Write-Host "Failed" -ForegroundColor Red 440 | Write-Host "Script will exit`n" -ForegroundColor Yellow 441 | Exit 1 442 | } 443 | 444 | } else { 445 | Write-Host "Did NOT find Applocker DLL rules with EnforcementMode Enabled or AuditOnly`n" -ForegroundColor Yellow 446 | } 447 | 448 | 449 | if($appxRuleCollectionXmlString) { 450 | $appxEnforcementMode = $appxRuleCollection.EnforcementMode 451 | Write-Host "Found Applocker Appx rules with EnforcementMode $appxEnforcementMode" 452 | Write-Host "Add Appx rules to Intune policy creation request body" 453 | 454 | $AppxJSON = @" 455 | { 456 | "displayName": "Applocker Appx Rule with EnforcementMode $appxEnforcementMode", 457 | "description": "", 458 | "omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Appx$Grouping/StoreApps/Policy", 459 | "@odata.type": "#microsoft.graph.omaSettingString", 460 | "value": "" 461 | } 462 | "@ 463 | 464 | # Create Powershell object from JSON 465 | $AppxRule = $AppxJSON | ConvertFrom-Json 466 | 467 | # Add rule XML value to object 468 | $AppxRule.value = $appxRuleCollectionXmlString 469 | 470 | # Add MSI Rule to Body object 471 | $Body.omaSettings += $AppxRule 472 | $Success = $? 473 | 474 | if($Success) { 475 | Write-Host "OK`n" -ForegroundColor Green 476 | 477 | # Add rule type and enforcement information to Description" 478 | $Body.Description += "`tAppx with EnforcementMode $appxEnforcementMode`n" 479 | 480 | } else { 481 | Write-Host "Failed" -ForegroundColor Red 482 | Write-Host "Script will exit`n" -ForegroundColor Yellow 483 | Exit 1 484 | } 485 | 486 | } else { 487 | Write-Host "Did NOT find Applocker Appx rules with EnforcementMode Enabled or AuditOnly`n" -ForegroundColor Yellow 488 | } 489 | 490 | 491 | $BodyJson = $Body | ConvertTo-Json -Depth 5 492 | 493 | # Save JSON to file 494 | if($WhatIf) { 495 | $ExportFilePath = "$PSScriptRoot\Applocker Policy - Created $DateTime - WhatIf.json" 496 | } else { 497 | $ExportFilePath = "$PSScriptRoot\Applocker Policy - Created $DateTime.json" 498 | } 499 | Write-Host "Export Applocker Intune policy JSON to file: $ExportFilePath" 500 | $BodyJson | Out-File -FilePath $ExportFilePath -Encoding UTF8 501 | $Success = $? 502 | if($Success) { 503 | Write-Host "OK`n" -ForegroundColor Green 504 | } else { 505 | Write-Host "Failed`n" -ForegroundColor Red 506 | } 507 | 508 | if($WhatIf) { 509 | Write-Host "What if: Would upload Applocker configuration to Intune with POST body json:`n`n$BodyJson`n" 510 | 511 | } else { 512 | Write-Host "Create Applocker Configuration profile to Intune" 513 | 514 | $Url = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations" 515 | $MSGraphRequest = Invoke-MSGraphRequest -Url $url -Content $BodyJson.ToString() -HttpMethod 'POST' 516 | $Success = $? 517 | if($Success -and $MSGraphRequest) { 518 | Write-Host "OK`n" -ForegroundColor Green 519 | Write-Host "Graph API response:`n" 520 | $MSGraphRequest 521 | Write-Host "" 522 | 523 | $ResponseFilePath = "$PSScriptRoot\Applocker Policy - Created $DateTime - GraphAPI response.json" 524 | Write-Host "Save Graph API response to file just in case that is needed later:" 525 | Write-Host "$ResponseFilePath" 526 | 527 | $MSGraphRequest | ConvertTo-Json -Depth 5 | Out-File -FilePath $ResponseFilePath -Encoding UTF8 528 | $Success = $? 529 | if($Success) { 530 | Write-Host "OK`n" -ForegroundColor Green 531 | Write-Host "Check Applocker Configuration profile in Intune:`n$DisplayName`n" 532 | } else { 533 | Write-Host "Failed`n" -ForegroundColor Red 534 | } 535 | 536 | } else { 537 | Write-Host "There was possible problems uploading Applocker policy to Intune!" -ForegroundColor Yellow 538 | $MSGraphRequest 539 | } 540 | } 541 | 542 | Write-Host "All done." 543 | 544 | 545 | # endregion main -------------------------------------------------------------------------------- /Applocker/README.md: -------------------------------------------------------------------------------- 1 | # Create Intune Applocker configuration profile from Applocker XML file # 2 | 3 | Create Intune Applocker Configuration Profile with this script. 4 | 5 | ## Quick links to files: 6 | * [Create_Applocker_Intune_policy.ps1](https://github.com/petripaavola/Intune/blob/master/Applocker/Create_Applocker_Intune_policy.ps1) 7 | 8 | ## Description 9 | 10 | This script reads Applocker exported XML rules file and creates Intune Applocker custom oma-uri configuration profile. 11 | 12 | Script makes XML syntax check first. Only **Enabled** or **AuditOnly** mode rules are created to Intune. NotConfigured rules are ignored. 13 | 14 | **Double check Intune rules configurations before applying to devices. Always test first on test devices before applying policy to production devices.** 15 | 16 | Script saves 2 JSON files locally to script folder: 17 | * **Intune Applocker configuration profile** 18 | * For example: *Applocker Policy - Created 2023-04-24 103049.json* 19 | * **Graph API response are saved in current directory** 20 | * For example. *Applocker Policy - Created 2023-04-24 103712 - GraphAPI response.json* 21 | 22 | With option **-WhatIf** script does XML syntax check and creates Intune configuration JSON file locally. 23 | Filename example is: *Applocker Policy - Created 2023-04-24 103044 - WhatIf.json* 24 | 25 | ### Intune Applocker configuration policy ### 26 | Script creates Intune Applocker policies with naming syntax: 27 | **Applocker Policy - Created 2023-04-24 103712 by admin.username@organization.com** 28 | 29 | **Test and double check Applocker configuration profile XML syntax before production deployment.** 30 | 31 | ### Parameters ### 32 | 33 | **-FilePath** 34 | Path to Applocker XML file 35 | ``` 36 | .\Create_Applocker_Intune_policy.ps1 -Filepath .\AppLocker.xml 37 | ``` 38 | 39 | **-WhatIf** 40 | Does XML file syntax check and creates rules json file. Filename is like: Applocker Policy - Created 2023-04-24 102623 - WhatIf.json 41 | ``` 42 | .\Create_Applocker_Intune_policy.ps1 -Filepath .\AppLocker.xml -WhatIf 43 | ``` 44 | 45 | ### Prerequisities ### 46 | 47 | Script uses Intune Powershell management module **Microsoft.Graph.Intune**. 48 | 49 | You can install Intune Powershell management module to your user account with command 50 | ``` 51 | Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser 52 | ``` 53 | -------------------------------------------------------------------------------- /Apps/Audacity_3.5.1_Intune_Win32_Custom_DetectionCheck_Script.ps1: -------------------------------------------------------------------------------- 1 | # Intune Win32 App Audacity Custom Detection check script for version 3.5.1 2 | # 3 | # This returns compliant with equal to 3.5.1 version 4 | # 5 | # 6 | # Petri.Paavola@yodamiitti.fi 7 | # Windows MVP - Windows and Intune 8 | # 2024-11-01 9 | # 10 | # Original script source: 11 | # https://github.com/petripaavola/Intune/blob/master/Apps/Audacity_3.5.1_Intune_Win32_Custom_DetectionCheck_Script.ps1 12 | 13 | 14 | 15 | # Check if file exists 16 | if(-not (Test-Path "C:\Program Files\Audacity\audacity.exe")) { 17 | # File does not exist 18 | exit 1 19 | } 20 | 21 | 22 | $FileVersionString = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("C:\Program Files\Audacity\audacity.exe").FileVersion 23 | 24 | # Convert , -> . 25 | $FileVersionString = $FileVersionString.Replace(',','.') 26 | 27 | #The below line trims the spaces before and after the version name 28 | $FileVersionString = $FileVersionString.Trim(); 29 | 30 | # Cast variable to [version] 31 | $FileVersion = [version]$FileVersionString 32 | 33 | 34 | # We could also get version with this command in most other cases 35 | #$file = Get-ChildItem "C:\Program Files\Audacity\audacity.exe" 36 | #[version]$FileVersion = $file.versioninfo.fileversion 37 | 38 | 39 | if ($FileVersion -eq "3.5.1.0" ) { 40 | # App detected 41 | 42 | # Write the version to STDOUT by default 43 | # Write anything to StdOut and exit 0 to make application show as detected 44 | Write-Host "$FileVersion" 45 | exit 0 46 | } 47 | else { 48 | # App NOT detected 49 | 50 | #Exit with non-zero failure code 51 | exit 1 52 | } 53 | -------------------------------------------------------------------------------- /Apps/Create_Multiple_IntuneMobileAppAssignments_Out-GridView.ps1: -------------------------------------------------------------------------------- 1 | # Create Multiple Intune Application assignments for selected Apps 2 | # Uses Out-GridView to show GUI to select App(s), AzureADGroup and Intent 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Devices for IT 6 | # 7 | # 27.4.2021 8 | 9 | 10 | function Create-IntuneApplicationAssignment { 11 | Param( 12 | $AzureADGroupId, 13 | $mobileAppId, 14 | $AssignmentIntent 15 | ) 16 | 17 | # Choices are: 'available', 'required', 'uninstall', 'availableWithoutEnrollment' 18 | 19 | $TargetObject = $null 20 | $TargetObject = New-Object PSObject 21 | $TargetObject | Add-Member NoteProperty '@odata.type' '#microsoft.graph.groupAssignmentTarget' 22 | $TargetObject | Add-Member NoteProperty 'groupId' $AzureADGroupId 23 | 24 | # Fix json - custom-object problem 25 | # This is not needed anymore 20190220 26 | #$TargetObject = $TargetObject |ConvertTo-json |ConvertFrom-json 27 | 28 | # Create Intune Application assignment 29 | New-DeviceAppManagement_MobileApps_Assignments -mobileAppId $mobileAppId -intent $AssignmentIntent -target $TargetObject 30 | 31 | $Success = $? 32 | return $Success 33 | } 34 | 35 | ##### 36 | 37 | # Update Graph API schema to beta to get Win32LobApps and possible other new features also 38 | Update-MSGraphEnvironment -SchemaVersion 'beta' 39 | 40 | $MSGraphEnvironment = Connect-MSGraph 41 | $Success = $? 42 | 43 | if (-not $Success) { 44 | Write-Error "Error connecting to Intune!" 45 | Exit 1 46 | } 47 | 48 | # Get AzureAD groups and show them in Out-GridView. User can select only 1 group 49 | 50 | # Notice we probably get only part of groups because GraphAPI returns limited number of groups 51 | $groups = Get-AADGroup -Filter 'securityEnabled eq true' 52 | 53 | # Quick workaround to get all Security Groups and also get actual objects and not .Value -attribute 54 | $AllGroups = Get-MSGraphAllPages -SearchResult $groups 55 | 56 | if(-not $AllGroups) { 57 | Write-Error "Could not find any AzureAD Groups!" 58 | Exit 1 59 | } 60 | 61 | # Show AzureAD groups in Out-GridView with selected properties and save selected AzureADGroup to variable 62 | Write-Output "Select AzureAD Group from Out-GridView" 63 | $SelectedAzureADGroup = $AllGroups | Select displayName, description, createdDateTime, id, groupTypes | Sort displayName | Out-GridView -Title "Select AzureAD Group to make assignment" -OutputMode Single 64 | 65 | # Exit if nothing was selected in Out-GridView 66 | if (-not $SelectedAzureADGroup) { Write-Output "No groups selected, exiting..."; Exit 0 } 67 | 68 | ##### 69 | 70 | # Get App information and show Apps in Out-GridView 71 | 72 | # We need assignments info to check existing assignments so -Expand assignment option is needed here 73 | $Apps = Get-DeviceAppManagement_MobileApps -Expand assignments 74 | 75 | # Quick workaround to get all Apps and also get actual objects and not .Value -attribute 76 | $AllApps = Get-MSGraphAllPages -SearchResult $Apps 77 | 78 | if(-not $AllApps) { 79 | Write-Error "Could not find any Intune Apps!" 80 | Exit 1 81 | } 82 | 83 | # Show Apps in Out-GridView, show only specified app properties 84 | Write-Output "Select Apps in Out-GridView to App assignments creation for group: $($SelectedApp.assignmentTargetGroupDisplayName)" 85 | $SelectedApps = $AllApps | Select '@odata.type', vppTokenAppleId, displayName, productVersion, publisher, fileName, size, commandLine, productCode, publishingState, createdDateTime, lastModifiedDateTime, id| Sort displayName | Out-GridView -PassThru -Title 'Select Application(s) to make assignment' 86 | 87 | # Exit if nothing was selected in Out-GridView 88 | if (-not $SelectedApps) { Write-Output "No Apps selected, exiting..."; Exit 0 } 89 | 90 | ##### 91 | 92 | # Get Intent with Out-GridView 93 | Write-Output "Select Intent for App Assignment" 94 | $SelectedIntent = @('required', 'available', 'uninstall', 'availableWithoutEnrollment') | Out-GridView -Title 'Select Intent for application assignment' -OutputMode Single 95 | 96 | # Exit if nothing was selected in Out-GridView 97 | if (-not $SelectedIntent) { Write-Output "No App assignment intent selected, exiting..."; Exit 0 } 98 | 99 | 100 | # Make assignments for all selected Apps 101 | foreach ($SelectedApp in $SelectedApps) { 102 | 103 | # Filter out those Apps that already have assignment for selected AzureADGroup 104 | $ExistingAssignment = $AllApps | Where-Object { ($_.id -eq $SelectedApp.id) -and ($_.Assignments.target.groupid -eq $SelectedAzureADGroup.id) } 105 | 106 | # If there is already is existing Assignment then show warning and continue to next App in foreach loop 107 | if ($ExistingAssignment) { 108 | # There is already assignment for this AzureADGroup 109 | # Show warning and skip this App assignment 110 | 111 | $intent = $ExistingAssignment | Where-Object { $_.Assignments.target.groupid -eq $SelectedAzureADGroup.id } | Select -ExpandProperty Assignments | Select -ExpandProperty Intent 112 | `Write-Host "Warning: Existing $intent Assignment for App: `"$($SelectedApp.displayName)`"" found for AzureADGroup: `"$($SelectedAzureADGroup.displayName)`"". Skipping this App assignment..." -ForegroundColor Yellow 113 | 114 | # Skip this App in foreach loop and process to next App 115 | Continue 116 | } 117 | 118 | # Create Application assignment - most important line in this script 119 | $Success = Create-IntuneApplicationAssignment $SelectedAzureADGroup.Id $SelectedApp.Id $SelectedIntent 120 | if ($Success) { 121 | Write-Host "Success: Created $SelectedIntent Application assignment for Application: `"$($SelectedApp.DisplayName)`" for AzureADGroup: `"$($SelectedAzureADGroup.DisplayName)`"" -Foreground Green 122 | } 123 | else { 124 | Write-Host "Failed: Error creating $SelectedIntent Application assignment for Application: `"$($SelectedApp.DisplayName)`" for AzureADGroup: `"$($SelectedAzureADGroup.DisplayName)`"" -Foreground Red 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Apps/Delete_Multiple_IntuneMobileAppAssignments_Out-GridView.ps1: -------------------------------------------------------------------------------- 1 | # Delete Multiple Intune Application assignments for selected Apps 2 | # Uses Out-GridView to show GUI to select AzureADGroup and App(s), 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Devices for IT 6 | # 7 | # 27.4.2021 8 | 9 | 10 | ###### 11 | 12 | # Update Graph API schema to beta to get Win32LobApps and possible other new features also 13 | Update-MSGraphEnvironment -SchemaVersion 'beta' 14 | 15 | $MSGraphEnvironment = Connect-MSGraph 16 | $Success = $? 17 | 18 | if (-not $Success) { 19 | Write-Error "Error connecting to Intune!" 20 | Exit 1 21 | } 22 | 23 | # Get AzureAD groups and show them in Out-GridView. User can select only 1 group 24 | 25 | # Notice we probably get only part of groups because GraphAPI returns limited number of groups 26 | $groups = Get-AADGroup -Filter 'securityEnabled eq true' 27 | 28 | # Quick workaround to get all Security Groups and also get actual objects and not .Value -attribute 29 | $AllGroups = Get-MSGraphAllPages -SearchResult $groups 30 | 31 | if(-not $AllGroups) { 32 | Write-Error "Could not find any AzureAD Groups!" 33 | Exit 1 34 | } 35 | 36 | # Show AzureAD groups in Out-GridView with selected properties and save selected AzureADGroup to variable 37 | Write-Output "Select AzureAD Group from Out-GridView" 38 | $SelectedAzureADGroup = $AllGroups | Select displayName, description, createdDateTime, id, groupTypes | Sort displayName | Out-GridView -Title "Select AzureAD Group to delete from App assignment" -OutputMode Single 39 | 40 | # Exit if nothing was selected in Out-GridView 41 | if (-not $SelectedAzureADGroup) { Write-Output "No groups selected, exiting..."; Exit 0 } 42 | 43 | ###### 44 | 45 | # Get App information and show Apps in Out-GridView 46 | 47 | # We need assignments info so -Expand assignment option is needed here 48 | $Apps = Get-DeviceAppManagement_MobileApps -Expand assignments 49 | 50 | # Quick workaround to get all Apps and also get actual objects and not .Value -attribute 51 | $AllApps = Get-MSGraphAllPages -SearchResult $Apps 52 | 53 | if(-not $AllApps) { 54 | Write-Error "Could not find any Intune Apps!" 55 | Exit 1 56 | } 57 | 58 | # Find apps which have assignments to our selected AzureAD Group id 59 | # Check data syntax from GraphAPI with request: https://graph.microsoft.com/v1.0/deviceAppManagement/mobileApps?$expand=assignments 60 | # or convert $Apps to json to get more human readable format: $Apps | ConvertTo-JSON 61 | $AppsWithAssignmentToSelectedGroup = $AllApps | Where-Object { $_.assignments.target.groupid -eq $SelectedAzureADGroup.id } 62 | 63 | # Exit if there were no assignments to selected AzureAD Group 64 | if (-not $AppsWithAssignmentToSelectedGroup) { 65 | Write-Output "No Application assignments found for AzureAD Group: $($SelectedAzureADGroup.displayName)" 66 | Exit 0 67 | } 68 | 69 | # Create custom object array and gather necessary app and assignment information. We need appId and assignmentId to remove selected assignments. All other attributes are for humans 70 | $AppsWithAssignmentInformation = @() 71 | 72 | # Go through each app and save necessary information to custom object 73 | Foreach ($App in $AppsWithAssignmentToSelectedGroup) { 74 | $Assignment = $App.Assignments | Where-Object { $_.target.groupid -eq $SelectedAzureADGroup.id } 75 | 76 | if($App.vppTokenAppleId) { $vppTokenAppleId = $App.vppTokenAppleId} else { $vppTokenAppleId = $null } 77 | 78 | $properties = @{ 79 | '@odata.type' = $App.'@odata.type' 80 | vppTokenAppleId = $vppTokenAppleId 81 | displayname = $App.displayname 82 | productVersion = $App.productVersion 83 | publisher = $App.publisher 84 | filename = $App.filename 85 | createdDateTime = $App.createdDateTime 86 | lastModifiedDateTime = $App.lastModifiedDateTime 87 | id = $App.id 88 | assignmentId = $Assignment.id 89 | assignmentIntent = $Assignment.intent 90 | assignmentTargetGroupId = $Assignment.target.groupid 91 | assignmentTargetGroupDisplayName = $SelectedAzureADGroup.displayName 92 | } 93 | 94 | # Create new custom object every time inside foreach-loop 95 | # This is really important step to do inside foreach-loop! 96 | # If you create custom object outside of foreach then you would edit same custom object on every foreach cycle resulting only 1 app in custom object array 97 | $CustomObject = New-Object -TypeName PSObject -Prop $properties 98 | 99 | # Add custom object to our custom object array. 100 | $AppsWithAssignmentInformation += $CustomObject 101 | } 102 | 103 | 104 | # Show Apps in Out-GridView, user can select multiple applications where assignments are deleted 105 | Write-Output "Select Apps in Out-GridView to delete App assignments for group: $($SelectedApp.assignmentTargetGroupDisplayName)" 106 | $SelectedApps = $AppsWithAssignmentInformation | Select '@odata.type', vppTokenAppleId, displayName, assignmentIntent, assignmentTargetGroupDisplayName, publisher, productVersion, filename, createdDateTime, lastModifiedDateTime, id, assignmentId, assignmentTargetGroupId | Sort displayName | Out-GridView -PassThru -Title "Select Application(s) to delete assignment from group: $($SelectedAzureADGroup.displayName)" 107 | 108 | # Exit if nothing was selected in Out-GridView 109 | if (-not $SelectedApps) { Write-Output "No Apps selected, exiting..."; Exit 0 } 110 | 111 | # Delete App assignment for selected Apps 112 | foreach ($SelectedApp in $SelectedApps) { 113 | 114 | # Remove Application Assignment - most important line in this script 115 | Remove-DeviceAppManagement_MobileApps_Assignments -mobileAppAssignmentId $SelectedApp.assignmentId -mobileAppId $SelectedApp.id 116 | $Success = $? 117 | 118 | if ($Success) { 119 | Write-Host "SUCCESS: Deleting $($SelectedApp.assignmentIntent) assignment from application $($SelectedApp.displayname) for group: $($SelectedApp.assignmentTargetGroupDisplayName)" -Foreground Green 120 | } 121 | else { 122 | Write-Host "FAILED: Deleting $($SelectedApp.assignmentIntent) assignment from application $($SelectedApp.displayname) for group: $($SelectedApp.assignmentTargetGroupDisplayName)" -Foreground Red 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /Apps/README.md: -------------------------------------------------------------------------------- 1 | # Scripts, tips and tricks to manage Intune Apps 2 | -------------------------------------------------------------------------------- /Apps/Show_IntuneMobileAppAssignments_to_selected_AzureADGroup_in_Out-GridView.ps1: -------------------------------------------------------------------------------- 1 | # Show Intune Application assignments for selected AzureADGroup 2 | # Uses Out-GridView to show GUI to select AzureADGroup and App(s) 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Devices for IT 6 | # 7 | # 27.4.2021 8 | 9 | 10 | ###### 11 | 12 | # Update Graph API schema to beta to get Win32LobApps and possible other new features also 13 | Update-MSGraphEnvironment -SchemaVersion 'beta' 14 | 15 | $MSGraphEnvironment = Connect-MSGraph 16 | $Success = $? 17 | 18 | if (-not $Success) { 19 | Write-Error "Error connecting to Intune!" 20 | Exit 1 21 | } 22 | 23 | # Get AzureAD groups and show them in Out-GridView. User can select only 1 group 24 | 25 | # Notice we probably get only part of groups because GraphAPI returns limited number of groups 26 | $groups = Get-AADGroup -Filter 'securityEnabled eq true' 27 | 28 | # Quick workaround to get all Security Groups and also get actual objects and not .Value -attribute 29 | $AllGroups = Get-MSGraphAllPages -SearchResult $groups 30 | 31 | if(-not $AllGroups) { 32 | Write-Error "Could not find any AzureAD Groups!" 33 | Exit 1 34 | } 35 | 36 | # Show AzureAD groups in Out-GridView with selected properties and save selected AzureADGroup to variable 37 | Write-Output "Select AzureAD Group from Out-GridView" 38 | $SelectedAzureADGroup = $AllGroups | Select displayName, description, createdDateTime, id, groupTypes | Sort displayName | Out-GridView -Title "Select AzureAD Group to show it's App Assignments" -OutputMode Single 39 | 40 | # Exit if nothing was selected in Out-GridView 41 | if (-not $SelectedAzureADGroup) { Write-Output "No groups selected, exiting..."; Exit 0 } 42 | 43 | ###### 44 | 45 | # Get App information and show Apps in Out-GridView 46 | 47 | # We need assignments info so -Expand assignment option is needed here 48 | $Apps = Get-DeviceAppManagement_MobileApps -Expand assignments 49 | 50 | # Quick workaround to get all Apps and also get actual objects and not .Value -attribute 51 | $AllApps = Get-MSGraphAllPages -SearchResult $Apps 52 | 53 | if(-not $AllApps) { 54 | Write-Error "Could not find any Intune Apps!" 55 | Exit 1 56 | } 57 | 58 | # Find apps which have assignments to our selected AzureAD Group id 59 | # Check data syntax from GraphAPI with request: https://graph.microsoft.com/v1.0/deviceAppManagement/mobileApps?$expand=assignments 60 | # or convert $Apps to json to get more human readable format: $Apps | ConvertTo-JSON 61 | $AppsWithAssignmentToSelectedGroup = $AllApps | Where-Object { $_.assignments.target.groupid -eq $SelectedAzureADGroup.id } 62 | 63 | # Exit if there were no assignments to selected AzureAD Group 64 | if (-not $AppsWithAssignmentToSelectedGroup) { 65 | Write-Output "No Application assignments found for AzureAD Group: $($SelectedAzureADGroup.displayName)" 66 | Exit 0 67 | } 68 | 69 | # Create custom object array and gather necessary app and assignment information. We need appId and assignmentId to remove selected assignments. All other attributes are for humans 70 | $AppsWithAssignmentInformation = @() 71 | 72 | # Go through each app and save necessary information to custom object 73 | Foreach ($App in $AppsWithAssignmentToSelectedGroup) { 74 | $Assignment = $App.Assignments | Where-Object { $_.target.groupid -eq $SelectedAzureADGroup.id } 75 | 76 | if($App.vppTokenAppleId) { $vppTokenAppleId = $App.vppTokenAppleId} else { $vppTokenAppleId = $null } 77 | 78 | $properties = @{ 79 | '@odata.type' = $App.'@odata.type' 80 | vppTokenAppleId = $vppTokenAppleId 81 | displayname = $App.displayname 82 | productVersion = $App.productVersion 83 | publisher = $App.publisher 84 | filename = $App.filename 85 | createdDateTime = $App.createdDateTime 86 | lastModifiedDateTime = $App.lastModifiedDateTime 87 | id = $App.id 88 | assignmentId = $Assignment.id 89 | assignmentIntent = $Assignment.intent 90 | assignmentTargetGroupId = $Assignment.target.groupid 91 | assignmentTargetGroupDisplayName = $SelectedAzureADGroup.displayName 92 | } 93 | 94 | # Create new custom object every time inside foreach-loop 95 | # This is really important step to do inside foreach-loop! 96 | # If you create custom object outside of foreach then you would edit same custom object on every foreach cycle resulting only 1 app in custom object array 97 | $CustomObject = New-Object -TypeName PSObject -Prop $properties 98 | 99 | # Add custom object to our custom object array. 100 | $AppsWithAssignmentInformation += $CustomObject 101 | } 102 | 103 | 104 | # Show Apps in Out-GridView 105 | $SelectedApps = $AppsWithAssignmentInformation | Select '@odata.type', vppTokenAppleId, displayName, assignmentIntent, assignmentTargetGroupDisplayName, publisher, productVersion, filename, createdDateTime, lastModifiedDateTime, id, assignmentId, assignmentTargetGroupId | Sort displayName | Out-GridView -Title "Found these App Assignment for AzureADGroup: $($SelectedAzureADGroup.displayName)" 106 | -------------------------------------------------------------------------------- /Apps/install_Audacity_3.5.1-EXE-example.ps1: -------------------------------------------------------------------------------- 1 | # Install Audacity + MP3 Lame encoder 2 | # IntuneWin32 EXE Powershell script install example 3 | # 4 | # 5 | # Petri.Paavola@yodamiitti.fi 6 | # Windows MVP - Windows and Intune 7 | # 2024-11-01 8 | # 9 | # Original script source: 10 | # https://github.com/petripaavola/Intune/blob/master/Apps/install_Audacity_3.5.1-EXE-example.ps1 11 | # 12 | # Intune install command: 13 | # Powershell.exe -ExecutionPolicy Bypass -File install_Audacity_3.5.1-EXE-example.ps1 14 | 15 | 16 | # 17 | # 18 | # # # 19 | # # 20 | # 21 | 22 | # Configure these values 23 | $SoftwareName = 'Audacity' 24 | $version = "3.5.1" 25 | 26 | $InstallerFilePath = "$PSScriptRoot\audacity-win-3.5.1-64bit.exe" 27 | $Arguments = @( 28 | '/verysilent' 29 | '/norestart' 30 | ) 31 | 32 | # 33 | # # 34 | # # # 35 | # 36 | # 37 | 38 | 39 | 40 | # Start script in 64bit environment if script was started in 32bit environment 41 | # Intune Win32 application install process starts in 32bit process by default (2024-11-01) 42 | # Few commands will require native bit command to work. Examples pnputil.exe and dism.exe 43 | # Below 64bit workaround is provided by Oliver Kieselbach 44 | # Original example: 45 | # https://github.com/okieselbach/Intune/blob/master/ManagementExtension-Samples/IntunePSTemplate.ps1 46 | if (-not [System.Environment]::Is64BitProcess) 47 | { 48 | # start new PowerShell as x64 bit process, wait for it and gather exit code and standard error output 49 | $sysNativePowerShell = "$($PSHOME.ToLower().Replace("syswow64", "sysnative"))\powershell.exe" 50 | 51 | $pinfo = New-Object System.Diagnostics.ProcessStartInfo 52 | $pinfo.FileName = $sysNativePowerShell 53 | $pinfo.Arguments = "-ex bypass -file `"$PSCommandPath`"" 54 | $pinfo.RedirectStandardError = $true 55 | $pinfo.RedirectStandardOutput = $true 56 | $pinfo.CreateNoWindow = $true 57 | $pinfo.UseShellExecute = $false 58 | $p = New-Object System.Diagnostics.Process 59 | $p.StartInfo = $pinfo 60 | $p.Start() | Out-Null 61 | $p.WaitForExit() # Wait for the 64-bit process to complete 62 | 63 | $exitCode = $p.ExitCode 64 | 65 | $stderr = $p.StandardError.ReadToEnd() 66 | 67 | if ($stderr) { Write-Error -Message $stderr } 68 | 69 | exit $exitCode 70 | 71 | } else { 72 | 73 | ############################################################# 74 | # Main script starts here 75 | 76 | # Start logging 77 | Start-Transcript "C:\Windows\Logs\Install_$($SoftwareName)_$($version)_IntuneWin.log" -Append 78 | 79 | # Running inside Try so we can catch fatal errors 80 | try { 81 | 82 | Write-Host "Install $SoftwareName $version" 83 | 84 | 85 | # Check that $InstallerFilePath exists 86 | if (-not (Test-Path $InstallerFilePath)) { 87 | Write-Host "Installer file not found: $InstallerFilePath" 88 | Write-Host "Exiting installation with error code 9999" 89 | Stop-Transcript 90 | Exit 9999 91 | } 92 | 93 | 94 | # Log file path for stdOut and stdError 95 | $RedirectStandardOutputFilePath = "C:\Windows\Logs\Install_$($SoftwareName)_$($version)_IntuneWin_RedirectStandardOutput.log" 96 | $RedirectStandardErrorFilePath = "C:\Windows\Logs\Install_$($SoftwareName)_$($version)_IntuneWin_RedirectStandardError.log" 97 | 98 | 99 | # We use Start-Process which gets most probability the ExitCode 100 | # Some really really rare cases $LastExitCode does not work, so this works better 101 | $Process = Start-Process $InstallerFilePath -ArgumentList $Arguments -Wait -Passthru -NoNewWindow -RedirectStandardOutput $RedirectStandardOutputFilePath -RedirectStandardError $RedirectStandardErrorFilePath 102 | $Process.WaitForExit() 103 | $ExitCode = $Process.ExitCode 104 | 105 | if($ExitCode -eq 0) { 106 | # Installation was successful 107 | 108 | Write-Host "$SoftwareName $version installation succeeded with ExitCode $ExitCode" 109 | } else { 110 | # Installation failed 111 | 112 | Write-Host "Error installing $SoftwareName $version. ExitCode: $ExitCode" 113 | Write-Host "Exiting installation with ExitCode $ExitCode" 114 | Stop-Transcript 115 | Exit $ExitCode 116 | } 117 | 118 | # Remove Desktop shortcut 119 | # This is best try so app installation will not fail if this for any reason did not succeed 120 | $DesktopShortcutPath = "C:\Users\Public\Desktop\Audacity.lnk" 121 | if(Test-Path $DesktopShortcutPath) { 122 | Write-Host "Delete desktop shortcut: $DesktopShortcutPath" 123 | Remove-Item $DesktopShortcutPath -Force -ErrorAction SilentlyContinue 124 | Write-Host "Success: $?" 125 | } 126 | 127 | 128 | Write-Host "Copy lame_enc.dll to Audacity installation folder" 129 | Copy-Item "$PSScriptRoot\lame_enc.dll" -Destination "C:\Program Files\Audacity" -Force 130 | $Success = $? 131 | 132 | if($Success) { 133 | Write-Host "Success copying file." 134 | 135 | Write-Host "$SoftwareName $version installation succeeded" 136 | Write-Host "Exiting installation with ExitCode 0" 137 | Stop-Transcript 138 | Exit 0 139 | } else { 140 | Write-Host "Error copying lame_enc.dll file" 141 | Write-Host "Exiting installation with ExitCode 2" 142 | Stop-Transcript 143 | Exit 2 144 | } 145 | 146 | } catch { 147 | # Handle any unexpected errors 148 | 149 | Write-Host "An unexpected fatal error occurred: $_" 150 | Write-Host "Exiting installation with ExitCode 99999" 151 | Stop-Transcript 152 | Exit 99999 153 | 154 | } 155 | } -------------------------------------------------------------------------------- /Apps/install_LibreOffice_7.0.6-MSI-example.ps1: -------------------------------------------------------------------------------- 1 | # Install LibreOffice 7.0.6 MSI 2 | # IntuneWin32 MSI Powershell script install example 3 | # 4 | # UI Languages: FI, EN, SE 5 | # Proofing languages: English, German, French, Swedish and Spanish 6 | # 7 | # 8 | # Petri.Paavola@yodamiitti.fi 9 | # Microsoft MVP - Windows and Intune 10 | # 2024-11-01 11 | # 12 | # Original script source: 13 | # https://github.com/petripaavola/Intune/blob/master/Apps/install_LibreOffice_7.0.6-MSI-example.ps1 14 | # 15 | # Intune install command: 16 | # Powershell.exe -ExecutionPolicy Bypass -File install_LibreOffice_7.0.6-MSI-example.ps1 17 | # 18 | # Intune uninstall command: 19 | # msiexec /x {9F9A9C01-5A65-4C2E-A243-FC88C81BC35F} /qn /l*v C:\Windows\Logs\Uninstall_LibreOffice_7.0.6_MSI.log 20 | # 21 | # MSI ProductCode={9F9A9C01-5A65-4C2E-A243-FC88C81BC35F} 22 | # 23 | # This is MSI application install example script for MSI-files 24 | # In this case there is so long parameter that it will not fit in Intune install command line 25 | 26 | 27 | # 28 | # 29 | # # # 30 | # # 31 | # 32 | 33 | # Configure these values 34 | $SoftwareName = "LibreOffice" 35 | $version = "7.0.6" 36 | $MSIFilePath = "$PSScriptRoot\LibreOffice_7.0.6_Win_x64.msi" 37 | 38 | # 39 | # # 40 | # # # 41 | # 42 | # 43 | 44 | 45 | 46 | # Start script in 64bit environment if script was started in 32bit environment 47 | # Intune Win32 application install process starts in 32bit process by default (2024-11-01) 48 | # Few commands will require native bit command to work. Examples pnputil.exe and dism.exe 49 | # Below 64bit workaround is provided by Oliver Kieselbach 50 | # Original example: 51 | # https://github.com/okieselbach/Intune/blob/master/ManagementExtension-Samples/IntunePSTemplate.ps1 52 | if (-not [System.Environment]::Is64BitProcess) 53 | { 54 | # start new PowerShell as x64 bit process, wait for it and gather exit code and standard error output 55 | $sysNativePowerShell = "$($PSHOME.ToLower().Replace("syswow64", "sysnative"))\powershell.exe" 56 | 57 | $pinfo = New-Object System.Diagnostics.ProcessStartInfo 58 | $pinfo.FileName = $sysNativePowerShell 59 | $pinfo.Arguments = "-ex bypass -file `"$PSCommandPath`"" 60 | $pinfo.RedirectStandardError = $true 61 | $pinfo.RedirectStandardOutput = $true 62 | $pinfo.CreateNoWindow = $true 63 | $pinfo.UseShellExecute = $false 64 | $p = New-Object System.Diagnostics.Process 65 | $p.StartInfo = $pinfo 66 | $p.Start() | Out-Null 67 | $p.WaitForExit() # Wait for the 64-bit process to complete 68 | 69 | $exitCode = $p.ExitCode 70 | 71 | $stderr = $p.StandardError.ReadToEnd() 72 | 73 | if ($stderr) { Write-Error -Message $stderr } 74 | 75 | exit $exitCode 76 | 77 | } else { 78 | 79 | ############################################################# 80 | # Main script starts here 81 | 82 | # Start logging 83 | Start-Transcript "C:\Windows\Logs\Install_$($SoftwareName)_$($version)_IntuneWin32App.log" -Append 84 | 85 | 86 | # Running inside Try so we can catch fatal errors 87 | try { 88 | Write-Host "Install $SoftwareName $version" 89 | 90 | 91 | # Check that $MSIFilePath exists 92 | if (-not (Test-Path $MSIFilePath)) { 93 | Write-Host "Installer file not found: $MSIFilePath" 94 | Write-Host "Exiting installation with error code 9999" 95 | Stop-Transcript 96 | Exit 9999 97 | } 98 | 99 | 100 | # Log file path for stdOut and stdError 101 | $RedirectStandardOutputFilePath = "C:\Windows\Logs\Install_$($SoftwareName)_$($version)_IntuneWin_RedirectStandardOutput.log" 102 | $RedirectStandardErrorFilePath = "C:\Windows\Logs\Install_$($SoftwareName)_$($version)_IntuneWin_RedirectStandardError.log" 103 | 104 | $Installer = "msiexec.exe" 105 | $Arguments = @( 106 | '/i' 107 | "`"$MSIFilePath`"" 108 | '/qn' 109 | "/l*v C:\Windows\Logs\Install_$($SoftwareName)_$($version)_MSI.log" 110 | '/norestart' 111 | 'RebootYesNo=No' 112 | 'ALLUSERS=1' 113 | 'CREATEDESKTOPLINK=0' 114 | 'REGISTER_ALL_MSO_TYPES=0' 115 | 'REGISTER_NO_MSO_TYPES=1' 116 | 'ISCHECKFORPRODUCTUPDATES=0' 117 | 'QUICKSTART=0' 118 | 'ADDLOCAL=ALL' 119 | 'UI_LANGS=en_US,sv,fi' 120 | 'REMOVE=gm_r_ex_Dictionary_Af,gm_r_ex_Dictionary_Sq,gm_r_ex_Dictionary_Tr,gm_r_ex_Dictionary_An,gm_r_ex_Dictionary_Ar,gm_r_ex_Dictionary_Be,gm_r_ex_Dictionary_Bg,gm_r_ex_Dictionary_Bn,gm_r_ex_Dictionary_Br,gm_r_ex_Dictionary_Pt_Pt,gm_r_ex_Dictionary_Pt_Br,gm_r_ex_Dictionary_Bs,gm_r_ex_Dictionary_Ca,gm_r_ex_Dictionary_Bo,gm_r_ex_Dictionary_Cs,gm_r_ex_Dictionary_Da,gm_r_ex_Dictionary_Nl,gm_r_ex_Dictionary_Et,gm_r_ex_Dictionary_Gd,gm_r_ex_Dictionary_Gl,gm_r_ex_Dictionary_Gu,gm_r_ex_Dictionary_He,gm_r_ex_Dictionary_Hi,gm_r_ex_Dictionary_Hu,gm_r_ex_Dictionary_Hr,gm_r_ex_Dictionary_Id,gm_r_ex_Dictionary_It,gm_r_ex_Dictionary_Is,gm_r_ex_Dictionary_Lt,gm_r_ex_Dictionary_Lo,gm_r_ex_Dictionary_Lv,gm_r_ex_Dictionary_Ne,gm_r_ex_Dictionary_No,gm_r_ex_Dictionary_Oc,gm_r_ex_Dictionary_Pl,gm_r_ex_Dictionary_Ro,gm_r_ex_Dictionary_Ru,gm_r_ex_Dictionary_Sr,gm_r_ex_Dictionary_Si,gm_r_ex_Dictionary_Sk,gm_r_ex_Dictionary_Sl,gm_r_ex_Dictionary_El,gm_r_ex_Dictionary_Te,gm_r_ex_Dictionary_Th,gm_r_ex_Dictionary_Uk,gm_r_ex_Dictionary_Vi,gm_r_ex_Dictionary_Zu' 121 | ) 122 | 123 | 124 | # We use Start-Process which gets most probability the ExitCode 125 | # Some really really rare cases $LastExitCode does not work, so this works better 126 | $Process = Start-Process $Installer -ArgumentList $Arguments -Wait -Passthru -NoNewWindow -RedirectStandardOutput $RedirectStandardOutputFilePath -RedirectStandardError $RedirectStandardErrorFilePath 127 | $Process.WaitForExit() 128 | $ExitCode = $Process.ExitCode 129 | 130 | if ($ExitCode -eq 0) { 131 | # MSI installation was successful 132 | 133 | Write-Host "$SoftwareName $version installation succeeded with ExitCode $ExitCode" 134 | Stop-Transcript 135 | exit $ExitCode 136 | 137 | } elseif ($ExitCode -eq 3010) { 138 | # MSI installation was successful but reboot is required after software install 139 | 140 | Write-Host "$SoftwareName $version installed successfully but requires a reboot (ExitCode: $ExitCode)" 141 | Stop-Transcript 142 | exit $ExitCode 143 | 144 | } else { 145 | # MSI installation failed 146 | 147 | Write-Host "Error installing $SoftwareName. ExitCode: $ExitCode" 148 | Write-Host "Exiting installation with ExitCode $ExitCode" 149 | Stop-Transcript 150 | Exit $ExitCode 151 | } 152 | } catch { 153 | # Handle any unexpected errors 154 | 155 | Write-Host "An unexpected fatal error occurred: $_" 156 | Write-Host "Exiting installation with ExitCode 99999" 157 | Stop-Transcript 158 | Exit 99999 159 | 160 | } 161 | } -------------------------------------------------------------------------------- /AutopilotBranding-Yodamiitti/README.md: -------------------------------------------------------------------------------- 1 | Petri's AutopilotBranding script will be here. 2 | 3 | There were some interesting findings in "all" AutopilotBranding scripts so there's a little delay until I can release current version to here. 4 | -------------------------------------------------------------------------------- /CVE-2023-24932/BlackLotus-Intune-Remediation-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/CVE-2023-24932/BlackLotus-Intune-Remediation-screenshot.png -------------------------------------------------------------------------------- /CVE-2023-24932/Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-1of3-Check if new 2023 certificate is installed to UEFI db.ps1: -------------------------------------------------------------------------------- 1 | # BlackLotus CVE-2023-24932 mitigation check 1/3 2 | # Check if Windows UEFI CA 2023 certificate is installed to UEFI database 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Devices 6 | # 24.5.2024 7 | # 8 | # Read Microsoft mitigation instructions from 9 | # https://support.microsoft.com/en-au/topic/kb5025885-how-to-manage-the-windows-boot-manager-revocations-for-secure-boot-changes-associated-with-cve-2023-24932-41a975df-beb2-40c1-99a3-b3ff139f832d 10 | 11 | 12 | # Check if Windows UEFI CA 2023 certificate is installed in UEFI database 13 | $NewUEFICertificateInstalled = [System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI db).bytes) -match 'Windows UEFI CA 2023' 14 | 15 | if($NewUEFICertificateInstalled) { 16 | # Windows UEFI CA 2023 certificate is installed to UEFI database 17 | 18 | Write-Host "Windows UEFI CA 2023 certificate is installed to UEFI database" 19 | Exit 0 20 | 21 | } else { 22 | # Windows UEFI CA 2023 certificate is NOT installed to UEFI database 23 | 24 | Write-Host "Windows UEFI CA 2023 certificate is NOT installed to UEFI database" 25 | Exit 1 26 | } 27 | -------------------------------------------------------------------------------- /CVE-2023-24932/Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-2of3-Check if new 2023 certificate is installed to Windows Boot Manager.ps1: -------------------------------------------------------------------------------- 1 | # BlackLotus CVE-2023-24932 mitigation check 2/3 2 | # 3 | # Check if Windows UEFI CA 2023 certificate 4 | # is installed to current running Windows boot manager 5 | # 6 | # Petri.Paavola@yodamiitti.fi 7 | # Microsoft MVP - Windows and Devices 8 | # 24.5.2024 9 | # 10 | # Read Microsoft mitigation instructions from 11 | # https://support.microsoft.com/en-au/topic/kb5025885-how-to-manage-the-windows-boot-manager-revocations-for-secure-boot-changes-associated-with-cve-2023-24932-41a975df-beb2-40c1-99a3-b3ff139f832d 12 | # 13 | # 14 | # This is heavily influenced by GaryTown script :) Thanks! 15 | # https://github.com/gwblok/garytown/blob/master/ConfigMgr/Baselines/CVE-2023-24932/KB5025885-CheckCompliance.ps1 16 | # 17 | # 18 | # You may also want to read GaryTown blogs about this issue 19 | # https://garytown.com/configmgr-task-sequence-kb5025885-how-to-manage-the-windows-boot-manager-revocations-for-secure-boot-changes-associated-with-cve-2023-24932 20 | # 21 | # 22 | 23 | # Get EFI folder path (without drive letter) 24 | $Volume = Get-Volume | Where-Object {$_.FileSystemType -eq "FAT32" -and $_.DriveType -eq "Fixed"} 25 | $SystemDisk = Get-Disk | Where-Object {$_.IsSystem -eq $true} 26 | $SystemPartition = Get-Partition -DiskNumber $SystemDisk.DiskNumber | Where-Object {$_.IsSystem -eq $true} 27 | $SystemVolume = $Volume | Where-Object {$_.UniqueId -match $SystemPartition.Guid} 28 | $FilePath = "$($SystemVolume.Path)\EFI\Microsoft\Boot\bootmgfw.efi" 29 | 30 | # Get bootmgfw.efi Boot Manager certificates 31 | $CertCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection 32 | $CertCollection.Import($FilePath, $null, 'DefaultKeySet') 33 | 34 | 35 | If ($CertCollection.Subject -like "*Windows UEFI CA 2023*") { 36 | # Windows UEFI CA 2023 certificate is installed to current running Windows Boot Manager 37 | 38 | Write-Host "Windows UEFI CA 2023 certificate is installed to current running Windows Boot Manager" 39 | Exit 0 40 | 41 | } else { 42 | # Windows UEFI CA 2023 certificate is NOT installed to current running Windows Boot Manager 43 | 44 | Write-Host "Windows UEFI CA 2023 certificate is NOT installed to current running Windows Boot Manager" 45 | Exit 1 46 | } 47 | -------------------------------------------------------------------------------- /CVE-2023-24932/Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-3of3-Check if old compromised 2011 certificate is revocaked in UEFI.ps1: -------------------------------------------------------------------------------- 1 | # BlackLotus CVE-2023-24932 mitigation check 3/3 2 | # Check if compromised old Microsoft Windows Production PCA 2011 3 | # certificate is revocated in UEFI database 4 | # 5 | # Petri.Paavola@yodamiitti.fi 6 | # Microsoft MVP - Windows and Devices 7 | # 24.5.2024 8 | # 9 | # Read Microsoft mitigation instructions from 10 | # https://support.microsoft.com/en-au/topic/kb5025885-how-to-manage-the-windows-boot-manager-revocations-for-secure-boot-changes-associated-with-cve-2023-24932-41a975df-beb2-40c1-99a3-b3ff139f832d 11 | 12 | 13 | # Check if compromised old Microsoft Windows Production PCA 2011 certificate 14 | # is added to UEFI certificate revocation database 15 | $CompromisedOldUEFICertificateRevocated = [System.Text.Encoding]::ASCII.GetString((Get-SecureBootUEFI dbx).bytes) -match 'Microsoft Windows Production PCA 2011' 16 | 17 | if($CompromisedOldUEFICertificateRevocated) { 18 | # Compromised old Windows Production PCA 2011 certificate is revocated in UEFI certificate database 19 | 20 | Write-Host "Compromised old Windows Production PCA 2011 certificate is revocated in UEFI certificate database" 21 | Exit 0 22 | 23 | } else { 24 | # Compromised old Windows Production PCA 2011 certificate is NOT revocated in UEFI certificate database 25 | 26 | Write-Host "Compromised old Windows Production PCA 2011 certificate is NOT revocated in UEFI certificate database" 27 | Exit 1 28 | } 29 | -------------------------------------------------------------------------------- /CVE-2023-24932/readme.md: -------------------------------------------------------------------------------- 1 | # CVE-2023-24932 - UEFI Secure Boot BlackLotus mitigations and reporting # 2 | 3 | ### Read Microsoft mitigation instructions ### 4 | [KB5025885: How to manage the Windows Boot Manager revocations for Secure Boot changes associated with CVE-2023-24932](https://support.microsoft.com/en-au/topic/kb5025885-how-to-manage-the-windows-boot-manager-revocations-for-secure-boot-changes-associated-with-cve-2023-24932-41a975df-beb2-40c1-99a3-b3ff139f832d) 5 | 6 | ### Does this affect me? ### 7 | **Yes** 8 | 9 | ### Short summary what is happening ### 10 | * BlackLotus Bootkit bypasses for example Secure Boot, Bitlocker drive encryption and Windows Defender 11 | * All Windows devices will be updated automatically by Microsoft 12 | * Process goes in 3 phases 13 | * Phase 1/3: **Secure Boot certificate needs to be updated in UEFI database** 14 | * Phase 2/3: **Windows Bootloaders will need to be signed with new certificate** 15 | * Phase 3/3: **Old Secure Boot certificate will need to be revoked from UEFI Secure Boot database** 16 | * **After 3rd phase all non updated boot medias will not** start because old Secure Boot certificate is recovated 17 | * This includes ConfigMgr/MDT PXE, WinPE, USB-drives, .ISO -images, OEM install medias, backup and recovery images, pretty much everything 18 | * This is the phase when you need to update your existing deployment methods (PXE, WinPE, USB, .ISO, etc...) 19 | * Windows devices are not trusting old bootloaders after 3rd phase so install medias need to be updated (unless Secure Boot is disabled for temporary recovery purposes) 20 | * Mitigations for Windows devices will start after July 9th 21 | * We are still waiting official instructions from Microsoft how to update all PXE, WinPE, USB and other medias 22 | * There is information but not yet clear path and instructions what to do and when 23 | 24 | ### Monitor mitigations with Intune Remediation ### 25 | 26 | 27 | 28 | **I have created 3 simple Intune remediation *Detection* scripts *to monitor* mitigations in your environment.** 29 | 30 | These scripts are only for monitoring (at least for now). They will not make any changes as this whole case is kind of tricky one to mitigate. 31 | 32 | ### Mitigation phase 1/3 report ### 33 | Name: **CVE-2023-24932 mitigation check 1/3 - Is Windows UEFI CA 2023 certificate installed to UEFI db** 34 | [Download Remediation detection script 1/3](./Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-1of3-Check%20if%20new%202023%20certificate%20is%20installed%20to%20UEFI%20db.ps1) 35 | 36 | 37 | 38 | ### Mitigation phase 2/3 report ### 39 | Name: **CVE-2023-24932 mitigation check 2/3 - Is Windows UEFI CA 2023 certificate installed to Windows Boot Manager (bootmgfw.efi)** 40 | [Download Remediation detection script 2/3](./Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-2of3-Check%20if%20new%202023%20certificate%20is%20installed%20to%20Windows%20Boot%20Manager.ps1) 41 | 42 | 43 | 44 | ### Mitigation phase 3/3 report ### 45 | Name: **CVE-2023-24932 mitigation check 3/3 - Is deprecated old Microsoft Windows Production PCA 2011 certificate revocated in UEFI db** 46 | [Download Remediation detection script 3/3](./Intune-Remediation-Detection-BlackLotus-CVE-2023-24932-3of3-Check%20if%20old%20compromised%202011%20certificate%20is%20revocaked%20in%20UEFI.ps1) 47 | 48 | **This is where the magic or pain happens. All non updated install medias and non updated Windows OSs will fail to load after this mitigation** 49 | 50 | #### Configure script options #### 51 | Run this script using the logged-on credentials: **No** 52 | Enforce script signature check: **No** 53 | Run script in 64-bit PowerShell: **Yes** 54 | 55 | ### Mitigations to PXE, WinPE and custom medias ### 56 | There are instructions how to do these. For now I'll leave this as placeholder and update when we get Microsoft official steps how to update offline-medias. 57 | 58 | ### Related blogs ### 59 | There are many other blogs and scripts for this topic which your favorite search engine will find. 60 | 61 | One of those are GaryTown's blogs and scripts 62 | [GaryTown Blog](https://garytown.com/configmgr-task-sequence-kb5025885-how-to-manage-the-windows-boot-manager-revocations-for-secure-boot-changes-associated-with-cve-2023-24932) 63 | [GaryTown GitHub](https://github.com/gwblok/garytown/tree/master/ConfigMgr/Baselines/CVE-2023-24932) 64 | -------------------------------------------------------------------------------- /Device_Query/readme.MD: -------------------------------------------------------------------------------- 1 | ## Table of Contents 2 | 3 | ### Intune Device Query examples 4 | - [Event logs Device Query](#event-logs-device-query) 5 | - [Registry Device Query](#registry-device-query) 6 | - [Files Device Query](#files-device-query) 7 | - [Certificates Device Query](#certificates-device-query) 8 | - [Process Device Query](#process-device-query) 9 | - [EncryptableVolume Device Query](#encryptablevolume-device-query) 10 | - [LocalUserAccount Device Query](#localuseraccount-device-query) 11 | - [WindowsDriver Device Query](#windowsdriver-device-query) 12 | - [WindowsService Device Query](#windowsservice-device-query) 13 | 14 | ### Intune Multi Device Query examples 15 | - [Battery](#Battery) 16 | - [BiosInfo](#BiosInfo) 17 | - [Cpu](#Cpu) 18 | - [Tpm](#Tpm) 19 | - [WindowsQfe](#windowsqfe) 20 | 21 | 22 | # Intune Device Query examples 23 | 24 | ## Event logs Device Query 25 | ``` 26 | // Get Intune MDM Management policy errors 27 | WindowsEvent('Microsoft-Windows-DeviceManagement-Enterprise-Diagnostics-Provider/Admin',12h) 28 | | where EventId == 404 and Message !contains "Fake" 29 | | project LoggedDateTime, EventId, Message 30 | ``` 31 | ``` 32 | // Get MSI Application installations from last 7 days 33 | WindowsEvent('Application', 7d) 34 | | where Message contains 'Installation completed successfully' 35 | | project LogName, EventId , Message, LoggedDateTime 36 | ``` 37 | ``` 38 | // Check for Windows 11 24H2 2025-01 Cumulative Update installation steps and times 39 | // You can see when CU was installed and when computer was restarted -> Staged state changed to Installed State 40 | WindowsEvent('Setup', 30d) 41 | | where Message contains "KB5050009" and (EventId == 1 or EventId == 4 or EventId == 2) 42 | | project LoggedDateTime, Message, EventId 43 | | order by LoggedDateTime desc 44 | ``` 45 | ``` 46 | // Get Bitlocker events from last 1 days 47 | WindowsEvent('Microsoft-Windows-Bitlocker/Bitlocker Management',1d) 48 | | order by LoggedDateTime 49 | ``` 50 | ``` 51 | // Get Bitlocker Errors and Warnings from last 7 days 52 | WindowsEvent('Microsoft-Windows-Bitlocker/Bitlocker Management',7d) 53 | | order by LoggedDateTime 54 | | where Level == 'ERROR' or Level == 'WARNING' 55 | ``` 56 | ``` 57 | // Get AppLocker warning for EXE and DLL 58 | WindowsEvent('Microsoft-Windows-AppLocker/EXE and DLL',7d) 59 | | where Level == 'Warning' 60 | ``` 61 | ``` 62 | // Get AppLocker warning for MSIs and scripts 63 | WindowsEvent('Microsoft-Windows-AppLocker/MSI and Script',1d) 64 | | where Level == 'Warning' 65 | ``` 66 | ``` 67 | // Get Boot times in last 40 days 68 | WindowsEvent('System',40d) 69 | | where EventId == 6005 70 | | order by LoggedDateTime 71 | | project LoggedDateTime, Message 72 | ``` 73 | 74 | 75 | ## Registry Device Query 76 | 77 | ``` 78 | // Get IP-address from registry 79 | WindowsRegistry('HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\*') 80 | | where ValueName contains 'IPaddress' and ValueData != '0.0.0.0' 81 | | project ValueName, ValueData 82 | ``` 83 | ``` 84 | // Get Defender for Endpoint DeviceTag value from registry 85 | WindowsRegistry('HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection\DeviceTagging') 86 | | where ValueName == 'Group' 87 | | project ValueData 88 | ``` 89 | ``` 90 | // Get Chrome policies registry keys 91 | WindowsRegistry('HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google\Chrome\*') 92 | | project RegistryKey, ValueName, ValueType, ValueData 93 | ``` 94 | ``` 95 | WindowsRegistry('HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender\Policy Manager') 96 | | where ValueData == '0' 97 | ``` 98 | 99 | ## Files Device Query 100 | ``` 101 | // Get cmd.exe file information 102 | FileInfo('c:\\windows\\system32\\cmd.exe') 103 | | project FileName, Directory, ProductVersion, DateTime=substring(tostring(CreatedDateTime), 0, 10) 104 | ``` 105 | ``` 106 | // Get all files and subfiles and subfolders in C:\Windows\Logs 107 | FileInfo('C:\Windows\Logs') 108 | ``` 109 | ``` 110 | // Get all user profiles in computer - workaround 111 | FileInfo('c:\users\*\ntuser.ini') 112 | | project User=substring(Directory,9,) 113 | | where User != 'Administrator' and User !contains 'default' and User !contains '$' 114 | | order by User asc 115 | ``` 116 | 117 | ## Certificates Device Query 118 | 119 | ``` 120 | // Get Intune Suite CloudPKI certificate 121 | Certificate 122 | | where CommonName startswith 'CloudPKI' 123 | | project CommonName, ValidToDateTime, SelfSigned, SigningAlgorithm, KeyStrength 124 | ``` 125 | ``` 126 | // Get DigiCert issuer certificates 127 | Certificate 128 | | where Issuer contains 'DigiCert' 129 | | project CommonName, ValidToDateTime, SelfSigned, SigningAlgorithm, KeyStrength 130 | ``` 131 | 132 | ## Process Device Query 133 | ``` 134 | // Get explorer.exe process information 135 | Process 136 | | where ProcessName == 'explorer.exe' 137 | | project ProcessId, ProcessName, Path, TotalSizeBytes 138 | ``` 139 | 140 | ## EncryptableVolume Device Query 141 | ``` 142 | EncryptableVolume 143 | | join Tpm 144 | | project WindowsDriveLetter, ProtectionStatus, EncryptionMethod, EncryptionPercentage, Activated, Enabled, SpecVersion, Manufacturer 145 | | where ProtectionStatus == 'protected' 146 | ``` 147 | 148 | ## LocalUserAccount Device Query 149 | ``` 150 | // List all local user accounts 151 | LocalUserAccount 152 | | project UserId, Username, HomeDirectory, WindowsSid 153 | ``` 154 | 155 | ## WindowsDriver Device Query 156 | 157 | ``` 158 | // Get Microsoft drivers 159 | WindowsDriver 160 | | where DriverDescription contains 'Microsoft' 161 | ``` 162 | 163 | ## WindowsService Device Query 164 | 165 | ``` 166 | // Get Windows services which have StartMode Automatic but current state is stopped 167 | WindowsService 168 | | where StartMode == "AUTO" 169 | | where State == "STOPPED" 170 | ``` 171 | 172 | # Intune Multi Device Query examples 173 | 174 | ## Battery 175 | ``` 176 | // Get devices Battery CycleCount and sort CycleCount descending and show only few properties (with project) 177 | Battery 178 | | project Device, CycleCount, DesignedCapacity, FullChargedCapacity 179 | | order by CycleCount desc 180 | ``` 181 | ``` 182 | // Get devices where Battery FullChargeCapacity is lower than DesignedCapacity. 183 | // This tells us if Battery is not 100% anymore 184 | Battery 185 | | where FullChargedCapacity < DesignedCapacity 186 | | order by CycleCount desc 187 | ``` 188 | 189 | ## BiosInfo 190 | ``` 191 | // Get BiosInfo for HP devices where BIOS information is not the latest version (P78 Ver. 01.49) 192 | BiosInfo 193 | | where Manufacturer == "HP" and SmBiosVersion != "P78 Ver. 01.49" 194 | ``` 195 | 196 | ## Cpu 197 | ``` 198 | // Get ARM64 devices using exact operator 199 | Cpu 200 | | where Architecture == "ARM64" 201 | ``` 202 | ``` 203 | // Get ARM devices using startswith operator 204 | Cpu 205 | | where Architecture startswith "ARM" 206 | ``` 207 | ``` 208 | // Get ARM devices using contains operator 209 | Cpu 210 | | where Architecture contains "ARM" 211 | ``` 212 | 213 | ## Tpm 214 | ``` 215 | // Get devices where TPM is NOT in desired state 216 | Tpm 217 | | where Activated == false or Enabled == false or Owned == false 218 | ``` 219 | ``` 220 | // Get devices where TPM IS Activated 221 | Tpm 222 | | where Activated == true 223 | ``` 224 | ``` 225 | // Get devices where TPM is NOT Activated 226 | Tpm 227 | | where Activated == false 228 | ``` 229 | 230 | 231 | ## WindowsQfe 232 | ``` 233 | // Get 2025-04 Cumulative Update for W11 23H2 and W11 24H2 234 | WindowsQfe 235 | | where HotFixId == "KB5055528" or HotFixId == "KB5055523" 236 | ``` 237 | -------------------------------------------------------------------------------- /Devices/Intune_Device_SendCustomNotification.ps1: -------------------------------------------------------------------------------- 1 | # Send Custom Notification to Intune Device 2 | # 3 | # Example: 4 | # .\Intune_Device_SendCustomNotification.ps1 -id 034edbcd-a295-4124-829a-e15b50bd003a -MessageTitle "Important message!" -MessageBody "Hello World!" 5 | # 6 | # You can pass device object in pipeline. id is used automatically 7 | # Get-DeviceManagement_ManagedDevices -Filter "deviceName eq 'etunimi.sukunimi_AndroidForWork_9/19/2019_12:10 PM'" | .\Intune_Device_SendCustomNotification.ps1 -MessageTitle "Important message!" -MessageBody "Hello World!" 8 | # 9 | # TODO: Proper processing if multiple deviceObjects are passed from pipeline 10 | # 11 | # Petri.Paavola@yodamiitti.fi 12 | # 24.10.2019 13 | 14 | Param( 15 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipelineByPropertyName = $true)] [String]$id, 16 | [Parameter(Mandatory = $true, Position = 1)] [String]$MessageTitle, 17 | [Parameter(Mandatory = $true, Position = 2)] [String]$MessageBody 18 | ) 19 | 20 | 21 | <# Request body JSON syntax 22 | { 23 | "notificationTitle":"Message from GraphAPI", 24 | "notificationBody":"Hello World!" 25 | } 26 | #> 27 | 28 | # Create Powershell custom object which will be converted to JSON 29 | $TargetObject = $null 30 | $TargetObject = New-Object PSObject 31 | $TargetObject | Add-Member NoteProperty 'notificationTitle' $MessageTitle 32 | $TargetObject | Add-Member NoteProperty 'notificationBody' $MessageBody 33 | 34 | # Create json from Powershell custom object 35 | # Cast variable as String, otherwise Invoke-MSGraphRequest will fail 36 | # This is really important step! 37 | [String]$SendMessageBodyJSON = $TargetObject | ConvertTo-json 38 | 39 | # Debug 40 | #$SendMessageBodyJSON 41 | 42 | # Device Custom Notification Message url 43 | $url = "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$id/sendCustomNotificationToCompanyPortal" 44 | 45 | # Send MSGraph request 46 | Invoke-MSGraphRequest -Url $url -Content $SendMessageBodyJSON -HttpMethod 'POST' 47 | $Success = $? 48 | 49 | Write-Output "Send Device (id:$id) Notification success: $Success" 50 | -------------------------------------------------------------------------------- /Devices/README.md: -------------------------------------------------------------------------------- 1 | # Intune Powershell Native Cmdlet examples and scripts 2 | -------------------------------------------------------------------------------- /Groups/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Linux/Intune_Linux_Custom_Compliance_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Intune Linux Custom Compliance script 4 | # This script runs actual compliance check as Powershell script 5 | # which is included in this script 6 | # 7 | # Compliance checks: 8 | # Check Powershell is installed 9 | # Reboot Required check (file should not exist /var/run/reboot-required) 10 | # Check MS Edge is installed (file should exist /opt/microsoft/msedge/msedge 11 | # Check MS Edge version 12 | # Check Powershell version 13 | # Check Kernel version 14 | # Check Kernel patch level 15 | # Check Kernel flavour 16 | # Check Kernel tainted state 17 | # Check SecureBoot status 18 | # Check sysctrl values (placeholder to check any value) 19 | # user.max_user_namespaces 20 | # Check gsettings values (placeholder to check any value) 21 | # org.gnome.desktop.screensaver lock-enabled 22 | # org.gnome.desktop.screensaver idle-activation-enabled 23 | # org.gnome.desktop.session idle-delay 24 | # Check Defender for Endpoint on Linux status 25 | # MicrosoftDefenderForEndpointOnLinux_Installed 26 | # MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization 27 | # MicrosoftDefenderForEndpointOnLinux_Healthy 28 | # MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date 29 | # MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled 30 | # 31 | # 32 | # Script creates 2 log files for debugging (which you may want to disable in production?) 33 | # /tmp/IntuneCustomComplianceScript_Bash.log 34 | # /tmp/IntuneCustomComplianceScript_Powershell.log 35 | # 36 | # Note: Make sure you have installed Powershell before running this script 37 | # 38 | # Note: make sure you are NOT putting anything 39 | # to STDOUT because that breaks the script 40 | # Powershell script returns compressed JSON to STDOUT 41 | # which is passed to Intune for compliance validation check 42 | # 43 | # 44 | # Petri.Paavola@yodamiitti.fi 45 | # Microsoft MVP - Windows and Devices for IT 46 | # 15.3.2023 47 | # 48 | # https://github.com/petripaavola/Intune/tree/master/Linux 49 | 50 | 51 | Version=3.0 52 | BashLogFilePath=/tmp/IntuneCustomComplianceScript_Bash.log 53 | 54 | # Create log for debugging 55 | echo "Intune Linux Custom Compliance bash script version $Version" > $BashLogFilePath 56 | echo "Runtime: $(/bin/date +%Y-%m-%d_%H:%M:%S)" >> $BashLogFilePath 57 | echo "Running script: $0" >> $BashLogFilePath 58 | echo "Running as user: $( /bin/whoami; )" >> $BashLogFilePath 59 | echo "Working directory: $( /bin/pwd; )" >> $BashLogFilePath 60 | 61 | # Create Powershell commands string 62 | PowerShellCommandsString=' 63 | # Note: Do not use single quotes in this Powershell script 64 | # because it will break the bash script 65 | # 66 | # Note: Start-Transcript breaks the script 67 | # Note: using Write-Host causes unnecessary data returned to bash script and may break the script 68 | # Note: using Write-Output breaks the script 69 | 70 | $ScriptStartTime = Get-Date 71 | 72 | # Create hash for JSON 73 | # We will add values 74 | $hash = @{} 75 | 76 | # Add PowershellInstalled=$True 77 | $hash.Add("PowershellInstalled", $True) 78 | 79 | # Create logData array 80 | $logData = @() 81 | $logData += "###################################################" 82 | $logData += "Starting Intune Linux Custom Compliance script Powershell`n" 83 | 84 | $RunTime = Get-Date -Format yyyyMMdd-HHmmss 85 | $logData += "RunTime = $RunTime" 86 | 87 | $Whoami = whoami 88 | $logData += "Whoami = $Whoami" 89 | 90 | $CurrentPath = Get-Location | Select-Object -ExpandProperty Path 91 | $logData += "CurrentPath = $CurrentPath" 92 | 93 | 94 | #################################################################################### 95 | # Check reboot pending 96 | 97 | $logData += "###################################################" 98 | $logData += "Run Reboot Pending compliance check`n" 99 | $logData += "file should not exist /var/run/reboot-required" 100 | if(Test-Path /var/run/reboot-required) { 101 | $RebootRequired = $True 102 | } else { 103 | $RebootRequired = $False 104 | } 105 | $logData += "RebootRequired=$RebootRequired" 106 | $hash.Add("RebootRequired", $RebootRequired) 107 | 108 | 109 | #################################################################################### 110 | # Check MSEdge is installed 111 | 112 | $logData += "###################################################" 113 | $logData += "Run Microsoft Edge is installed check`n" 114 | $logData += "file should exist /opt/microsoft/msedge/msedge" 115 | if(Test-Path /opt/microsoft/msedge/msedge) { 116 | $MSEdgeInstalled = $True 117 | } else { 118 | $MSEdgeInstalled = $False 119 | } 120 | $logData += "MSEdgeInstalled=$MSEdgeInstalled" 121 | $hash.Add("MSEdgeInstalled", $MSEdgeInstalled) 122 | 123 | 124 | #################################################################################### 125 | # Check MSEdge version 126 | 127 | $logData += "###################################################" 128 | $logData += "Run Microsoft Edge version check`n" 129 | $logData += "with command /opt/microsoft/msedge/msedge --version" 130 | 131 | $MSEdgeVersionString = /opt/microsoft/msedge/msedge --version 132 | # Example string which can be tested with regex101.com 133 | # Microsoft Edge 109.0.1518.70 unknown 134 | $regex = "^.* ([0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}\.[0-9]{1,}).*" 135 | if($MSEdgeVersionString -match $regex) { 136 | $MSEdgeVersion = $Matches[1] 137 | } else { 138 | # Regex did not find MSEdge version 139 | $MSEdgeVersion = "0.0.0.0" 140 | } 141 | 142 | $logData += "MSEdgeVersion=$MSEdgeVersion" 143 | $hash.Add("MSEdgeVersion", $MSEdgeVersion) 144 | 145 | 146 | #################################################################################### 147 | # Check Powershell version 148 | 149 | $logData += "###################################################" 150 | $logData += "Run Powershell version check`n" 151 | [String]$PSVersion = $PSVersionTable | Select-Object -ExpandProperty PSVersion 152 | $logData += "PSVersion=$PSVersion" 153 | $hash.Add("PSVersion", $PSVersion) 154 | 155 | 156 | #################################################################################### 157 | # Check Kernel version, patch level and flavour 158 | 159 | $logData += "###################################################" 160 | $logData += "Run Kernel version check`n" 161 | # Example kernel version: 5.15.0-52-generic 162 | # Kernel version is: 5.15.0 163 | # Patch level is: 52 164 | # Linux distro/kernel specific additional info: generic 165 | 166 | # Get kernel information 167 | $KernelVersionUname = & /bin/uname -r 168 | 169 | # Extract version info to own variables 170 | $KernelVersion = ($KernelVersionUname.Split("-"))[0] 171 | $logData += "KernelVersion=$KernelVersion" 172 | $hash.Add("KernelVersion", $KernelVersion) 173 | 174 | $KernelPatchLevel = ($KernelVersionUname.Split("-"))[1] 175 | # Add .0 to kernel patch level version 52 -> 52.0 176 | # Otherwise our compliance JSON will no work correctly for Version comparison 177 | $KernelPatchLevel = "$($KernelPatchLevel).0" 178 | $logData += "KernelPatchLevel=$KernelPatchLevel" 179 | $hash.Add("KernelPatchLevel", $KernelPatchLevel) 180 | 181 | $KernelFlavour = ($KernelVersionUname.Split("-"))[2] 182 | $logData += "KernelFlavour=$KernelFlavour" 183 | $hash.Add("KernelFlavour", $KernelFlavour) 184 | 185 | 186 | #################################################################################### 187 | # Check Kernel tainted state 188 | # 189 | # There is a lot going on with tainted kernel situation 190 | # Kernel could be in tainted mode intentionally 191 | # when using for example proprietary NVIDIA or AMD graphics drivers 192 | # But many times tainted kernel state is causing bigger problems 193 | # https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html#decoding-tainted-state-at-runtime 194 | # Fix for tainted kernel is to reboot the device 195 | # Value 0 is normal when kernel is NOT tainted 196 | 197 | $logData += "###################################################" 198 | $logData += "Run Kernel tainted state check`n" 199 | 200 | [int]$TaintedKernelState = Get-Content /proc/sys/kernel/tainted 201 | $logData += "/proc/sys/kernel/tainted" 202 | $logData += "$TaintedKernelState" 203 | 204 | $logData += "TaintedKernelState = $TaintedKernelState" 205 | $hash.Add("TaintedKernelState", $TaintedKernelState) 206 | 207 | 208 | #################################################################################### 209 | # Check SecureBoot status 210 | 211 | $logData += "###################################################" 212 | $logData += "Check SecureBoot status`n" 213 | 214 | $SecureBootStatus = & /bin/mokutil --sb-state 215 | $logData += "/bin/mokutil --sb-state result" 216 | $logData += "$SecureBootStatus" 217 | 218 | # SecureBoot status can be: 219 | # SecureBoot enabled 220 | # SecureBoot disabled 221 | if($SecureBootStatus -eq "SecureBoot enabled") { 222 | $SecureBootEnabled = $True 223 | } else { 224 | $SecureBootEnabled = $False 225 | } 226 | $logData += "SecureBootEnabled = $SecureBootEnabled" 227 | $hash.Add("SecureBootEnabled", $SecureBootEnabled) 228 | 229 | 230 | #################################################################################### 231 | # Get sysctl values 232 | # This is another pandoras box of values 233 | # Either disable this step, include only necessary values or pass on all the values 234 | # If you pass all the values then you can later deside what values to check in JSON 235 | # Note: passing all values might slow Compliance check 236 | 237 | $logData += "###################################################" 238 | $logData += "Get sysctl values and convert to hashtable`n" 239 | 240 | # Get sysctrl values 241 | $sysctlArray = & /sbin/sysctl -a 242 | 243 | $sysctlValuesAddedToHash = 0 244 | 245 | 246 | # 247 | # 248 | # # # 249 | ### 250 | # 251 | 252 | # Process only specified sysctl settings 253 | $sysctlArray = $sysctlArray | Where-Object { 254 | $_ -like "user.max_user_namespaces*" -or 255 | $_ -like "net.ipv4.conf.all.accept_redirects*" 256 | } 257 | 258 | # 259 | ### 260 | # # # 261 | # 262 | # 263 | 264 | 265 | # Process all sysctl values except ignore specified values 266 | # Ignore dev.cdrom.info -values because there are many values with overlapping name 267 | #$sysctlArray = $sysctlArray | Where-Object { $_ -notlike "dev.cdrom.info*"} 268 | 269 | 270 | # Create hash table from values 271 | foreach($sysctl in $sysctlArray) { 272 | $sysctlValueName = $null 273 | $sysctlValue = $null 274 | $Value = $null 275 | 276 | # Validate and extract data with regex 277 | # You can test and validate regex in https://regex101.com 278 | # String: user.max_user_namespaces = 15233 279 | # Regex: ^(.*) = (.*)$ 280 | 281 | $regex = "^(.*) = (.*)$" 282 | $Success = $sysctl -match $regex 283 | 284 | # Continue only if regex was match 285 | if($Success) { 286 | # Extract values 287 | $sysctlValueName = "sysctl $($Matches[1])" 288 | $sysctlValue = ($Matches[2]).Trim() 289 | 290 | if($sysctlValue) { 291 | # We have name and value 292 | 293 | # Trim empty spaces 294 | $sysctlValue = $sysctlValue.Trim() 295 | 296 | $logData += "`$sysctlValue=$sysctlValue" 297 | $logData += "GetType `$sysctlValue=$($sysctlValue.GetType())" 298 | 299 | # Check if value is integer 300 | if(($sysctlValue -is [Int32]) -or ($sysctlValue -is [Int64]) -or ($sysctlValue -is [uint32])) { 301 | $Value = [int64]$sysctlValue 302 | $logData += "`$Value=$Value is integer" 303 | } else { 304 | # Cast value as String 305 | $Value = [String]$sysctlValue 306 | $logData += "`$Value=$Value is string" 307 | } 308 | 309 | } else { 310 | # We have name but value is null 311 | $Value = $null 312 | } 313 | 314 | # Add hash key/value if it does not already exist 315 | # This prevents us getting unnecessary errors which could fail this bash script 316 | # and help debugging possible problems 317 | if(-not ($hash.ContainsKey($sysctlValueName))) { 318 | $hash.Add($sysctlValueName, $Value) 319 | 320 | $sysctlValuesAddedToHash++ 321 | } else { 322 | $logData += "Skipped duplicate hash valueName: $sysctlValueName" 323 | } 324 | 325 | } else { 326 | # sysctl line did not match our regex 327 | # Print value to log for debugging 328 | $logData += "Did not match regex: $sysctl" 329 | } 330 | } 331 | 332 | $logData += "Added $sysctlValuesAddedToHash sysctl values to hash" 333 | 334 | 335 | #################################################################################### 336 | # Get gsettings 337 | 338 | # Configure below gsettings to be included in compliance check 339 | 340 | # 341 | # 342 | # # # 343 | ### 344 | # 345 | 346 | # Add only selected settings to $hash 347 | $SelectedGsettingsArray = @( 348 | "gsettings get org.gnome.desktop.screensaver lock-enabled",` 349 | "gsettings get org.gnome.desktop.screensaver idle-activation-enabled",` 350 | "gsettings get org.gnome.desktop.session idle-delay" 351 | ) 352 | 353 | # 354 | ### 355 | # # # 356 | # 357 | # 358 | 359 | $logData += "###################################################" 360 | $logData += "Get gsettings`n" 361 | 362 | ### function start ### 363 | function Add_gsettingsArrayToHash { 364 | Param( 365 | [Parameter(Mandatory=$true, 366 | ValueFromPipeline=$true, 367 | Position=0)] 368 | $gsettingsArray 369 | ) 370 | 371 | $gsettingsHash = @{} 372 | 373 | foreach($gsetting in $gsettingsArray) { 374 | $gsettingName = $null 375 | $gsettingValue = $null 376 | $Value = $null 377 | 378 | # gsettings are either boolean, uint32, integer or string types (all else are strings) 379 | # org.gnome.desktop.screensaver idle-activation-enabled true 380 | # org.gnome.desktop.screensaver lock-delay uint32 0 381 | # org.gnome.desktop.screensaver picture-opacity 100 382 | # org.gnome.desktop.screensaver picture-options singlequotezoomsinglequote 383 | # org.gnome.desktop.search-providers disabled $as [] 384 | 385 | 386 | # org.gnome.desktop.screensaver lock-delay uint32 0 387 | # Get uint32 regex match lines 388 | if($gsetting -match "^(.*) (.*) uint32 (.*)$") { 389 | # setting is uint32 type 390 | $gsettingName = "gsettings get $($matches[1]) $($matches[2])" 391 | $gsettingValue = [uint32]$matches[3] 392 | 393 | } elseif ($gsetting -match "^(.*?) (.*?) (.*)$") { 394 | # Using ? in first 2 groups to get lazy regex 395 | # so it would match lines ending with (includes additional space): @a{ss} {} 396 | # org.gnome.desktop.search-providers disabled $as [] 397 | 398 | # There was no uint32 type value 399 | 400 | # setting is boolean, integer or string type (we ignore other variable types) 401 | $gsettingName = "gsettings get $($matches[1]) $($matches[2])" 402 | $Value = $matches[3] 403 | 404 | if(($Value -is [int32]) -or ($Value -is [uint32]) -or ($Value -is [int64])) { 405 | 406 | # $Value is integer 407 | $gsettingValue = [int64]$Value 408 | 409 | } elseif(($Value -eq "true") -or ($Value -eq "false")) { 410 | 411 | # value is boolean 412 | if($Value -eq "true") { 413 | $gsettingValue = $True 414 | } else { 415 | $gsettingValue = $False 416 | } 417 | 418 | } else { 419 | # Save Value as string 420 | $gsettingValue = [string]$Value 421 | } 422 | 423 | } else { 424 | # Regex did not match anything 425 | $logData += "regex did not catch this: $gsetting" 426 | 427 | } 428 | 429 | # Add gsetting to hash if we have Hash Key-value -> our regex worked 430 | if($gsettingName) { 431 | if(-not ($gsettingsHash.ContainsKey($gsettingName))) { 432 | $gsettingsHash.Add($gsettingName, $gsettingValue) 433 | } else { 434 | $logData += "Skipped duplicate hash Key: $gsettingName" 435 | } 436 | } else { 437 | $logData += "Regex did not catch gsettings line. Fix regex in this script! $gsettingName" 438 | } 439 | } 440 | 441 | return $gsettingsHash 442 | } 443 | ### function end ### 444 | 445 | 446 | # Get all gsettings to array 447 | $logData += "Get all gsettings" 448 | $gsettingsArray = & /bin/gsettings list-recursively 449 | 450 | # Parse gsettingsArray results and return hashtable 451 | $logData += "Parse gsettingsArray results and return hashtable" 452 | $gsettingsHashtable = Add_gsettingsArrayToHash $gsettingsArray 453 | 454 | 455 | # $SelectedGsettingsArray this array was manually specified in the beginning of this block 456 | # Add specified gsettings to $hash 457 | foreach($gsetting in $SelectedGsettingsArray) { 458 | # Make sure hash Key exists in $gsettingsHashtable before adding it to $hash 459 | if($gsettingsHashtable.ContainsKey($gsetting)) { 460 | if(-not ($hash.ContainsKey($gsetting))) { 461 | $logData += "Add to `$hash: Key: $gsetting Value: $($gsettingsHashtable[$gsetting])" 462 | $hash.Add($gsetting, $gsettingsHashtable[$gsetting]) 463 | } else { 464 | $logData += "Skipped duplicate hash Name: $gsetting" 465 | $logData += "This should not be possible case normally" 466 | } 467 | } else { 468 | $logData += "`$gsettingsHashtable Key not found: $gsetting" 469 | $logData += "usually this means there is typo in `$SelectedGsettingsArray gsettings entries" 470 | } 471 | } 472 | 473 | 474 | <# 475 | # Do not do below because JSON will get BIG. 476 | # Left code here just in case needed in the future 477 | # 478 | # Add all $gsettingsHashtable hash values to $hash which we will convert to JSON in the end 479 | foreach($key in $gsettingsHashtable.keys) { 480 | if(-not ($hash.ContainsKey($key))) { 481 | $hash.Add($key, $gsettingsHashtable[$key]) 482 | } else { 483 | $logData += "Skipped duplicate hash Key: $($key)" 484 | $logData += "This should not be possible case normally" 485 | } 486 | } 487 | #> 488 | 489 | 490 | #################################################################################### 491 | # Check Microsoft Defender Endpoint for Linux status 492 | 493 | $logData += "###################################################" 494 | $logData += "Check Microsoft Defender Endpoint for Linux status`n" 495 | 496 | # Check mdatp is installed in default path 497 | $mdatpPath = "/opt/microsoft/mdatp/sbin/wdavdaemonclient" 498 | if(Test-Path $mdatpPath) { 499 | $MicrosoftDefenderForEndpointOnLinux_Installed = $True 500 | 501 | # Check Defender is registered to organization 502 | $mdatp_org_id = & $mdatpPath --field org_id 503 | 504 | # Example value includes also double quotes 505 | # "0899fda5-fcad-42ef-9325-f8a0c7e30560" 506 | $regex = "^?[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}?$" 507 | if($mdatp_org_id -match $regex) { 508 | # Found valid guid 509 | $MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization = $True 510 | } else { 511 | # Did not find valid guid 512 | $MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization = $True 513 | } 514 | 515 | # Check Defender is functioning properly (is healthy) 516 | $mdatp_healthy = & $mdatpPath health --field healthy 517 | 518 | if($mdatp_healthy -eq "true") { 519 | # Defender is healthy 520 | $MicrosoftDefenderForEndpointOnLinux_Healthy = $True 521 | } else { 522 | # Defender is NOT healthy 523 | $MicrosoftDefenderForEndpointOnLinux_Healthy = $False 524 | } 525 | 526 | # Check Defender definitions are up_to_date 527 | $mdatp_definitions_status = & $mdatpPath health --field definitions_status 528 | 529 | if($mdatp_definitions_status -eq "`"up_to_date`"") { 530 | # Definitions are up to date 531 | $MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date = $True 532 | } else { 533 | # Definitions are NOT up to date 534 | $MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date = $False 535 | } 536 | 537 | # Check Defender real_time_protection_enabled 538 | $mdatp_real_time_protection_enabled = & $mdatpPath health --field real_time_protection_enabled 539 | 540 | if($mdatp_real_time_protection_enabled -eq "true") { 541 | # Defender Realtime Protection is enabled 542 | $MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled = $True 543 | } else { 544 | # Defender Realtime Protection is NOT enabled 545 | $MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled = $False 546 | } 547 | 548 | } else { 549 | # Microsoft Defender Endpoint for Linux status is NOT installed 550 | 551 | $MicrosoftDefenderForEndpointOnLinux_Installed = $False 552 | $MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization = $False 553 | $MicrosoftDefenderForEndpointOnLinux_Healthy = $False 554 | $MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date = $False 555 | $MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled = $False 556 | 557 | } 558 | 559 | # Add Defender information to $hash 560 | 561 | $logData += "MicrosoftDefenderForEndpointOnLinux_Installed=$MicrosoftDefenderForEndpointOnLinux_Installed" 562 | $hash.Add("MicrosoftDefenderForEndpointOnLinux_Installed", $MicrosoftDefenderForEndpointOnLinux_Installed) 563 | 564 | $logData += "MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization=$MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization" 565 | $hash.Add("MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization", $MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization) 566 | 567 | $logData += "MicrosoftDefenderForEndpointOnLinux_Healthy=$MicrosoftDefenderForEndpointOnLinux_Healthy" 568 | $hash.Add("MicrosoftDefenderForEndpointOnLinux_Healthy", $MicrosoftDefenderForEndpointOnLinux_Healthy) 569 | 570 | $logData += "MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date=$MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date" 571 | $hash.Add("MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date", $MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date) 572 | 573 | $logData += "MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled=$MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled" 574 | $hash.Add("MicrosoftDefenderForEndpointOnLinux_Real_time_protection_enabled", $MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled) 575 | 576 | 577 | $logData += "###################################################" 578 | 579 | # Convert hashtable to JSON 580 | $returnJson = $hash | ConvertTo-Json -Compress 581 | 582 | $logData += "Custom Compliance returnJson:`n" 583 | $logData += "$returnJson" 584 | $logData += "" 585 | 586 | $logData += "###################################################" 587 | $logData += "Powershell script end`n" 588 | 589 | # Calculate script runtime 590 | $ScriptEndTime = Get-Date 591 | $ScriptRunTime = New-Timespan $ScriptStartTime $ScriptEndTime 592 | $logData += "Script runtime $($ScriptRunTime.Minutes) min $($ScriptRunTime.Seconds) sec $($ScriptRunTime.Milliseconds) millisec" 593 | 594 | $logData += "###################################################" 595 | $logData += "Possible error data below`n" 596 | 597 | # Export log array to log file 598 | $logData | Out-File -FilePath /tmp/IntuneCustomComplianceScript_Powershell.log -Force 599 | 600 | # Export error data to help debugging errors in Powershell 601 | # Of course we will not make any errors but just in case :) 602 | if($Error) { 603 | $Error | Out-File -FilePath /tmp/IntuneCustomComplianceScript_Powershell.log -Append 604 | } 605 | 606 | # Return compressed JSON 607 | return $returnJson 608 | ' 609 | 610 | echo "####################################################" >> $BashLogFilePath 611 | echo "Running Powershell script: $PowerShellCommandsString" >> $BashLogFilePath 612 | echo "####################################################" >> $BashLogFilePath 613 | 614 | # Check that pwsh exist 615 | PWSHPATH=/opt/microsoft/powershell/7/pwsh 616 | if [ -f "$PWSHPATH" ]; then 617 | # Powershell found 618 | # Run Powershell Commands 619 | 620 | echo "Run Powershell script part" >> $BashLogFilePath 621 | JSON=$($PWSHPATH -Command "& { ${PowerShellCommandsString} }") 622 | ExitCode=$? 623 | echo "Powershell script run exitcode: $ExitCode" >> $BashLogFilePath 624 | echo "JSON From Powershell" >> $BashLogFilePath 625 | echo "" >> $BashLogFilePath 626 | echo "$JSON" >> $BashLogFilePath 627 | echo "" >> $BashLogFilePath 628 | echo "Bash script end" >> $BashLogFilePath 629 | echo "$JSON" 630 | else 631 | # pwsh (Powershell) not found 632 | # return hardcoded json which will make device Not Compliant 633 | 634 | echo "pwsh not found from path: $PWSHPATH" >> $BashLogFilePath 635 | echo "Compliance Check will fail" >> $BashLogFilePath 636 | JSON='{"PowershellInstalled":false}' 637 | echo "Return json:" >> $BashLogFilePath 638 | echo "$JSON" >> $BashLogFilePath 639 | echo "Bash script end" >> $BashLogFilePath 640 | echo "$JSON" 641 | fi 642 | -------------------------------------------------------------------------------- /Linux/Intune_Linux_Custom_Compliance_script_Rules_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "Rules":[ 3 | { 4 | "SettingName":"PowershellInstalled", 5 | "Operator":"IsEquals", 6 | "DataType":"Boolean", 7 | "Operand":true, 8 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.2", 9 | "RemediationStrings":[ 10 | { 11 | "Language": "en_US", 12 | "Title": "Powershell not installed", 13 | "Description": "Powershell is required with Compliance Checks scripts. Install Powershell and Refresh settings." 14 | } 15 | ] 16 | }, 17 | { 18 | "SettingName":"PSVersion", 19 | "Operator":"GreaterEquals", 20 | "DataType":"Version", 21 | "Operand":"7.3.3", 22 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.2", 23 | "RemediationStrings":[ 24 | { 25 | "Language": "en_US", 26 | "Title": "Powershell version is not up to date.", 27 | "Description": "Required Powershell version 7.3.3 provides tools used with Compliance Checks scripts. Update Powershell and Refresh settings." 28 | } 29 | ] 30 | }, 31 | { 32 | "SettingName":"SecureBootEnabled", 33 | "Operator":"IsEquals", 34 | "DataType":"Boolean", 35 | "Operand":true, 36 | "MoreInfoUrl":"https://wiki.ubuntu.com/UEFI/SecureBoot", 37 | "RemediationStrings":[ 38 | { 39 | "Language": "en_US", 40 | "Title": "SecureBoot is not enabled.", 41 | "Description": "SecureBoot needs to enabled to get access to company resources. Enable SecureBoot and Refresh settings." 42 | } 43 | ] 44 | }, 45 | { 46 | "SettingName":"MicrosoftDefenderForEndpointOnLinux_Installed", 47 | "Operator":"IsEquals", 48 | "DataType":"Boolean", 49 | "Operand":true, 50 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-linux", 51 | "RemediationStrings":[ 52 | { 53 | "Language": "en_US", 54 | "Title": "Your Organization requires Microsoft Defender for Endpoint on Linux devices.", 55 | "Description": "Install Defender for Endpoint and try again." 56 | } 57 | ] 58 | }, 59 | { 60 | "SettingName":"MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization", 61 | "Operator":"IsEquals", 62 | "DataType":"Boolean", 63 | "Operand":true, 64 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-linux", 65 | "RemediationStrings":[ 66 | { 67 | "Language": "en_US", 68 | "Title": "Your Organization requires Microsoft Defender for Endpoint on Linux devices is registered to your organization.", 69 | "Description": "Configure Defender for Endpoint and try again." 70 | } 71 | ] 72 | }, 73 | { 74 | "SettingName":"MicrosoftDefenderForEndpointOnLinux_Healthy", 75 | "Operator":"IsEquals", 76 | "DataType":"Boolean", 77 | "Operand":true, 78 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-linux", 79 | "RemediationStrings":[ 80 | { 81 | "Language": "en_US", 82 | "Title": "Your Organization requires Microsoft Defender for Endpoint on Linux devices is in healthy state.", 83 | "Description": "Configure and/or update Defender for Endpoint and try again." 84 | } 85 | ] 86 | }, 87 | { 88 | "SettingName":"MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date", 89 | "Operator":"IsEquals", 90 | "DataType":"Boolean", 91 | "Operand":true, 92 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-linux", 93 | "RemediationStrings":[ 94 | { 95 | "Language": "en_US", 96 | "Title": "Your Organization requires Microsoft Defender for Endpoint on Linux devices is updated.", 97 | "Description": "Update Defender for Endpoint Definitions and try again." 98 | } 99 | ] 100 | }, 101 | { 102 | "SettingName":"MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled", 103 | "Operator":"IsEquals", 104 | "DataType":"Boolean", 105 | "Operand":true, 106 | "MoreInfoUrl":"https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-endpoint-linux", 107 | "RemediationStrings":[ 108 | { 109 | "Language": "en_US", 110 | "Title": "Your Organization requires Microsoft Defender for Endpoint on Linux devices has Realtime scan enable.", 111 | "Description": "Configure Defender for Endpoint Realtime settings and try again." 112 | } 113 | ] 114 | }, 115 | { 116 | "SettingName":"RebootRequired", 117 | "Operator":"IsEquals", 118 | "DataType":"Boolean", 119 | "Operand":false, 120 | "MoreInfoUrl":"https://www.google.com", 121 | "RemediationStrings":[ 122 | { 123 | "Language": "en_US", 124 | "Title": "Reboot Required detected.", 125 | "Description": "Reboot computer and Refresh settings." 126 | } 127 | ] 128 | }, 129 | { 130 | "SettingName":"MSEdgeInstalled", 131 | "Operator":"IsEquals", 132 | "DataType":"Boolean", 133 | "Operand":true, 134 | "MoreInfoUrl":"https://www.microsoft.com/en-us/edge", 135 | "RemediationStrings":[ 136 | { 137 | "Language": "en_US", 138 | "Title": "Microsoft Edge browser is not installed.", 139 | "Description": "Microsoft Edge browser is used for safe access to company resources. Install Microsoft Edge browser and Refresh settings." 140 | } 141 | ] 142 | }, 143 | { 144 | "SettingName":"MSEdgeVersion", 145 | "Operator":"GreaterEquals", 146 | "DataType":"Version", 147 | "Operand":"111.0.1661.41", 148 | "MoreInfoUrl":"https://www.microsoft.com/en-us/edge", 149 | "RemediationStrings":[ 150 | { 151 | "Language": "en_US", 152 | "Title": "Microsoft Edge version is not up to date.", 153 | "Description": "Required Microsfot Edge version should be at least 111.0.1661.41. Update Microsoft Edge and Refresh settings." 154 | } 155 | ] 156 | }, 157 | { 158 | "SettingName":"KernelVersion", 159 | "Operator":"GreaterEquals", 160 | "DataType":"Version", 161 | "Operand":"5.19.0", 162 | "MoreInfoUrl":"https://www.google.com", 163 | "RemediationStrings":[ 164 | { 165 | "Language": "en_US", 166 | "Title": "Linux kernel version is not up to date.", 167 | "Description": "Required kernel version should be at least 5.19.0. Update your device and Refresh settings." 168 | } 169 | ] 170 | }, 171 | { 172 | "SettingName":"KernelPatchLevel", 173 | "Operator":"GreaterEquals", 174 | "DataType":"Version", 175 | "Operand":"35.0", 176 | "MoreInfoUrl":"https://www.google.com", 177 | "RemediationStrings":[ 178 | { 179 | "Language": "en_US", 180 | "Title": "Linux kernel Patch version is not up to date.", 181 | "Description": "Required kernel Patch version should be at least 35. Update your device and Refresh settings." 182 | } 183 | ] 184 | }, 185 | { 186 | "SettingName":"KernelFlavour", 187 | "Operator":"IsEquals", 188 | "DataType":"String", 189 | "Operand":"generic", 190 | "MoreInfoUrl":"https://www.microsoft.com/en-us/edge", 191 | "RemediationStrings":[ 192 | { 193 | "Language": "en_US", 194 | "Title": "Linux kernel flavour is wrong.", 195 | "Description": "Kernel flavour should be generic. Change kernel to generic flavour and Refresh settings." 196 | } 197 | ] 198 | }, 199 | { 200 | "SettingName":"TaintedKernelState", 201 | "Operator":"IsEquals", 202 | "DataType":"Int64", 203 | "Operand":0, 204 | "MoreInfoUrl":"https://www.kernel.org/doc/html/latest/admin-guide/tainted-kernels.html#decoding-tainted-state-at-runtime", 205 | "RemediationStrings":[ 206 | { 207 | "Language": "en_US", 208 | "Title": "Linux kernel is in tainted mode.", 209 | "Description": "Tainted kernel can cause a lot of problems. NVIDIA or AMD graphics drivers can cause kernel tainted mode. Contact your administrator if you think this is false positive error. Reboot computer and Refresh settings." 210 | } 211 | ] 212 | }, 213 | { 214 | "SettingName":"gsettings get org.gnome.desktop.screensaver lock-enabled", 215 | "Operator":"IsEquals", 216 | "DataType":"Boolean", 217 | "Operand":true, 218 | "MoreInfoUrl":"https://help.ubuntu.com/stable/ubuntu-help/privacy-screen-lock.html.en", 219 | "RemediationStrings":[ 220 | { 221 | "Language": "en_US", 222 | "Title": "Automatic screen lock is not enabled.", 223 | "Description": "Screen should lock automatically when screensaver is enabled. Change screen lock setting and Refresh settings." 224 | } 225 | ] 226 | }, 227 | { 228 | "SettingName":"gsettings get org.gnome.desktop.screensaver idle-activation-enabled", 229 | "Operator":"IsEquals", 230 | "DataType":"Boolean", 231 | "Operand":true, 232 | "MoreInfoUrl":"https://help.ubuntu.com/stable/ubuntu-help/privacy-screen-lock.html.en", 233 | "RemediationStrings":[ 234 | { 235 | "Language": "en_US", 236 | "Title": "Automatic Screensaver idle activation not enabled.", 237 | "Description": "Enable idle activation lock with command: gsettings set org.gnome.desktop.screensaver idle-activation-enabled true and Refresh settings." 238 | } 239 | ] 240 | }, 241 | { 242 | "SettingName":"gsettings get org.gnome.desktop.session idle-delay", 243 | "Operator":"LessEquals", 244 | "DataType":"Int64", 245 | "Operand":600, 246 | "MoreInfoUrl":"https://help.ubuntu.com/stable/ubuntu-help/display-blank.html.en", 247 | "RemediationStrings":[ 248 | { 249 | "Language": "en_US", 250 | "Title": "Screen Blank timeout over 10 minutes.", 251 | "Description": "Screen Blank maximum timeout is 10 minutes. Change Screen Blank timeout setting and Refresh settings." 252 | } 253 | ] 254 | } 255 | ] 256 | } -------------------------------------------------------------------------------- /Linux/README.md: -------------------------------------------------------------------------------- 1 | # Intune Linux Custom Compliance script 3.0 2 | Example scripts how to run Linux Custom Compliance checks with Intune. 3 | This script is Bash script which includes Powershell script inside. 4 | 5 | ![Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant_small.png](./pics/Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant_small.png) 6 | ![Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant_small.png](./pics/Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant_small.png) 7 | 8 | ## Quick links to files: 9 | * [Intune_Linux_Custom_Compliance_script.sh](https://github.com/petripaavola/Intune/blob/master/Linux/Intune_Linux_Custom_Compliance_script.sh) 10 | * [Intune_Linux_Custom_Compliance_script_Rules_file.json](https://github.com/petripaavola/Intune/blob/master/Linux/Intune_Linux_Custom_Compliance_script_Rules_file.json) 11 | 12 | ## Custom Compliance checks 13 | Custom Compliance checks are configured in **.json**-file ([Intune_Linux_Custom_Compliance_script_Rules_file.json](https://github.com/petripaavola/Intune/blob/master/Linux/Intune_Linux_Custom_Compliance_script_Rules_file.json)) 14 | 15 | You can configure, enable and disable custom compliance check settings in json file without even touching to .sh script. For correct json and rule operator syntax it is best to copy existing rule and edit values on that. 16 | 17 | **Note! Json rule name is case-sensitive so make sure you have exactly same value in json that script returns!** 18 | 19 | You can validate json syntax with command (red means json is not valid) 20 | ``` 21 | Get-Content -Path .\Intune_Linux_Custom_Compliance_script_Rules_file.json | ConvertFrom-Json 22 | ``` 23 | ## Custom Compliance checks in this version: 24 | * **Powershell is installed** 25 | * **Powershell version** 26 | * 7.3.3 minimum currently configured in json 27 | * **Reboot Required check** 28 | * file should not exist /var/run/reboot-required) 29 | * **MS Edge** 30 | * Check MS Edge is installed 31 | * file should exist /opt/microsoft/msedge/msedge 32 | * Check MS Edge version 33 | * **Kernel version** 34 | * Check Kernel version 35 | * 5.19.35-generic minimum currently configured in json 36 | * Check Kernel patch level 37 | * Check Kernel flavour 38 | * Check Kernel tainted state 39 | * **SecureBoot status** 40 | * require SecureBoot configured in json 41 | * **sysctrl values** (placeholder to check any value) 42 | * user.max_user_namespaces 43 | * **gsettings values** (placeholder to check any value) 44 | * org.gnome.desktop.screensaver lock-enabled 45 | * org.gnome.desktop.screensaver idle-activation-enabled 46 | * org.gnome.desktop.session idle-delay 47 | * 10 minutes currently configured in json 48 | * **Defender for Endpoint on Linux status** 49 | * MicrosoftDefenderForEndpointOnLinux_Installed 50 | * MicrosoftDefenderForEndpointOnLinux_RegisteredToOrganization 51 | * MicrosoftDefenderForEndpointOnLinux_Healthy 52 | * MicrosoftDefenderForEndpointOnLinux_DefinitionsStatus_up_to_date 53 | * MicrosoftDefenderForEndpointOnLinux_real_time_protection_enabled 54 | ## How script works 55 | Linux Compliance script itself is Bash script because POSIX-compliant shell script is requirement for Intune. However script includes Powershell script and compliance checks are done in Powershell part. 56 | 57 | So Bash script is just launcher for Powershell script 58 | 59 | Script creates 2 **log files** for debugging and testing 60 | * **/tmp/IntuneCustomComplianceScript_Bash.log** 61 | * **/tmp/IntuneCustomComplianceScript_Powershell.log** 62 | 63 | You can also check syslog when Refreshing settings 64 | ``` 65 | tail -f /var/log/syslog 66 | ``` 67 | 68 | Linux custom compliance script runs in user context who enrolled Ubuntu device to Intune. You can verify this from log files. 69 | 70 | When you upload scripts to Intune they are usually immediately used in client on next Refresh so you can edit code and test really quickly. You can verify this from log files -> change script version number for example. 71 | 72 | Be sure NOT to write anything to STDOUT because that will break the compliance check. Only Powershell part of script is allowed to return compressed JSON to STDOUT which is then passed to Intune for custom Compliance Check. 73 | 74 | ## Requirements 75 | * **Powershell must be installed for this script to work** 76 | * Script will check existence of Powershell (/opt/microsoft/powershell/7/pwsh) 77 | * Check these [Microsoft instructions](https://learn.microsoft.com/en-us/powershell/scripting/install/install-ubuntu?view=powershell-7.2) how to install Powershell to Ubuntu 78 | * You can create Dynamic Azure AD Group for targeting the Linux Compliance Policy using this rule 79 | * **(device.deviceOSType -eq "Linux")** 80 | 81 | ## Limits 82 | * Check current limits from [Microsoft documentation](https://learn.microsoft.com/en-us/mem/intune/protect/compliance-custom-script#limits) 83 | * Scripts can be no larger than 1 megabyte (MB) each. 84 | * Output generated by each script can be no larger than 1 MB. 85 | * Scripts must have a limited run time: 86 | * **On Linux, scripts must take five minutes or less to run.** 87 | * Note! You can find script total runtime from end of log file /tmp/IntuneCustomComplianceScript_Powershell.log 88 | * On Windows, scripts must take 10 minutes or less to run. 89 | * Own personal note: script runs in user context who enrolled device to Intune. This might limit being creative and thinking outside the box :) 90 | 91 | ## Add script to Microsoft Intune 92 | In [Microsoft Intune](https://intune.microsoft.com) add Linux Custom Compliance script 93 | **Devices -> Compliance policies -> Script -> Add** 94 | 95 | 96 | Then create Linux Compliance Policy and select Custom Compliance from Settings picker 97 | **Devices -> Linux -> Compliance policies -> Create policy** 98 | 99 | 100 | 101 | 102 | ## Screenshots 103 | ![Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant.png](https://github.com/petripaavola/Intune/blob/master/Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant.png) 104 | ![Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant.png](https://github.com/petripaavola/Intune/blob/master/Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant.png) 105 | 106 | **Intune Linux Compliance Policy** 107 | 108 | ![Intune_Linux_Compliance_Policy.png](https://github.com/petripaavola/Intune/blob/master/Linux/pics/Intune_Linux_Compliance_Policy.png) 109 | -------------------------------------------------------------------------------- /Linux/pics/IntuneAddCustomCompliancePolicySelectFiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/IntuneAddCustomCompliancePolicySelectFiles.png -------------------------------------------------------------------------------- /Linux/pics/IntuneAddCustomCompliancePolicySettingsPicker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/IntuneAddCustomCompliancePolicySettingsPicker.png -------------------------------------------------------------------------------- /Linux/pics/IntuneAddLinuxCustomComplianceScript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/IntuneAddLinuxCustomComplianceScript.png -------------------------------------------------------------------------------- /Linux/pics/Intune_Linux_Compliance_Policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/Intune_Linux_Compliance_Policy.png -------------------------------------------------------------------------------- /Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant.png -------------------------------------------------------------------------------- /Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_Compliant_small.png -------------------------------------------------------------------------------- /Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant.png -------------------------------------------------------------------------------- /Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Linux/pics/Intune_Linux_CustomComplianceCheck_RebootRequired_NotCompliant_small.png -------------------------------------------------------------------------------- /Microsoft_Graph_API/AppRegistrations_Secret_Expiry_Report_v1.ps1: -------------------------------------------------------------------------------- 1 | # App registration secret expiration report console version 2 | # NVS_2022_Application 3 | # 17.3.2022 4 | # 5 | # Petri.Paavola@yodamiitti.fi 6 | # Microsoft MVP - Windows and Devices for IT 7 | # https://www.github.com/petripaavola 8 | 9 | ############################################################# 10 | #region App information 11 | $AppId = 'f779d5d4-170b-FIXME' 12 | $TenantId = 'bef185b9-FIXME' 13 | $Scope = 'https://graph.microsoft.com/.default' 14 | # $AppSecret = 'REMOVED' 15 | 16 | # Export AppSecret to local disk with Export-CliXML 17 | # This encrypts secret and works only on that specific computer 18 | # $Cred = Get-Credential 19 | # $Cred 20 | # $Cred | Export-CliXml -Path .\AppSecret.xml 21 | 22 | # Import App Registration Secret from XML file 23 | Try { 24 | $Cred = Import-Clixml -Path "$PSScriptRoot\AppSecret.xml" 25 | $Success = $? 26 | if(-not $Success) { 27 | Write-Error "Error importing Application Secret!" 28 | Exit 1 29 | } 30 | 31 | # Get App secret in clear text 32 | $AppSecret = (New-Object PSCredential "user",$Cred.Password).GetNetworkCredential().Password 33 | #Write-Host "`$AppSecret=$AppSecret" 34 | 35 | } Catch { 36 | Write-Error "$($_.Exception.Message)" 37 | Write-Error "Error importing Application Secret!" 38 | Exit 2 39 | } 40 | #endregion 41 | 42 | ############################################################# 43 | #region Do Azure AD Application based Authentication to Graph API 44 | 45 | $Url = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" 46 | 47 | # Add System.Web for urlencode 48 | Add-Type -AssemblyName System.Web 49 | 50 | # Create body 51 | $Body = @{ 52 | client_id = $AppId 53 | client_secret = $AppSecret 54 | scope = $Scope 55 | grant_type = 'client_credentials' 56 | } 57 | 58 | # Splat the parameters for Invoke-RestMethod for cleaner code 59 | $PostSplat = @{ 60 | ContentType = 'application/x-www-form-urlencoded' 61 | Method = 'POST' 62 | # Create string by joining bodylist with '&' 63 | Body = $Body 64 | Uri = $Url 65 | } 66 | 67 | Write-Host "Authenticate to Graph API using Azure AD Application based authentication" 68 | 69 | # Request the token! 70 | $Request = Invoke-RestMethod @PostSplat 71 | $Success = $? 72 | 73 | if($Success) { 74 | Write-Host "Success`n" -ForegroundColor 'Green' 75 | } else { 76 | Write-Host "Failed to authenticate using Azure AD Application secret" -ForegroundColor 'Red' 77 | Exit 3 78 | } 79 | 80 | 81 | # Create header 82 | $Header = @{ 83 | Authorization = "$($Request.token_type) $($Request.access_token)" 84 | } 85 | 86 | #endregion 87 | 88 | #region Get date 89 | Write-Host "Get App Registrations" 90 | $url = "https://graph.microsoft.com/beta/myorganization/applications?$select=displayName,id,appId,info,createdDateTime,keyCredentials,passwordCredentials,deletedDateTime" 91 | $Request = Invoke-RestMethod -Uri $url -Headers $Header -Method Get -ContentType "application/json" 92 | $Success = $? 93 | 94 | # Check if Invoke-RestMethod returned True (which is success) 95 | if(-not $Success) { 96 | Write-Error "There was error getting Graph API data with url $url" 97 | Exit 0 98 | } 99 | 100 | # DEBUG 101 | #$Request 102 | #$Request | Format-List * 103 | #Exit 0 104 | 105 | # Note we are not handling possible Graph API .NextLink in this example !!! 106 | $AppRegistrations = $Request.Value 107 | 108 | #endregion 109 | 110 | #region Process date 111 | 112 | # DEBUG from script to clipboard 113 | # What data we have now compared to Graph Explorer ???????? 114 | #$AppRegistrations | ConvertTo-Json -Depth 5 | Set-Clipboard 115 | 116 | Foreach($AppRegistration in $AppRegistrations) { 117 | $displayName = $AppRegistration.displayName 118 | $passwordCredentials = $AppRegistration.passwordCredentials 119 | 120 | if($passwordCredentials) { 121 | Write-Host "App Registration $displayName secrets expires" -ForegroundColor Cyan 122 | foreach($passwordCredential in $passwordCredentials) { 123 | $secretDisplayName = $passwordCredential.displayName 124 | $endDateTime = $passwordCredential.endDateTime 125 | 126 | $TimeSpan = New-TimeSpan (Get-Date) (Get-Date $endDateTime) 127 | $ExpiresInDays = $TimeSpan | Select-Object -ExpandProperty Days 128 | 129 | if($ExpiresInDays -gt 0) { 130 | # Secret is not expired yet 131 | Write-Host "$secretDisplayName password expires in $ExpiresInDays days" -ForegroundColor Green 132 | } else { 133 | # Secret has expired 134 | # Do something 135 | Write-Host "$secretDisplayName password expires in $ExpiresInDays days" -ForegroundColor Red 136 | } 137 | } 138 | Write-Host 139 | } 140 | } 141 | 142 | #endregion 143 | 144 | 145 | # DEBUG DATA 146 | #return $AppRegistrations 147 | -------------------------------------------------------------------------------- /Microsoft_Graph_API/AppRegistrations_Secret_Expiry_Report_v2.ps1: -------------------------------------------------------------------------------- 1 | # App registration secret expiration report console and HTML 2 | # NVS_2022_Application 3 | # 17.3.2022 4 | # 5 | # Petri.Paavola@yodamiitti.fi 6 | # Microsoft MVP - Windows and Devices for IT 7 | # https://www.github.com/petripaavola 8 | 9 | ############################################################# 10 | #region App information 11 | $AppId = 'f779d5d4-170b-FIXME' 12 | $TenantId = 'bef185b9-FIXME' 13 | $Scope = 'https://graph.microsoft.com/.default' 14 | # $AppSecret = 'REMOVED' 15 | 16 | # Export secret to local disk with Export-CliXML 17 | # This encrypts secret and works only on that specific computer 18 | # $Cred = Get-Credential 19 | # $Cred 20 | # $Cred | Export-CliXml -Path .\AppSecret.xml 21 | 22 | # Import App Registration Secret from XML file 23 | Try { 24 | $Cred = Import-Clixml -Path "$PSScriptRoot\AppSecret.xml" 25 | $Success = $? 26 | if(-not $Success) { 27 | Write-Error "Error importing Application Secret!" 28 | Exit 1 29 | } 30 | 31 | # Get App secret in clear text 32 | $AppSecret = (New-Object PSCredential "user",$Cred.Password).GetNetworkCredential().Password 33 | #Write-Host "`$AppSecret=$AppSecret" 34 | 35 | } Catch { 36 | Write-Error "$($_.Exception.Message)" 37 | Write-Error "Error importing Application Secret!" 38 | Exit 2 39 | } 40 | #endregion 41 | 42 | ############################################################# 43 | #region Do Azure AD Application based Authentication to Graph API 44 | 45 | $Url = "https://login.microsoftonline.com/$TenantID/oauth2/v2.0/token" 46 | 47 | # Add System.Web for urlencode 48 | Add-Type -AssemblyName System.Web 49 | 50 | # Create body 51 | $Body = @{ 52 | client_id = $AppId 53 | client_secret = $AppSecret 54 | scope = $Scope 55 | grant_type = 'client_credentials' 56 | } 57 | 58 | # Splat the parameters for Invoke-RestMethod for cleaner code 59 | $PostSplat = @{ 60 | ContentType = 'application/x-www-form-urlencoded' 61 | Method = 'POST' 62 | # Create string by joining bodylist with '&' 63 | Body = $Body 64 | Uri = $Url 65 | } 66 | 67 | Write-Host "Authenticate to Graph API using Azure AD Application based authentication" 68 | 69 | # Request the token! 70 | $Request = Invoke-RestMethod @PostSplat 71 | $Success = $? 72 | 73 | if($Success) { 74 | Write-Host "Success`n" -ForegroundColor 'Green' 75 | } else { 76 | Write-Host "Failed to authenticate using Azure AD Application secret" -ForegroundColor 'Red' 77 | Exit 3 78 | } 79 | 80 | 81 | # Create header 82 | $Header = @{ 83 | Authorization = "$($Request.token_type) $($Request.access_token)" 84 | } 85 | 86 | #endregion 87 | 88 | #region Get date 89 | Write-Host "Get App Registrations" 90 | $url = "https://graph.microsoft.com/beta/myorganization/applications?$select=displayName,id,appId,info,createdDateTime,keyCredentials,passwordCredentials,deletedDateTime" 91 | $Request = Invoke-RestMethod -Uri $url -Headers $Header -Method Get -ContentType "application/json" 92 | $Success = $? 93 | 94 | # Check if Invoke-RestMethod returned True (which is success) 95 | if(-not $Success) { 96 | Write-Error "There was error getting Graph API data with url $url" 97 | Exit 0 98 | } 99 | 100 | # DEBUG 101 | #$Request 102 | #$Request | Format-List * 103 | 104 | # Note we are not handling possible Graph API .NextLink in this example !!! 105 | $AppRegistrations = $Request.Value 106 | 107 | #endregion 108 | 109 | #region Process date 110 | 111 | # DEBUG from script to clipboard 112 | # What data we have now compared to Graph Explorer ???????? 113 | #$AppRegistrations | ConvertTo-Json -Depth 5 | Set-Clipboard 114 | 115 | # Create array for report 116 | $AppRegistrationSecretReportObjects = @() 117 | 118 | Foreach($AppRegistration in $AppRegistrations) { 119 | $displayName = $AppRegistration.displayName 120 | $passwordCredentials = $AppRegistration.passwordCredentials 121 | 122 | if($passwordCredentials) { 123 | $secretExpiresInformationString = $null 124 | Write-Host "App Registration $displayName secrets expires" -ForegroundColor Cyan 125 | foreach($passwordCredential in $passwordCredentials) { 126 | $secretDisplayName = $passwordCredential.displayName 127 | $endDateTime = $passwordCredential.endDateTime 128 | 129 | $TimeSpan = New-TimeSpan (Get-Date) (Get-Date $endDateTime) 130 | $ExpiresInDays = $TimeSpan | Select-Object -ExpandProperty Days 131 | 132 | if($ExpiresInDays -gt 0) { 133 | # Secret is not expired yet 134 | Write-Host "$secretDisplayName password expires in $ExpiresInDays days" -ForegroundColor Green 135 | $secretExpiresInformationString = $secretExpiresInformationString + "$secretDisplayName password expires in $ExpiresInDays days
" 136 | } else { 137 | # Secret has expired 138 | # Do something 139 | Write-Host "$secretDisplayName password expires in $ExpiresInDays days" -ForegroundColor Red 140 | $secretExpiresInformationString = $secretExpiresInformationString + "$secretDisplayName password expires in$ExpiresInDays days
" 141 | } 142 | } 143 | Write-Host 144 | 145 | # Create custom object 146 | $PowershellCustomObject = New-Object -TypeName psobject 147 | $PowershellCustomObject | Add-Member –Membertype NoteProperty –Name AppRegistrationDisplayName –Value $displayName 148 | $PowershellCustomObject | Add-Member –Membertype NoteProperty –Name SecretExpires –Value $secretExpiresInformationString 149 | 150 | # Add Custom object to array 151 | $AppRegistrationSecretReportObjects += $PowershellCustomObject 152 | } 153 | } 154 | 155 | #endregion 156 | 157 | #region Create HTML report 158 | 159 | # Create HTML Report 160 | 161 | $ReportHTML = $AppRegistrationSecretReportObjects | Select-Object -Property AppRegistrationDisplayName,SecretExpires | ConvertTo-Html -Fragment -PreContent "

Secrets expires

" | Out-String 162 | 163 | # Fix special characters 164 | $ReportHTML = $ReportHTML.Replace('<', '<') 165 | $ReportHTML = $ReportHTML.Replace('>', '>') 166 | $ReportHTML = $ReportHTML.Replace('"', '"') 167 | 168 | $head = @' 169 | 190 | '@ 191 | 192 | ConvertTo-HTML -head $head -PostContent $ReportHTML -PreContent "

Application registrations

for tenant $TenantId$secretDisplayName password expires in $ExpiresInDays days
" 97 | } else { 98 | # Secret has expired 99 | # Do something 100 | Write-Host "$secretDisplayName password expires in $ExpiresInDays days" -ForegroundColor Red 101 | $secretExpiresInformationString = $secretExpiresInformationString + "$secretDisplayName password expires in$ExpiresInDays days
" 102 | } 103 | } 104 | Write-Host 105 | 106 | # Create custom object 107 | $PowershellCustomObject = New-Object -TypeName psobject 108 | $PowershellCustomObject | Add-Member –Membertype NoteProperty –Name AppRegistrationDisplayName –Value $displayName 109 | $PowershellCustomObject | Add-Member –Membertype NoteProperty –Name SecretExpires –Value $secretExpiresInformationString 110 | 111 | # Add Custom object to array 112 | $AppRegistrationSecretReportObjects += $PowershellCustomObject 113 | } 114 | } 115 | 116 | #endregion 117 | 118 | #region Create HTML report 119 | 120 | # Create HTML Report 121 | 122 | $ReportHTML = $AppRegistrationSecretReportObjects | Select-Object -Property AppRegistrationDisplayName,SecretExpires | ConvertTo-Html -Fragment -PreContent "

Secrets expires

" | Out-String 123 | 124 | # Fix special characters 125 | $ReportHTML = $ReportHTML.Replace('<', '<') 126 | $ReportHTML = $ReportHTML.Replace('>', '>') 127 | $ReportHTML = $ReportHTML.Replace('"', '"') 128 | 129 | $head = @' 130 | 151 | '@ 152 | 153 | ConvertTo-HTML -head $head -PostContent $ReportHTML -PreContent "

Application registrations

for tenant $OrganizationName
TenantId: $TenantId$secretDisplayName password expires in $ExpiresInDays days
" 116 | } else { 117 | # Secret has expired 118 | # Do something 119 | Write-Host "$secretDisplayName password expires in $ExpiresInDays days" -ForegroundColor Red 120 | $secretExpiresInformationString = $secretExpiresInformationString + "$secretDisplayName password expires in$ExpiresInDays days
" 121 | } 122 | } 123 | Write-Host 124 | 125 | # Create custom object 126 | $PowershellCustomObject = New-Object -TypeName psobject 127 | $PowershellCustomObject | Add-Member –Membertype NoteProperty –Name AppRegistrationDisplayName –Value $displayName 128 | $PowershellCustomObject | Add-Member –Membertype NoteProperty –Name SecretExpires –Value $secretExpiresInformationString 129 | 130 | # Add Custom object to array 131 | $AppRegistrationSecretReportObjects += $PowershellCustomObject 132 | } 133 | } 134 | 135 | #endregion 136 | 137 | #region Create HTML report 138 | 139 | # Create HTML Report 140 | 141 | $ReportHTML = $AppRegistrationSecretReportObjects | Select-Object -Property AppRegistrationDisplayName,SecretExpires | ConvertTo-Html -Fragment -PreContent "

Secrets expires

" | Out-String 142 | 143 | # Fix special characters 144 | $ReportHTML = $ReportHTML.Replace('<', '<') 145 | $ReportHTML = $ReportHTML.Replace('>', '>') 146 | $ReportHTML = $ReportHTML.Replace('"', '"') 147 | 148 | $head = @' 149 | 170 | '@ 171 | 172 | ConvertTo-HTML -head $head -PostContent $ReportHTML -PreContent "

Application registrations

for tenant $OrganizationName
TenantId: $TenantIdIntune devices

" | Out-String 101 | 102 | # Fix special characters 103 | $ReportHTML = $ReportHTML.Replace('<', '<') 104 | $ReportHTML = $ReportHTML.Replace('>', '>') 105 | $ReportHTML = $ReportHTML.Replace('"', '"') 106 | 107 | $head = @' 108 | 129 | '@ 130 | 131 | ConvertTo-HTML -head $head -PostContent $ReportHTML -PreContent "

Intune managed devices LastSyncDateTime report

for tenant $OrganizationName
TenantId: $TenantId 76 | 77 | # Get Intune devices using Intune Powershell commands 78 | $ManagedDevices = Get-IntuneManagedDevice -Select Id,DeviceName,OperatingSystem,OSVersion,EnrolledDateTime,LastSyncDateTime,UserPrincipalName 79 | 80 | # Get all results 81 | $ManagedDevices = Get-MSGraphAllPages -SearchResult $ManagedDevices 82 | 83 | 84 | # DEBUG 85 | #Write-Verbose $ManagedDevices 86 | #$ManagedDevices 87 | #$ManagedDevices | Format-List * 88 | #$ManagedDevices.Count 89 | #$ManagedDevices | ConvertTo-Json -Depth 6 | Set-Clipboard 90 | #Exit 0 91 | 92 | #endregion 93 | 94 | #region Process by LastSyncDateTime 95 | 96 | # Sort devices by lastSyncDateTime (oldest first) 97 | $ManagedDevices = $ManagedDevices | Sort-Object -Property LastSyncDateTime 98 | 99 | # Debug sorting 100 | #$ManagedDevices | Select-Object -Property DeviceName,LastSyncDateTime 101 | #exit 0 102 | 103 | Foreach($ManagedDevice in $ManagedDevices) { 104 | $DeviceName = $ManagedDevice.DeviceName 105 | $LastSyncDateTime = $ManagedDevice.LastSyncDateTime 106 | 107 | # Calculate how many days ago since lastSync 108 | $TimeSpan = New-TimeSpan (Get-Date) (Get-Date $LastSyncDateTime) 109 | $LastSyncInDays = $TimeSpan | Select-Object -ExpandProperty Days 110 | 111 | # Add $LastSyncInDays property to existing device object 112 | $ManagedDevice | Add-Member –Membertype NoteProperty –Name LastSyncDateTimeInDays –Value $LastSyncInDays 113 | 114 | if($LastSyncInDays -lt -180) { 115 | # Last Sync over 180 days ago 116 | Write-Host "Device $DeviceName last sync is $LastSyncInDays ago. Device will be retired from Intune" -ForegroundColor Red 117 | 118 | # Retire device 119 | # run retire action 120 | 121 | } else { 122 | # Last Sync less than 180 days ago 123 | Write-Host "Device $DeviceName last sync is $LastSyncInDays ago." -ForegroundColor Green 124 | } 125 | } 126 | 127 | #endregion 128 | 129 | #region Create HTML report 130 | 131 | # Create HTML Report 132 | 133 | $ReportHTML = $ManagedDevices | Select-Object -Property DeviceName,LastSyncDateTime | ConvertTo-Html -Fragment -PreContent "

Intune devices

" | Out-String 134 | 135 | # Fix special characters 136 | $ReportHTML = $ReportHTML.Replace('<', '<') 137 | $ReportHTML = $ReportHTML.Replace('>', '>') 138 | $ReportHTML = $ReportHTML.Replace('"', '"') 139 | 140 | $head = @' 141 | 162 | '@ 163 | 164 | ConvertTo-HTML -head $head -PostContent $ReportHTML -PreContent "

Intune managed devices LastSyncDateTime report

for tenant $OrganizationName
TenantId: $TenantId 19 | 20 | 21 | #defined initial data 22 | $LicenseStatusText = @("Unlicensed","Activated","OOB Grace","OOT Grace","Non-Genuine Grace","Notification","Extended Grace") 23 | 24 | $LicenseStatus = Get-CimInstance -ClassName SoftwareLicensingProduct | Where-Object { $_.PartialProductKey -and $_.Name -like "*Windows*"} | Select-Object -ExpandProperty licenseStatus 25 | 26 | if($LicenseStatus -eq 1) { 27 | Write-Host "Activated" 28 | Exit 0 29 | } else { 30 | Write-Host "$($LicenseStatusText[$LicenseStatus])" 31 | Exit 1 32 | } -------------------------------------------------------------------------------- /Remediations/List_WLAN_profiles_Detection.ps1: -------------------------------------------------------------------------------- 1 | # Get WLAN profiles installed to device 2 | # 3 | # Petri.Paavola@yodamiitti.fi 4 | # Microsoft MVP - Windows and Intune 5 | # 6.2.2025 6 | 7 | 8 | # Run netsh command to get WLAN profiles 9 | $netshOutput = netsh wlan show profiles 10 | 11 | # Extract profile names from netsh output 12 | $profileNames = $netshOutput | Select-String -Pattern ':\s+(.+)$' | ForEach-Object { $_.Matches.Groups[1].Value.Trim() } 13 | 14 | # Create custom objects 15 | $profileObjects = $profileNames | ForEach-Object { 16 | [PSCustomObject]@{ 17 | ProfileName = $_ 18 | } 19 | } 20 | 21 | # Convert to compressed JSON 22 | $json = $profileObjects | ConvertTo-Json -Compress 23 | 24 | # Output the JSON 25 | $json 26 | 27 | exit 0 -------------------------------------------------------------------------------- /Remediations/Set-TimeZone Win32App Remediation/01-Requirements-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1: -------------------------------------------------------------------------------- 1 | # Intune Win32App custom Requirement script will check if device system locale is fi-FI 2 | # so script/Win32App/remediation is only run for fi-FI devices 3 | # 4 | # 5 | # Petri.Paavola@yodamiitti.fi 6 | # Microsoft MVP - Windows and Intune 7 | # 2025-02-06 8 | 9 | 10 | # Get the system locale (this returns an object with a Name property, e.g. "fi-FI") 11 | $systemLocale = Get-WinSystemLocale 12 | 13 | if ($systemLocale.Name -eq "fi-FI") { 14 | # Requirements are met 15 | # we can continue installing Win32 application ("Remediation" in this case) 16 | 17 | #Write-Host "Finnish language detected. Application is applicable and we can continue to Detection check" 18 | 19 | # Return 20 | $true 21 | } else { 22 | # Requirements are NOT met 23 | 24 | #Write-Host "Non-Finnish language detected. Application is not applicable." 25 | 26 | # Return 27 | $false 28 | } -------------------------------------------------------------------------------- /Remediations/Set-TimeZone Win32App Remediation/02-Detection-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1: -------------------------------------------------------------------------------- 1 | # Intune Win32App custom Detection script will check if device TimeZone is 'FLE Standard Time' 2 | # 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Intune 6 | # 2025-02-06 7 | 8 | # Get current TimeZone 9 | $currentTimeZone = (Get-TimeZone).Id 10 | 11 | if ($currentTimeZone -eq "FLE Standard Time") { 12 | Write-Host "TimeZone is correctly set to 'FLE Standard Time'." 13 | exit 0 14 | } else { 15 | # Write-Host "TimeZone is NOT set to 'FLE Standard Time'. We need to 'Remediate'" 16 | exit 1 17 | } 18 | -------------------------------------------------------------------------------- /Remediations/Set-TimeZone Win32App Remediation/Set-TimeZone-to-FLEStandardTime-Win32-Remediation-Install-script.intunewin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Remediations/Set-TimeZone Win32App Remediation/Set-TimeZone-to-FLEStandardTime-Win32-Remediation-Install-script.intunewin -------------------------------------------------------------------------------- /Remediations/Set-TimeZone Win32App Remediation/Win32 App notes.txt: -------------------------------------------------------------------------------- 1 | Name: Set TimeZone to 'FLE Standard Time' only on fi-FI devices - Win32 App Remediation 2 | Description: Set TimeZone to 'FLE Standard Time' only on fi-FI devices - Win32 App Remediation 3 | Publisher: Yodamiitti Oy 4 | Version: 1.0 5 | 6 | Install command: Powershell.exe -Executionpolicy bypass -file Set-TimeZone-to-FLEStandardTime-Win32-Remediation-Install-script.ps1 7 | Uninstall command: cmd /c exit 0 8 | 9 | 10 | Requirements: 11 | 12 | - Requirement type: Script 13 | - Script Name: 01-Requirements-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1 14 | - Script file: 01-Requirements-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1 15 | - Run script as 32-bit process on 64-bit clients: No 16 | - Run this script using the logged on credentials: No 17 | - Enforce script signature check: No 18 | - Select output data type: Boolean 19 | - Operator: Equals 20 | - Value: Yes 21 | 22 | 23 | Detection check: 24 | - Use custom detection script 25 | - 02-Detection-script-Set-TimeZone-to-FLEStandardTime-Win32-Remediation.ps1 26 | - Run script as 32-bit process on 64-bit clients: No 27 | - Enforce script signature check and run script silently: No 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Remediations/Set-TimeZone Win32App Remediation/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Remediations/Set-TimeZone Win32App Remediation/logo.png -------------------------------------------------------------------------------- /Remediations/Set-TimeZone Win32App Remediation/source/Set-TimeZone-to-FLEStandardTime-Win32-Remediation-Install-script.ps1: -------------------------------------------------------------------------------- 1 | # Intune Win32App custom "Remediation" script to set device TimeZone to 'FLE Standard Time' 2 | # 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Intune 6 | # 2025-02-06 7 | 8 | 9 | # Set TimeZone to FLE Standard Time 10 | Set-Timezone -Id 'FLE Standard Time' 11 | $Success = $? 12 | 13 | if($Success) { 14 | # Set Timezone succeeded 15 | 16 | Write-Host "OK: TimeZone set to FLE Standard Time" 17 | exit 0 18 | } else { 19 | # Set TimeZone failed 20 | Write-Host "ERROR: Failed to set TimeZone to FLE Standard Time" 21 | Exit 1 22 | } 23 | -------------------------------------------------------------------------------- /Remediations/Set-TimeZone-to-FLEStandardTime-Detection.ps1: -------------------------------------------------------------------------------- 1 | # Change TimeZone to FLE Standard Time 2 | # Intune Remediation script 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Intune 6 | # 2025-02-06 7 | 8 | # Set TimeZone to FLE Standard Time 9 | Set-Timezone -Id 'FLE Standard Time' 10 | $Success = $? 11 | 12 | if($Success) { 13 | # Set Timezone succeeded 14 | 15 | Write-Host "OK: TimeZone set to FLE Standard Time" 16 | exit 0 17 | } else { 18 | # Set TimeZone failed 19 | Write-Host "ERROR: Setting TimeZone to FLE Standard Time" 20 | Exit 1 21 | } 22 | -------------------------------------------------------------------------------- /Remediations/Set-TimeZone-to-GreenwichStandardTime-Detection.ps1: -------------------------------------------------------------------------------- 1 | # Change TimeZone to Greenwich Standard Time 2 | # Intune Remediation script 3 | # 4 | # Petri.Paavola@yodamiitti.fi 5 | # Microsoft MVP - Windows and Intune 6 | # 2025-02-06 7 | 8 | # Set TimeZone to Greenwich Standard Time 9 | Set-Timezone -Id 'Greenwich Standard Time' 10 | $Success = $? 11 | 12 | if($Success) { 13 | # Set Timezone succeeded 14 | 15 | Write-Host "OK: TimeZone set to Greenwich Standard Time" 16 | exit 0 17 | } else { 18 | # Set TimeZone failed 19 | Write-Host "ERROR: Setting TimeZone to Greenwich Standard Time" 20 | Exit 1 21 | } 22 | -------------------------------------------------------------------------------- /Remediations/WindowsUpdateRegistryValuesCheck-Detection.ps1: -------------------------------------------------------------------------------- 1 | # Intune Remediation Detection script to check Windows Update registry values 2 | # This script checks the following registry values: 3 | # - HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\DoNotConnectToWindowsUpdateInternetLocations 4 | # - HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU\UseWUServer 5 | # The expected values are: 6 | # - DoNotConnectToWindowsUpdateInternetLocations: 0 7 | # - UseWUServer: 1 8 | # If any of the registry values have an unexpected value, the script will return a non-compliant status. 9 | # It is ok to not have those registry values at all 10 | # 11 | # Petri.Paavola@yodamiitti.fi 12 | # Microsoft MVP - Windows and Intune 13 | # 2025-02-06 14 | 15 | 16 | # Define compliance flag 17 | $compliant = $true 18 | 19 | # Define return string 20 | $returnString = '' 21 | 22 | # Define Windows Update registry paths and expected values 23 | $wuRegPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" 24 | $key1Name = "DoNotConnectToWindowsUpdateInternetLocations" 25 | $expectedValue1 = 0 26 | 27 | $auRegPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" 28 | $key2Name = "UseWUServer" 29 | $expectedValue2 = 1 30 | 31 | # Check the first registry key 32 | if (Test-Path $wuRegPath) { 33 | 34 | # First we need to determine if value DoNotConnectToWindowsUpdateInternetLocations exists 35 | $key1 = Get-ItemProperty -Path $wuRegPath -Name $key1Name -ErrorAction SilentlyContinue 36 | if($key1) { 37 | # Value DoNotConnectToWindowsUpdateInternetLocations exists 38 | # Now we need to check if the value is correct 39 | if ($key1.$key1Name -ne $expectedValue1) { 40 | $returnString = "Non-compliant: '$wuRegPath\$key1Name' has value $($key1.$key1Name) (expected $expectedValue1). " 41 | $compliant = $false 42 | } 43 | } 44 | } 45 | 46 | # Check the second registry key 47 | if (Test-Path $auRegPath) { 48 | 49 | # First we need to determine if value UseWUServer exists 50 | $key2 = Get-ItemProperty -Path $auRegPath -Name $key2Name -ErrorAction SilentlyContinue 51 | if($key2) { 52 | # Value UseWUServer exists 53 | # Now we need to check if the value is correct 54 | if ($key2.$key2Name -ne $expectedValue2) { 55 | $returnString = "$returnString Non-compliant: '$auRegPath\$key2Name' has value $($key2.$key2Name) (expected $expectedValue2)." 56 | $compliant = $false 57 | } 58 | } 59 | } 60 | 61 | 62 | 63 | # Output compliance status and exit appropriately 64 | if ($compliant) { 65 | Write-Output "WindowsUpdate registry values are compliant." 66 | exit 0 67 | } else { 68 | Write-Host "$returnString" 69 | exit 1 70 | } -------------------------------------------------------------------------------- /Remediations/WindowsUpdateRegistryValuesCheck-Remediate.ps1: -------------------------------------------------------------------------------- 1 | # Intune Remediation Remediate script to change Windows Update registry values 2 | # This script changes the following registry values: 3 | # - HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\DoNotConnectToWindowsUpdateInternetLocations 4 | # - HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU\UseWUServer 5 | # The expected values to be set are: 6 | # - DoNotConnectToWindowsUpdateInternetLocations: 0 7 | # - UseWUServer: 1 8 | # 9 | # Petri.Paavola@yodamiitti.fi 10 | # Microsoft MVP - Windows and Intune 11 | # 2025-02-06 12 | 13 | 14 | # Define compliance flag 15 | $remediationSuccessful = $true 16 | 17 | # Define return string 18 | $returnString = $null 19 | 20 | # Define Windows Update registry paths and expected values 21 | $wuRegPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" 22 | $key1Name = "DoNotConnectToWindowsUpdateInternetLocations" 23 | $expectedValue1 = 0 24 | 25 | $auRegPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" 26 | $key2Name = "UseWUServer" 27 | $expectedValue2 = 1 28 | 29 | # Check the first registry key 30 | if (Test-Path $wuRegPath) { 31 | 32 | # First we need to determine if value DoNotConnectToWindowsUpdateInternetLocations exists 33 | $key1 = Get-ItemProperty -Path $wuRegPath -Name $key1Name -ErrorAction SilentlyContinue 34 | if($key1) { 35 | # Value DoNotConnectToWindowsUpdateInternetLocations exists 36 | # Now we need to check if the value is correct 37 | if ($key1.$key1Name -eq $expectedValue1) { 38 | # Registry key value is correct, no need to change it 39 | $returnString = "Compliant: '$wuRegPath\$key1Name' has value $expectedValue1. " 40 | } else { 41 | # Value is wrong so we need to change it 42 | Set-ItemProperty -Path $wuRegPath -Name $key1Name -Value $expectedValue1 -Force 43 | $Success = $? 44 | if($Success) { 45 | $returnString = "Remediated: '$wuRegPath\$key1Name' value changed to $expectedValue1. " 46 | } else { 47 | $returnString = "Failed to remediate: '$wuRegPath\$key1Name' value change to $expectedValue1. " 48 | $remediationSuccessful = $false 49 | } 50 | } 51 | } 52 | } 53 | 54 | # Check the second registry key 55 | if (Test-Path $auRegPath) { 56 | 57 | # First we need to determine if value UseWUServer exists 58 | $key2 = Get-ItemProperty -Path $auRegPath -Name $key2Name -ErrorAction SilentlyContinue 59 | if($key2) { 60 | # Value UseWUServer exists 61 | # Now we need to check if the value is correct 62 | if ($key2.$key2Name -eq $expectedValue2) { 63 | # Registry key value is correct, no need to change it 64 | $returnString = "$returnString Remediated: '$auRegPath\$key2Name' value changed to $expectedValue2." 65 | } else { 66 | # Value is wrong so we need to change it 67 | Set-ItemProperty -Path $auRegPath -Name $key2Name -Value $expectedValue2 -Force 68 | $Success = $? 69 | if($Success) { 70 | $returnString = "$returnString Remediated: '$auRegPath\$key2Name' value changed to $expectedValue2." 71 | } else { 72 | $returnString = "$returnString Failed to remediate: '$auRegPath\$key2Name' value change to $expectedValue2." 73 | $remediationSuccessful = $false 74 | } 75 | } 76 | } 77 | } 78 | 79 | 80 | 81 | # Output compliance status and exit appropriately 82 | if ($remediationSuccessful) { 83 | if ($returnString) { 84 | # Remediation was successful and was done because we have values in $returnString 85 | Write-Host "$returnString" 86 | } else { 87 | # Variable $returnString is empty so registry values did not exist at all so we are Compliant 88 | Write-Host "WindowsUpdate registry keys do not exist. No remediation needed, we are Compliant" 89 | } 90 | exit 0 91 | } else { 92 | Write-Host "$returnString" 93 | exit 1 94 | } -------------------------------------------------------------------------------- /Remediations/readme.MD: -------------------------------------------------------------------------------- 1 | # Intune Remediation scripts 2 | -------------------------------------------------------------------------------- /Reports/README.md: -------------------------------------------------------------------------------- 1 | # Intune HTML reports - Check updated Intune App Assignment Report 2 | Reports which we don't have in Intune (at least at this time) 3 | 4 | **Quick links to reports** 5 | * **[Intune Application Assignment Report - New version updated](#IntuneAppAssignmentsReport)** 6 | * **Huge update to ver3.0. Check it out! (24.5.2023)** 7 | * **[Intune Configurations Assignment Report](#IntuneConfigurationAssignmentsReport)** 8 | * Updated version to 3.0 work in progress 9 | 10 | 11 | 12 | ## Create-IntuneAppAssignmentsReport.ps1 ver3.0 - Updated 24.5.2023 ### 13 | **Huge update to current 3.0 version! Last Intune Application Assignment Report you need :)** 14 | 15 | This PowerShell script creates an HTML report from all Intune App Assignments. This report shows information not easily available in the Intune UI. 16 | 17 | 18 | 19 | Link to script [Create-IntuneAppAssignmentsReport.ps1](https://github.com/petripaavola/Intune/blob/master/Reports/Create-IntuneAppAssignmentsReport.ps1) 20 | 21 | **Quick start** 22 | ``` 23 | # Download from PowershellGallery 24 | Save-Script Create-IntuneAppAssignmentsReport -Path ./ 25 | 26 | # Run script (create report) 27 | ./Create-IntuneAppAssignmentsReport.ps1 28 | ``` 29 | 30 | ### Features ### 31 | 32 | * This report shows information which is not available in Intune UI without making tens or hundreds of clicks to all Apps and AzureAD Groups 33 | * 2 main views usually used: 34 | * Find AzureAD Groups where single Application is assigned to 35 | * Sort by **App name** column (default) 36 | * **Find all Apps that are assigned to single AzureAD Group** 37 | * **this view does not exist in Intune** 38 | * Sort by **Target Group** column 39 | * See **impact** of Assignment 40 | * Get number of devices and users in Assignment group 41 | * Realtime **filtering and free text search** 42 | * Filter by OS, App Type, Assignment Target Group, Intune Filter Name 43 | * Free text search 44 | * **Sort** by any column 45 | * **Hover** on ApplicationName, TargetGroup and/or FilterName to get more information 46 | * **Web link** to Intune Application, Target AzureAD Group and Intune Filter 47 | * **Export** CSV file, json file and paste to Excel 48 | 49 | ### Report overview ### 50 | ![IntuneApplicationAssignmentReport.png](https://github.com/petripaavola/Intune/blob/master/Reports/pics/IntuneApplicationAssignmentReport.png) 51 | 52 | ### What Apps are assigned to AzureAD Group ### 53 | **Intune does not have this kind of view.** 54 | ![IntuneApplicationAssignmentReportSortByAssignmentGroup.png](https://github.com/petripaavola/Intune/blob/master/Reports/pics/IntuneApplicationAssignmentReportSortByAssignmentGroup.png) 55 | 56 | ### Usage ### 57 | 58 | Make sure you have **Intune Powershell module** installed and updated 59 | You can install Intune Powershell management module to your user account with command 60 | ``` 61 | Install-Module -Name Microsoft.Graph.Intune -Scope CurrentUser 62 | ``` 63 | **Download** script from [PowershellGallery](https://www.powershellgallery.com/packages/Create-IntuneAppAssignmentsReport) with command: 64 | ``` 65 | Save-Script Create-IntuneAppAssignmentsReport -Path ./ 66 | ``` 67 | **Run script** 68 | ``` 69 | ./Create-IntuneAppAssignmentsReport.ps1 70 | ``` 71 | ### Parameters ### 72 | 73 | The script accepts the following parameters: 74 | 75 | | Parameter | Description | 76 | |----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 77 | | `-ExportCSV` | Export report as a CSV file. | 78 | | `-ExportJSON` | Export report as a JSON file. | 79 | | `-ExportToExcelCopyPaste` | Export report to Clipboard for easy pasting into Excel. | 80 | | `-UseOfflineCache` | Create report using files from the cache folder. | 81 | | `-DoNotOpenReportAutomatically` | Prevent automatic opening of the HTML report in a web browser. Useful for automating report creation. | 82 | | `-UpdateIconsCache` | Update the App icon cache. New Apps will always get icons downloaded automatically but existing icons are not automatically updated. | 83 | | `-IncludeAppsWithoutAssignments` | Include Intune Applications without Assignments. This will display a lot of Apps you didn't even know exist inside Intune/Graph API. | 84 | | `-DoNotDownloadAppIcons` | Prevent downloading of application icons. | 85 | | `-IncludeIdsInReport` | Include application IDs in the report. This makes the report wider so it is disabled by default. | 86 | | `-IncludeBase64ImagesInReport` | Includes Application icons inside the HTML file. Note! This is slow and creates a large HTML file. | 87 | ### Examples ### 88 | ``` 89 | .\Create-IntuneAppAssignmentsReport.ps1 90 | .\Create-IntuneAppAssignmentsReport.ps1 -UseOfflineCache 91 | .\Create-IntuneAppAssignmentsReport.ps1 -ExportCSV -ExportJSON 92 | .\Create-IntuneAppAssignmentsReport.ps1 -ExportToExcelCopyPaste 93 | .\Create-IntuneAppAssignmentsReport.ps1 -UpdateIconsCache 94 | .\Create-IntuneAppAssignmentsReport.ps1 -DoNotDownloadAppIcons 95 | .\Create-IntuneAppAssignmentsReport.ps1 -DoNotOpenReportAutomatically 96 | .\Create-IntuneAppAssignmentsReport.ps1 -IncludeAppsWithoutAssignments 97 | ``` 98 | 99 | --- 100 | 101 | ## Create_IntuneConfigurationAssignments_HTML_Report.ps1 102 | [Create_IntuneConfigurationAssignments_HTML_Report.ps1](https://github.com/petripaavola/Intune/blob/master/Reports/Create_IntuneConfigurationAssignments_HTML_Report.ps1) 103 | 104 | **Work is in progress to update this to 3.0 version which has same features than the Application report has** 105 | 106 | 2 views: 107 | * Find where single Configuration Profile is assigned to 108 | * Find all Configuration Profiles that are assigned to single AzureAD Group (this view does not exist in Intune) 109 | 110 | Platform based tables and quick links 111 | 112 | ### Overview 113 | ![01-DeviceConfigurationsReport_Overview.png](https://github.com/petripaavola/Intune/blob/master/Reports/pics/01-DeviceConfigurationsReport_Overview.png) 114 | 115 | ### Where Configuration Profile is assigned to 116 | ![02-DeviceConfigurationsReport_Platform_by_profilename.png](https://github.com/petripaavola/Intune/blob/master/Reports/pics/02-DeviceConfigurationsReport_Platform_by_profilename.png) 117 | 118 | ### What Configuration Profiles are assigned to AzureAD Group 119 | ![03-DeviceConfigurationsReport_Targeted_to_AzureADGroup.png](https://github.com/petripaavola/Intune/blob/master/Reports/pics/03-DeviceConfigurationsReport_Targeted_to_AzureADGroup.png) 120 | -------------------------------------------------------------------------------- /Reports/pics/01-DeviceConfigurationsReport_Overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Reports/pics/01-DeviceConfigurationsReport_Overview.png -------------------------------------------------------------------------------- /Reports/pics/02-DeviceConfigurationsReport_Platform_by_profilename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Reports/pics/02-DeviceConfigurationsReport_Platform_by_profilename.png -------------------------------------------------------------------------------- /Reports/pics/03-DeviceConfigurationsReport_Targeted_to_AzureADGroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Reports/pics/03-DeviceConfigurationsReport_Targeted_to_AzureADGroup.png -------------------------------------------------------------------------------- /Reports/pics/IntuneApplicationAssignmentReport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Reports/pics/IntuneApplicationAssignmentReport.png -------------------------------------------------------------------------------- /Reports/pics/IntuneApplicationAssignmentReportSortByAssignmentGroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Reports/pics/IntuneApplicationAssignmentReportSortByAssignmentGroup.png -------------------------------------------------------------------------------- /Reports/pics/README.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Troubleshooting/README.md: -------------------------------------------------------------------------------- 1 | # Intune troubleshooting tools and tips 2 | 3 | ### Intune Management Extension log analyzer and LogViewerUI ### 4 | Check my [Get-IntuneManagementExtensionDiagnostics](https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics) tool. 5 | 6 | 7 | 8 | ### Firewall #### 9 | Check Firewall rules configured from Intune 10 | ``` 11 | Get-NetFirewallRule -PolicyStore ActiveStore | Where-Object { $_.PolicyStoreSource -eq 'Mdm' } | Select-Object -Property DisplayName,Action,Direction 12 | ``` 13 | Firewall configurations and rules registry path is: 14 | Computer\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\SharedAccess\Parameters\FirewallPolicy\Mdm 15 | 16 | 17 | #### [Show-IntuneIMELogsInOutGridView.ps1](./Show-IntuneIMELogsInOutGridView.ps1) 18 | **You really should look my [Get-IntuneManagementExtensionDiagnostics](https://github.com/petripaavola/Get-IntuneManagementExtensionDiagnostics) tool for Intune LogViewerUI.** 19 | 20 | * Show Intune Management Extension logs in cmtrace style window using Out-GridView cmdlet 21 | * Shows list of IME log files to show in Out-GridView if log file is not specified as command line parameter 22 | 23 | Either give log file as parameter or select file from Out-GridView 24 | ![01-Show-IntuneIMELogsInOutGridView.png](https://github.com/petripaavola/Intune/blob/master/Troubleshooting/pics/01-Show-IntuneIMELogsInOutGridView.png) 25 | 26 | Out-Gridview looks like cmtrace.exe. Filter enables quick search by typing any text 27 | ![02-Show-IntuneIMELogsInOutGridView.png](https://github.com/petripaavola/Intune/blob/master/Troubleshooting/pics/02-Show-IntuneIMELogsInOutGridView.png) 28 | -------------------------------------------------------------------------------- /Troubleshooting/Show-IntuneIMELogsInOutGridView.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Show Intune Management Extension (IME) logs in Out-GridView ver 1.2 4 | This looks a lot like cmtrace.exe tool 5 | 6 | Accepts log file as parameter. 7 | 8 | If log file is not specified then log files can be 9 | selected from graphical Out-GridView 10 | 11 | 12 | Author: 13 | Petri.Paavola@yodamiitti.fi 14 | Modern Management Principal 15 | Microsoft MVP - Windows and Devices for IT 16 | 17 | 2023-02-28 18 | 19 | https://github.com/petripaavola/Intune/tree/master/Troubleshooting 20 | .DESCRIPTION 21 | 22 | .EXAMPLE 23 | .\Show-IntuneIMELogsInOutGridView.ps1 24 | .EXAMPLE 25 | .\Show-IntuneIMELogsInOutGridView.ps1 "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" 26 | .EXAMPLE 27 | .\Show-IntuneIMELogsInOutGridView.ps1 -LogFilePath "C:\ProgramData\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log" 28 | .EXAMPLE 29 | Get-ChildItem C:\programdata\Microsoft\IntuneManagementExtension\Logs\IntuneManagementExtension.log | .\Show-IntuneIMELogsInOutGridView.ps1 30 | 31 | .INPUTS 32 | Intune Management Extension (IME) log file full path 33 | or 34 | Intune Management Extension (IME) log file type object which has property FullName 35 | .OUTPUTS 36 | None 37 | .NOTES 38 | .LINK 39 | https://github.com/petripaavola/Intune/tree/master/Troubleshooting 40 | #> 41 | 42 | [CmdletBinding()] 43 | Param( 44 | [Parameter(Mandatory=$false, 45 | HelpMessage = 'Enter Intune IME log file path', 46 | ValueFromPipeline=$true, 47 | ValueFromPipelineByPropertyName=$true)] 48 | [Alias("FullName")] 49 | [String]$LogFilePath = $null 50 | ) 51 | 52 | 53 | Write-Host "Starting IME Log Out-GridView Tool`n" 54 | 55 | # If LogFilePath is not specified then show log files in Out-GridView 56 | # from folder C:\ProgramData\Microsoft\intunemanagementextension\Logs 57 | if(-not $LogFilePath) { 58 | $LogFiles = Get-ChildItem -Path 'C:\ProgramData\Microsoft\intunemanagementextension\Logs' -Filter *.log 59 | 60 | # Show log files in Out-GridView 61 | $SelectedLogFile = $LogFiles | Out-GridView -Title 'Select log file to show in Out-GridView' -OutputMode Single 62 | 63 | if($SelectedLogFile) { 64 | $LogFilePath = $SelectedLogFile.FullName 65 | } else { 66 | Write-Host "No log file selected. Script will exit!`n" -ForegroundColor Yellow 67 | Exit 0 68 | } 69 | } 70 | 71 | # Initialize variables 72 | $LineNumber=1 73 | $MultilineLogEntryStartsLineNumber=0 74 | 75 | # Create Generic list where log entry custom objects are added 76 | $LogEntryList = [System.Collections.Generic.List[PSObject]]@() 77 | 78 | $Log = Get-Content -Path $LogFilePath 79 | 80 | # This matches for cmtrace type logs 81 | # Test with https://regex101.com 82 | # String: 83 | 84 | # This matches single line full log entry 85 | $SingleLineRegex = '^\<\!\[LOG\[(.*)]LOG\].*\$' 86 | 87 | # Start of multiline log entry 88 | $FirstLineOfMultiLineLogRegex = '^\<\!\[LOG\[(.*)$' 89 | 90 | # End of multiline log entry 91 | $LastLineOfMultiLineLogRegex = '^(.*)\]LOG\]\!>\$' 92 | 93 | 94 | # Process each log file line one by one 95 | Foreach ($CurrentLogEntry in $Log) { 96 | 97 | # Get data from CurrentLogEntry 98 | if($CurrentLogEntry -Match $SingleLineRegex) { 99 | # This matches single line full log entry 100 | 101 | # Regex found match 102 | $LogMessage = $Matches[1].Trim() 103 | 104 | $Hour = $Matches[2] 105 | $Minute = $Matches[3] 106 | $Second = $Matches[4] 107 | 108 | $MilliSecond = $Matches[5] 109 | # Cut milliseconds to 0-999 110 | # Time unit is so small that we don't even bother to round the value 111 | $MilliSecond = $MilliSecond.Substring(0,3) 112 | 113 | $Month = $Matches[6] 114 | $Day = $Matches[7] 115 | $Year = $Matches[8] 116 | 117 | $Component = $Matches[9] 118 | $Context = $Matches[10] 119 | $Type = $Matches[11] 120 | $Thread = $Matches[12] 121 | $File = $Matches[13] 122 | 123 | $Param = @{ 124 | Hour=$Hour 125 | Minute=$Minute 126 | Second=$Second 127 | MilliSecond=$MilliSecond 128 | Year=$Year 129 | Month=$Month 130 | Day=$Day 131 | } 132 | 133 | #$LogEntryDateTime = Get-Date @Param 134 | #Write-Host "DEBUG `$LogEntryDateTime: $LogEntryDateTime" -ForegroundColor Yellow 135 | 136 | # This works for humans but does not sort 137 | #$DateTimeToLogFile = "$($Hour):$($Minute):$($Second).$MilliSecond $Day/$Month/$Year" 138 | 139 | # This does sorting right way 140 | $DateTimeToLogFile = "$Year-$Month-$Day $($Hour):$($Minute):$($Second).$MilliSecond" 141 | 142 | # Create Powershell custom object and add it to list 143 | $LogEntryList.add([PSCustomObject]@{ 144 | 'Line' = $LineNumber; 145 | 'DateTime' = $DateTimeToLogFile; 146 | 'Message' = $LogMessage; 147 | 'Component' = $Component; 148 | 'Context' = $Context; 149 | 'Type' = $Type; 150 | 'Thread' = $Thread; 151 | 'File' = $File 152 | }) 153 | 154 | } elseif ($CurrentLogEntry -Match $FirstLineOfMultiLineLogRegex) { 155 | # Single line regex did not get results so we are dealing multiline case separately here 156 | # Test if this is start of multiline log entry 157 | 158 | #Write-Host "DEBUB Start of multiline regex: $CurrentLogEntry" -ForegroundColor Yellow 159 | 160 | $MultilineLogEntryStartsLineNumber = $LineNumber 161 | 162 | # Regex found match 163 | $LogMessage = $Matches[1].Trim() 164 | 165 | $DateTimeToLogFile = '' 166 | $Component = '' 167 | $Context = '' 168 | $Type = '' 169 | $Thread = '' 170 | $File = '' 171 | 172 | # Create Powershell custom object and add it to list 173 | $LogEntryList.add([PSCustomObject]@{ 174 | 'Line' = $LineNumber; 175 | 'DateTime' = $DateTimeToLogFile; 176 | 'Message' = $LogMessage; 177 | 'Component' = $Component; 178 | 'Context' = $Context; 179 | 'Type' = $Type; 180 | 'Thread' = $Thread; 181 | 'File' = $File 182 | }) 183 | 184 | } elseif ($CurrentLogEntry -Match $LastLineOfMultiLineLogRegex) { 185 | # Single line regex did not get results so we are dealing multiline case separately here 186 | # Test if this is end of multiline log entry 187 | 188 | # Regex found match 189 | $LogMessage = $Matches[1].Trim() 190 | 191 | $Hour = $Matches[2] 192 | $Minute = $Matches[3] 193 | $Second = $Matches[4] 194 | 195 | $MilliSecond = $Matches[5] 196 | # Cut milliseconds to 0-999 197 | # Time unit is so small that we don't even bother to round the value 198 | $MilliSecond = $MilliSecond.Substring(0,3) 199 | 200 | $Month = $Matches[6] 201 | $Day = $Matches[7] 202 | $Year = $Matches[8] 203 | 204 | $Component = $Matches[9] 205 | $Context = $Matches[10] 206 | $Type = $Matches[11] 207 | $Thread = $Matches[12] 208 | $File = $Matches[13] 209 | 210 | $Param = @{ 211 | Hour=$Hour 212 | Minute=$Minute 213 | Second=$Second 214 | MilliSecond=$MilliSecond 215 | Year=$Year 216 | Month=$Month 217 | Day=$Day 218 | } 219 | 220 | #$LogEntryDateTime = Get-Date @Param 221 | #Write-Host "DEBUG `$LogEntryDateTime: $LogEntryDateTime" -ForegroundColor Yellow 222 | 223 | # This works for humans but does not sort 224 | #$DateTimeToLogFile = "$($Hour):$($Minute):$($Second).$MilliSecond $Day/$Month/$Year" 225 | 226 | # This does sorting right way 227 | $DateTimeToLogFile = "$Year-$Month-$Day $($Hour):$($Minute):$($Second).$MilliSecond" 228 | 229 | # Create Powershell custom object and add it to list 230 | $LogEntryList.add([PSCustomObject]@{ 231 | 'Line' = $LineNumber; 232 | 'DateTime' = ''; 233 | 'Message' = $LogMessage; 234 | 'Component' = $Component; 235 | 'Context' = $Context; 236 | 'Type' = $Type; 237 | 'Thread' = $Thread; 238 | 'File' = $File 239 | }) 240 | 241 | # Add DateTime, Component, Context, Type, Thread, File information to object which is starting multiline log entry 242 | $LogEntryList[$MultilineLogEntryStartsLineNumber-1].DateTime = $DateTimeToLogFile 243 | $LogEntryList[$MultilineLogEntryStartsLineNumber-1].Component = $Component 244 | $LogEntryList[$MultilineLogEntryStartsLineNumber-1].Context = $Context 245 | $LogEntryList[$MultilineLogEntryStartsLineNumber-1].Type = $Type 246 | $LogEntryList[$MultilineLogEntryStartsLineNumber-1].Thread = $Thread 247 | $LogEntryList[$MultilineLogEntryStartsLineNumber-1].File = $File 248 | 249 | } else { 250 | # We didn't catch log entry with our regex 251 | # This should be multiline log entry but not first or last line in that log entry 252 | # This can also be some line that should be matched with (other) regex 253 | 254 | #Write-Host "DEBUG: $CurrentLogEntry" -ForegroundColor Yellow 255 | 256 | # Create Powershell custom object and add it to list 257 | $LogEntryList.add([PSCustomObject]@{ 258 | 'Line' = $LineNumber; 259 | 'DateTime' = ''; 260 | 'Message' = $CurrentLogEntry; 261 | 'Component' = ''; 262 | 'Context' = ''; 263 | 'Type' = ''; 264 | 'Thread' = ''; 265 | 'File' = '' 266 | }) 267 | } 268 | 269 | $LineNumber++ 270 | } 271 | 272 | $LogEntryList | Out-GridView -Title "Intune IME Log Viewer $LogFilePath" 273 | 274 | Write-Host "Script end" 275 | -------------------------------------------------------------------------------- /Troubleshooting/pics/01-Show-IntuneIMELogsInOutGridView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Troubleshooting/pics/01-Show-IntuneIMELogsInOutGridView.png -------------------------------------------------------------------------------- /Troubleshooting/pics/02-Show-IntuneIMELogsInOutGridView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/petripaavola/Intune/c8b187f96e7e0462b91625e8b81c756817927dd6/Troubleshooting/pics/02-Show-IntuneIMELogsInOutGridView.png -------------------------------------------------------------------------------- /Troubleshooting/pics/README.md: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------