Skip to content

Commit

Permalink
Volume and Uev
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronparker committed Mar 20, 2024
1 parent e74460d commit 8beaf15
Show file tree
Hide file tree
Showing 23 changed files with 2,172 additions and 0 deletions.
237 changes: 237 additions & 0 deletions UserExperienceVirtualization/Detect-Uev.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
#Requires -PSEdition Desktop
#Requires -Version 5
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Detect whether the UE-V service is enabled
.DESCRIPTION
Detect whether the UE-V service is enabled and returns status code for Proactive Remediations
.NOTES
Author: Aaron Parker
Twitter: @stealthpuppy
.LINK
https://stealthpuppy.com/user-experience-virtualzation-intune/
.EXAMPLE
Set-Uev.ps1
#>
[CmdletBinding(SupportsShouldProcess = $False, HelpURI = "https://github.com/aaronparker/intune/blob/main/Uev/README.md")]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingWriteHost", "", Justification = "Output required by Proactive Remediations.")]
param (
[Parameter(Mandatory = $false)]
[System.String] $Uri = "https://stpydeviceause.blob.core.windows.net/uev/?comp=list",

[Parameter(Mandatory = $false)]
[System.String] $CustomTemplatesPath = "$env:ProgramData\Microsoft\UEV\CustomTemplates"
)

#region Functions
function Get-AzureBlobItem {
<#
.SYNOPSIS
Returns an array of items and properties from an Azure blog storage URL.
.DESCRIPTION
Queries an Azure blog storage URL and returns an array with properties of files in a Container.
Requires Public access level of anonymous read access to the blob storage container.
Works with PowerShell Core.
.NOTES
Author: Aaron Parker
Twitter: @stealthpuppy
.PARAMETER Url
The Azure blob storage container URL. The container must be enabled for anonymous read access.
The URL must include the List Container request URI. See https://docs.microsoft.com/en-us/rest/api/storageservices/list-containers2 for more information.
.EXAMPLE
Get-AzureBlobItems -Uri "https://aaronparker.blob.core.windows.net/folder/?comp=list"
Description:
Returns the list of files from the supplied URL, with Name, URL, Size and Last Modified properties for each item.
#>
[CmdletBinding(SupportsShouldProcess = $False)]
[OutputType([System.Management.Automation.PSObject])]
param (
[Parameter(ValueFromPipeline = $True, Mandatory = $True, HelpMessage = "Azure blob storage URL with List Containers request URI '?comp=list'.")]
[ValidatePattern("^(http|https)://")]
[System.String] $Uri
)

begin {}
process {
# Get response from Azure blog storage; Convert contents into usable XML, removing extraneous leading characters
try {
$iwrParams = @{
Uri = $Uri
UseBasicParsing = $True
ContentType = "application/xml"
ErrorAction = "Stop"
}
$list = Invoke-WebRequest @iwrParams
}
catch [System.Net.WebException] {
Write-Warning -Message ([System.String]::Format("Error : {0}", $_.Exception.Message))
throw $_.Exception.Message
}
catch [System.Exception] {
Write-Warning -Message "failed to download: $Uri."
throw $_.Exception.Message
}
if ($Null -ne $list) {
[System.Xml.XmlDocument] $xml = $list.Content.Substring($list.Content.IndexOf("<?xml", 0))

# Build an object with file properties to return on the pipeline
$fileList = New-Object -TypeName System.Collections.ArrayList
foreach ($node in (Select-Xml -XPath "//Blobs/Blob" -Xml $xml).Node) {
$PSObject = [PSCustomObject] @{
Name = $($node | Select-Object -ExpandProperty "Name")
Uri = $($node | Select-Object -ExpandProperty "Url")
Size = $($node | Select-Object -ExpandProperty "Size")
LastModified = $($node | Select-Object -ExpandProperty "LastModified")
}
$fileList.Add($PSObject) | Out-Null
}
if ($Null -ne $fileList) {
Write-Output -InputObject $fileList
}
}
}
}

function Test-WindowsEnterprise {
try {
Import-Module -Name "Dism"
$edition = Get-WindowsEdition -Online -ErrorAction "SilentlyContinue"
}
catch [System.Exception] {
Write-Error -Message "Failed to run Get-WindowsEdition. Defaulting to False."
}
if ($edition.Edition -eq "Enterprise") {
return $True
}
else {
return $False
}
}
function Write-ToEventLog ($Message) {
switch -Regex ($Message) {
"^Information" {
$EntryType = "Information"
$Number = 0
}
"^Warning" {
$EntryType = "Warning"
$Number = 1
}
"^Error" {
$EntryType = "Error"
$Number = 2
}
default {
$EntryType = "Information"
$Number = 0
}
}
$params = @{
LogName = "Application"
Source = "UevProactiveRemediation"
EventID = (8000 + $Number)
EntryType = $EntryType
Message = $Message
ErrorAction = "SilentlyContinue"
}
Write-EventLog @params
}
#endregion

# Create a new event log source
$params = @{
LogName = "Application"
Source = "UevProactiveRemediation"
ErrorAction = "SilentlyContinue"
}
New-EventLog @params

# If running Windows 10/11 Enterprise
if (Test-WindowsEnterprise) {

# If the UEV module is installed
if (Get-Module -ListAvailable -Name "UEV") {

# Detect the UE-V service
Import-Module -Name "UEV"
$status = Get-UevStatus
if ($status.UevEnabled -eq $True) {
$Message = "Detection result:"
$Result = 0

try {
# Retrieve the list of templates from the Azure Storage account, filter for .XML files only
$SrcTemplates = Get-AzureBlobItem -Uri $Uri | Where-Object { $_.Uri -match ".*.xml$" }
}
catch [System.Exception] {
$Message += "`nError at $Uri with $($_.Exception.Message)."
$Result = 1
}

# Validate the local copy of the custom templates against the Azure Storage account
$CustomTemplates = $(Get-ChildItem -Path $CustomTemplatesPath -Filter "*.xml" -ErrorAction "SilentlyContinue").Name
$params = @{
ReferenceObject = $SrcTemplates.Name
DifferenceObject = $CustomTemplates
ErrorAction = "SilentlyContinue"
}
if (($Null -eq $CustomTemplates) -or ($Null -ne (Compare-Object @params))) {
Write-ToEventLog -Message "Error: Local templates in $CustomTemplates do not match $Uri."
$Message += "`nLocal templates in $CustomTemplates do not match $Uri."
$Result = 1
}

# Check whether templates are registered
$RegisteredTemplates = Get-UevTemplate -ErrorAction "SilentlyContinue"
if ($Null -eq $RegisteredTemplates) {
Write-ToEventLog -Message "Error: No settings templates are registered."
$Message += "`nNo settings templates are registered."
$Result = 1
}

# Check whether a reboot is required
if ($status.UevRebootRequired -eq $True) {
Write-ToEventLog -Message "Warning: Reboot required to enable the UE-V service."
$Message += "`nReboot required to enable the UE-V service."
$Result = 1
}

# Exit with an error
if ($Result -eq 1) {
Write-Host $Message
exit $Result
}
else {
# If we get here, all is good
Write-ToEventLog -Message "UE-V service is enabled. $($RegisteredTemplates.Count) settings templates are registered."
Write-Host "UE-V service is enabled. $($RegisteredTemplates.Count) settings templates are registered."
exit 0
}
}
else {
Write-ToEventLog -Message "Warning: UE-V service is not enabled."
Write-Host "UE-V service is not enabled."
exit 1
}
}
else {
Write-ToEventLog -Message "Error: UEV module not installed."
Write-Host "UEV module not installed."
exit 1
}
}
else {
Write-ToEventLog -Message "Warning: Windows 10/11 Enterprise is required to enable UE-V."
Write-Host "Windows 10/11 Enterprise is required to enable UE-V."
exit 1
}
72 changes: 72 additions & 0 deletions UserExperienceVirtualization/Get-AzureBlobItem.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
function Get-AzureBlobItem {
<#
.SYNOPSIS
Returns an array of items and properties from an Azure blog storage URL.
.DESCRIPTION
Queries an Azure blog storage URL and returns an array with properties of files in a Container.
Requires Public access level of anonymous read access to the blob storage container.
Works with PowerShell Core.
.NOTES
Author: Aaron Parker
Twitter: @stealthpuppy
.PARAMETER Url
The Azure blob storage container URL. The container must be enabled for anonymous read access.
The URL must include the List Container request URI. See https://docs.microsoft.com/en-us/rest/api/storageservices/list-containers2 for more information.
.EXAMPLE
Get-AzureBlobItems -Uri "https://aaronparker.blob.core.windows.net/folder/?comp=list"
Description:
Returns the list of files from the supplied URL, with Name, URL, Size and Last Modified properties for each item.
#>
[CmdletBinding(SupportsShouldProcess = $False)]
[OutputType([System.Management.Automation.PSObject])]
param (
[Parameter(ValueFromPipeline = $True, Mandatory = $True, HelpMessage = "Azure blob storage URL with List Containers request URI '?comp=list'.")]
[ValidatePattern("^(http|https)://")]
[System.String] $Uri
)

begin {}
process {
# Get response from Azure blog storage; Convert contents into usable XML, removing extraneous leading characters
try {
$iwrParams = @{
Uri = $Uri
UseBasicParsing = $True
ContentType = "application/xml"
ErrorAction = "Stop"
}
$list = Invoke-WebRequest @iwrParams
}
catch [System.Net.WebException] {
Write-Warning -Message ([System.String]::Format("Error : {0}", $_.Exception.Message))
throw $_.Exception.Message
}
catch [System.Exception] {
Write-Warning -Message "failed to download: $Uri."
throw $_.Exception.Message
}
if ($Null -ne $list) {
[System.Xml.XmlDocument] $xml = $list.Content.Substring($list.Content.IndexOf("<?xml", 0))

# Build an object with file properties to return on the pipeline
$fileList = New-Object -TypeName "System.Collections.ArrayList"
foreach ($node in (Select-Xml -XPath "//Blobs/Blob" -Xml $xml).Node) {
$PSObject = [PSCustomObject] @{
Name = $($node | Select-Object -ExpandProperty "Name")
Uri = $($node | Select-Object -ExpandProperty "Url")
Size = $($node | Select-Object -ExpandProperty "Size")
LastModified = $($node | Select-Object -ExpandProperty "LastModified")
}
$fileList.Add($PSObject) | Out-Null
}
if ($Null -ne $fileList) {
Write-Output -InputObject $fileList
}
}
}
}
55 changes: 55 additions & 0 deletions UserExperienceVirtualization/Publish-Templates.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
trigger:
branches:
include:
- main
paths:
include:
- Uev/templates/*
- Uev/tests/*
- Uev/Publish-Templates.yml

jobs:
- job: push_templates
pool:
vmImage: windows-latest
steps:
- checkout: self
persistCredentials: true

- task: PowerShell@2
displayName: "Install Pester"
inputs:
targetType: 'inline'
script: |
Install-Module -Name "Pester" -Force -Confirm:$False
verbosePreference: 'SilentlyContinue'
pwsh: true
workingDirectory: '$(Build.SourcesDirectory)'

- task: PowerShell@2
displayName: "Validate templates against UE-V schema"
inputs:
targetType: 'inline'
script: |
Import-Module -Name "Pester" -Force
$Config = [PesterConfiguration]::Default
$Config.Run.Path = '$(Build.SourcesDirectory)\Uev\tests'
$Config.Run.PassThru = $True
$Config.CodeCoverage.Enabled = $False
$Config.TestResult.Enabled = $True
$Config.TestResult.OutputFormat = "NUnitXml"
$Config.TestResult.OutputPath = ".\TestResults.xml"
Invoke-Pester -Configuration $Config
verbosePreference: 'SilentlyContinue'
pwsh: true
workingDirectory: '$(Build.SourcesDirectory)\Uev\tests'

- task: AzureFileCopy@4
displayName: "Push templates to storage account"
inputs:
sourcePath: '$(Build.SourcesDirectory)\Uev\templates\*.xml'
azureSubscription: 'Visual Studio Enterprise Subscription(63e8f660-f6a4-4ac5-ad4e-623268509f20)'
destination: 'AzureBlob'
storage: 'stpydeviceause'
containerName: 'uev'
additionalArgumentsForBlobCopy: '--log-level=INFO'
17 changes: 17 additions & 0 deletions UserExperienceVirtualization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# User Experience Virtualization

* `Remediate-Uev.ps1` - a Proactive Remediation that enables the UE-V client, and downloads and registers a set of templates from an Azure storage account
* `Detect-Uev.ps1` - a Proactive Remediation to detect the status of the UE-V client
* `./templates` - custom UE-V templates
* `UserExperienceVirtualization-Profile.json` - an Intune Settings Catalog device configuration profile to configure User Experience Virtualization on Windows PCs

## Tests

[![Build Status](https://dev.azure.com/stealthpuppyLab/Uev/_apis/build/status/aaronparker.intune?branchName=main)](https://dev.azure.com/stealthpuppyLab/Uev/_build/latest?definitionId=15&branchName=main)

* `Publish-Templates.yml` - an Azure Pipeline that validates UE-V templates and uploads the templates to blob storage on an Azure storage account
* `./tests` - Pester tests to validate the UE-V templates

## Other scripts

* `Get-AzureBlobItem.ps1` - a function that returns items from Azure blob storage
Loading

0 comments on commit 8beaf15

Please sign in to comment.