How to handle Windows 10 App language updates – implemented with SCCM

Logbook of the Captain – sidereal time: 2018.08.14

Since one customer came up with the need of some Windows 10 apps in the deployed image, I was searching for the easiest but securest way of deploying the apps with the correct preferred language of the client.

On the one hand I want to deploy the updates for the apps (only available via Windows Store), but on the second hand I need to find a way to restrict the ability for the users to install and use apps by themselves.

Attention: All my ideas were implemented with ConfigMgr, AD and Powershell

So what did I do?

  1. make shure, the default user have one preferred language
  2. Define folders, where users are allowed to execute scripts, executables or packaged apps (Win 10 Apps)
  3. Disable Access to the main-partition for the authenticated users (when needed, they get explicit access to the folders they need)
  4. Create Applocker rules for scripts, executables and packaged apps and allow only the in step 1 defined folders and apps
  5. Create Scheduled Task for the logon of the first user with the trigger script that includes the start of the update process
  6. Ensure that access to the store is defined via policy

So lets start!

I always include all needed language packs in my image via capture task sequence, i.e. with the following PowerShell function (It’s a “Run commandline” step with language packs in subfolder LP_Win10 of the package attatched):

#.SYNOPSIS
# Install all available language packs in attatched configmgr package folder
#.DESCRIPTION
#
function Install-LanguagePacks(){
  BEGIN{$Global:FunctionName = $myinvocation.mycommand.name}
  PROCESS{
     try{
         #prepare variables
         Write-Host -Value "Install windows Language Packs - Start"
         $source = ".\LP_Win10\"
         $array = @(Get-ChildItem $source -Filter *.cab)
         $cabs = $array.Name
         Write-Host -Value "Following Cab-Files available: $cabs"
              foreach ($cab in $cabs){
                    $pathtocab = $source + $cab
                    $block = { cmd.exe /c DISM.exe /online /Add-Package /PackagePath:$pathtocab }
                    Write-Host -Value "Running the Command: $block"
                    Invoke-Command -ScriptBlock $block
                    Write-Host -Value "Successfully integrated $cab into Windows image"
        }
     } catch {
              $ErrorExceptions = $Global:Error
              foreach($ErrorException in $ErrorExceptions){
                                      Write-Host ($ErrorException)
                                      }
              $Global:Error.Clear()
              Exit -1
             }
     Write-Host "Install windows Language Packs - FINISH"
    }
  END{}
}

@Step1 ( make shure, the default user have one preferred language ):

In the deployment task sequence you can define the target language with the help of a variable. how the variable is filled is up to you. For example, via a UI or a collection variable.

With the following function you can determine the name of the variable for the language via the parameter “CustVariable”. This variable must be part of the collection i.e.

#.SYNOPSIS
# Sets the language settings via a given collection variable
#.DESCRIPTION
#
function Set-LanguageByCollectionVariable(){
    param(
    [Parameter(Mandatory=$True)][string]$CustVariable
    )
        BEGIN{$Global:FunctionName = $myinvocation.mycommand.name}
        PROCESS{
            try{
            	Write-Host "Set language by collection variable - START"
                Write-Host "Use Custom Variable for Collection with Language: $CustVariable"
                $tsenv = New-Object -COMObject Microsoft.SMS.TSEnvironment
                $Language = $tsenv.Value($CustVariable)
                Write-Host "ConfigFile (TS-Var): $Language"
                $XMLFile = ("$LocationPath\" + $Language + ".xml")
                Write-Host "XMl file: $XMLFile"
                $FileName = "control.exe"
	        $Arguments = "intl.cpl,, /f:`"$XMLFile`""
                Write-Host "Running following command: $FileName $Arguments" -Value "Running following command: $FileName $Arguments"
		Start-Process $FileName $Arguments -Wait
               } catch {
                    $ErrorExceptions = $Global:Error
                    foreach($ErrorException in $ErrorExceptions){
                        Write-Host ($ErrorException)
                        }
                    $Global:Error.Clear()
                    Exit -1
                    }
                    Write-Host "Set language by collection variable - FINISH"
		}
        END{}
}

The script will execute the command and apply the xml included in atteched package. it selects the xml by the name of the variable, so in my example the XML files in the packages are:

en-US.xml

de-DE.xml

the xml has to look something like that for english i.e.:

<gs:GlobalizationServices xmlns:gs=”urn:longhornGlobalizationUnattend”>
<!– User List –>
<gs:UserList>
<gs:User UserID=”Current” CopySettingsToSystemAcct=”true” CopySettingsToDefaultUserAcct=”true”/>
</gs:UserList>
<!– Display Language –>
<gs:MUILanguagePreferences>
<gs:MUILanguage Value=”en-US” />
</gs:MUILanguagePreferences>
<!– System locale –>
<gs:SystemLocale Name=”en-US”/>
<!– User Locale –>
<gs:UserLocale>
<gs:Locale Name=”en-US” SetAsCurrent=”true”/>
</gs:UserLocale>
<!– Location –>
<!– GeoIDs: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/intl/nls_locations.asp –>
<gs:LocationPreferences>
<gs:GeoID Value=”244″/>
</gs:LocationPreferences>
<!– Keyboard preferences –>
<gs:InputPreferences>
<!–en-US–>
<gs:InputLanguageID Action=”add” ID=”0409:00000409″ Default=”true”/>
<!–de-DE–>
<gs:InputLanguageID Action=”remove” ID=”0407:00000407″/>
</gs:InputPreferences>
</gs:GlobalizationServices>

After applying this, all users logging in for the first time getting this language.

@Step2 (Define folders, where users are allowed to execute scripts, executables or packaged apps):

As a rule, standard directories are something like ProgramFiles or Windows folders (but these are defined as default rules in Applocker anyway). Think about which directories you still need! I always put under C: \ a folder with the name ClientMgmt, in the folder I include the following folders: AdminTools, Logs, Temp and Scripts. These will be released later in order to be able to execute them as user items from there.

@Step3 (Disable Access to the main-partition for the authenticated users (when needed, they get explicit access to the folders they need)):

I prefer to take away the C: \ partition permission of the Authenticated Users. The normal user now have only read permissions at the C:\ drive. Each additional writing access to the C: \ disk can now be entered separately. This increases safety many times over, but also means expenses.

You can achieve this goal with the following PowerShell code:

#Set Permissions to ClientMGMT Folder
$Clientmgmt = "$($env:SystemDrive)\ClientMgmt"
$ACL = Get-Acl -Path $Clientmgmt
$accessrule = New-Object System.Security.AccessControl.FileSystemAccessRule("Users","Read",,,"Allow")
$ACL.RemoveAccessRuleAll($accessrule)
Set-Acl -Path $Clientmgmt -AclObject $ACL
icacls C:\ /remove:g *S-1-5-11

@Step4 (Create Applocker rules for scripts, executables and packaged apps and allow only the in step 1 defined folders and apps):

The Applocker policys for executables and scripts should look like the following. Define them under Computer\Policies\Windows Settings\Security Settings\Application Control Policies. Right Klick in each of them (Appx Rules, Executables, scripts) and create Default Rules, then add your own. After rules are created you have to enforce them via the main config (root entry in policy). If you have problems, use the way the guys from ccmexec did it for the built-in apps:

https://ccmexec.com/2015/08/blocking-built-in-apps-in-windows-10-using-applocker/

We now allow in the appx policy all the apps we want to run, everything should look something like this:

20180814-GPO_Applocker1

20180814-GPO_Applocker2

Go sure, the application Identity Service is also enabled via your gpo

@Step5 (Create Scheduled Task for the logon of the first user with the trigger script that includes the start of the update process):

I’ve created a script, that will be used by the scheduled tasks. This script can be copied locally in a task sequence step under C:\ClientMgmt\Scripts and will then be executed by the scheduled task. Code should look something like that:

# Get all the provisioned packages
$Packages = (get-item 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore\Applications') | Get-ChildItem

# Filter the list if provided a filter
$PackageFilter = $args[0]
if ([string]::IsNullOrEmpty($PackageFilter))
       { echo "No filter specified, attempting to re-register all provisioned apps." }
else{
      $Packages = $Packages | where {$_.Name -like $PackageFilter}
       if ($Packages -eq $null)
        {
         echo "No provisioned apps match the specified filter."
         exit
        }
        else {
           echo "Registering the provisioned apps that match $PackageFilter"
        }
}

ForEach($Package in $Packages)
{
	# get package name & path
	$PackageName = $Package | Get-ItemProperty | Select-Object -ExpandProperty PSChildName
	$PackagePath = [System.Environment]::ExpandEnvironmentVariables(($Package | Get-ItemProperty | Select-Object -ExpandProperty Path))
        # register the package
        echo "Attempting to register package: $PackageName"
        Add-AppxPackage -register $PackagePath -DisableDevelopmentMode
}

$namespaceName = "root\cimv2\mdm\dmmap"
$className = "MDM_EnterpriseModernAppManagement_AppManagement01"
$wmiObj = Get-WmiObject -Namespace $namespaceName -Class $className
$result = $wmiObj.UpdateScanMethod()

your scheduled task schould look like this (my Script name is “reinstall-preinstalledApps.ps1”):

20180814-gpo_scheduledtask1

20180814-GPO_scheduledtask2

@Step6 (Ensure that access to the store is defined via policy):

The Options in the GPO for the store should be like this (ATTENTION: The other Settings in this policy folder should set to “Not configured”!!!):

20180814-GPO_storesettings

Finish 🙂

Captain over and out…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s