Moving Applocker control from Group Policy to Intune
Lately I've been trying to migrate a lot of GPOs to Intune so that our endpoints don't have to depend on a VPN for updating policy. Applocker was an important one for us since VPNs are flaky and it's important that users be able to run updated software while away from the office network. Chrome once updated its signing certificate and because it auto-updates itself we had users who couldn't launch Chrome until they were able to get connected to the VPN and run a gpupdate.
I already had the plumbing in place which allows Admins to upload files to a file share and have them automatically added as whitelisted to the Applocker GPO. Now I just had to move the enforcement of the GPO into Intune.
I decided to keep the dependency on our Domain Controllers merging the changes into the Applocker policy since there are some pretty good Powershell commands that take care of that. The GPO for Applocker is still being updated but is only used for Intune to pull from and turn into a Device Configuration Policy.
The pieces used in this process consist of the following:
Azure Storage Account file share (for uploading the .exe needing to be whitelisted)
Azure Automation Account (for Runbooks)
Azure Automation Hybrid Worker (for executing the necessary code needing to run on the Domain Controller)
Microsoft Graph API (for connecting to Intune from the Runbook)
Intune Device Configuration Profile (this takes the place of the Group Policy Object for enforcing the Applocker policy)
Azure Active Directory App Registration (this provides the permissions to the Runbook for Microsoft Graph)
A diagram showing how the pieces connect together:
Step 1 is pretty self explanatory. We just use an Azure File Share only accessible by us Admins for uploading the .exe file which we want to add to the whitelist.
Step 2 is covered here
Step 3 is simply an addition to the scheduled task referenced in step 2 to execute the Azure Automation Runbook once the GPO merges are complete. So the Scheduled Task script ends up looking like this:
Step 4 uses an Azure Automation Runbook along with a Hybrid Worker to run code on a Domain Controller to get the updated GPO containing the newly whitelisted software and push it into an Intune Device Configuration Profile using the Microsoft Graph API.
This Runbook is what took the most work to figure out because of all the slicing and dicing of the XML required to get it to work with Microsoft Graph. There may be a better way to do this but this is what worked for me and only modifies the EXE portion of the Applocker GPO. The runbook code is below:
I already had the plumbing in place which allows Admins to upload files to a file share and have them automatically added as whitelisted to the Applocker GPO. Now I just had to move the enforcement of the GPO into Intune.
I decided to keep the dependency on our Domain Controllers merging the changes into the Applocker policy since there are some pretty good Powershell commands that take care of that. The GPO for Applocker is still being updated but is only used for Intune to pull from and turn into a Device Configuration Policy.
The pieces used in this process consist of the following:
Azure Storage Account file share (for uploading the .exe needing to be whitelisted)
Azure Automation Account (for Runbooks)
Azure Automation Hybrid Worker (for executing the necessary code needing to run on the Domain Controller)
Microsoft Graph API (for connecting to Intune from the Runbook)
Intune Device Configuration Profile (this takes the place of the Group Policy Object for enforcing the Applocker policy)
Azure Active Directory App Registration (this provides the permissions to the Runbook for Microsoft Graph)
A diagram showing how the pieces connect together:
Step 1 is pretty self explanatory. We just use an Azure File Share only accessible by us Admins for uploading the .exe file which we want to add to the whitelist.
Step 2 is covered here
Step 3 is simply an addition to the scheduled task referenced in step 2 to execute the Azure Automation Runbook once the GPO merges are complete. So the Scheduled Task script ends up looking like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
======================================================================= | |
# This script will auto-add stuff to the AppLocker Enforce GPO | |
# | |
# Parse a directory to obtain either the file hash or publisher of a file | |
$acctKey = ConvertTo-SecureString -String "XXXXX" -AsPlainText -Force | |
$credential = New-Object System.Management.Automation.PSCredential -ArgumentList "Azure\XXXXX", $acctKey | |
New-PSDrive -Name Z -PSProvider FileSystem -Root "\\XXXXX.file.core.windows.net\XXXXX" -Credential $credential | |
$exeDirectory = 'z:\' | |
$testFilePath = 'z:\*' | |
$xmlFile = 'c:\temp\applocker.txt' | |
$xmlPolicy = 'c:\temp\applockerpolicy.xml' | |
$ldapPathEnforce = 'LDAP://domaincontroller/CN={GPO ID},CN=Policies,CN=System,DC=YOURDOMAIN,DC=ORG' | |
$webHook = "https://s1events.azure-automation.net/webhooks?token=XXXXX" | |
$EmailTo = "" | |
$EmailFrom = "" | |
$Subject = "Applocker rule added" | |
$SMTPServer = "smtp.office365.com" | |
$SecureString = "c:\scripts\azureemailer.creds" | |
$SmtpPort = "587" | |
$Creds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $EmailFrom,(Get-Content -Path $SecureString | ConvertTo-SecureString -AsPlainText -Force) | |
#Only run if something is in the directory | |
if (test-path $testFilePath) { | |
Get-AppLockerFileInformation -Filetype exe -Directory $exeDirectory -Recurse | New-AppLockerPolicy -RuleType Publisher,Hash -User Everyone -RuleNamePrefix AutoGenerated -XML | Out-File $xmlFile | |
# Merge contents of the newly generated XML file into the current AppLocker GPO | |
Set-AppLockerPolicy -XmlPolicy $xmlFile -LDAP $ldapPathEnforce -Merge | |
# Get new applocker policy into an object | |
$newPolicy = get-applockerpolicy -domain -ldap 'LDAP://domaincontroller/CN={GPO ID},CN=Policies,CN=System,DC=YOURDOMAIN,DC=ORG' | |
# Delete the contents of the AppLocker temp directory to avoid duplicate rules | |
Remove-Item z:\* | |
Remove-PSDrive -Name Z | |
Send-MailMessage -To $EmailTo -From $EmailFrom -Subject $Subject -Attachments $xmlFile -SmtpServer $SMTPServer -Port $SmtpPort -Credential $Creds | |
# Delete the temporary XML file used to update the GPO | |
Remove-Item c:\temp\applocker.XML | |
# Run Azure Automation Runbook to update InTune via a webhook | |
invoke-webrequest -uri "$webHook" -Method "POST" -UseBasicParsing | |
} |
Step 4 uses an Azure Automation Runbook along with a Hybrid Worker to run code on a Domain Controller to get the updated GPO containing the newly whitelisted software and push it into an Intune Device Configuration Profile using the Microsoft Graph API.
This Runbook is what took the most work to figure out because of all the slicing and dicing of the XML required to get it to work with Microsoft Graph. There may be a better way to do this but this is what worked for me and only modifies the EXE portion of the Applocker GPO. The runbook code is below:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$VerbosePreference = ‘Continue’ | |
function Connect-MSGraphAPI { | |
# Get the stored credentials from Azure Automation Credential Manager to use in the PowerShell Runbook for MS Graph authentication | |
$graphClientCreds = Get-AutomationPSCredential -Name 'xxxxx' | |
$graphClientPW = $graphClientCreds.GetNetworkCredential().Password | |
$graphUserCreds = Get-AutomationPSCredential -Name 'xxxxx' | |
$graphUserPW = $graphUserCreds.GetNetworkCredential().Password | |
# MS Graph Token Request Body for Azure PowerShell Runbook: | |
$graphTokenRequestBody = @{ | |
"scope" = "https://graph.microsoft.com/.default"; | |
"grant_type" = "password"; | |
"client_id" = "$($graphClientCreds.UserName)"; | |
"client_secret" = "$graphClientPW"; | |
"username" = "$($graphUserCreds.UserName)"; | |
"password" = "$graphUserPW"; | |
} | |
# Define the URI using your own Azure tenant name: | |
$tenantName = "your-tenant-name" | |
$graphRequestUri = "https://login.microsoftonline.com/$tenantName.onmicrosoft.com/oauth2/v2.0/token" | |
# Get the current datetime and add one hour to it: | |
$graphTokenExpirationDate = (Get-Date).AddHours(1) | |
# Make sure the error variable starts empty: | |
$GraphAPITokenRequestError = $null | |
# Send the Post request to Microsoft and receive an OAuth2 token: | |
$script:GraphAPIAuthResult = (Invoke-RestMethod -Method Post -Uri $graphRequestUri -Body $graphTokenRequestBody -ErrorAction SilentlyContinue -ErrorVariable GraphAPITokenRequestError) | |
# If there's an error requesting the token, say so, display the error, and break: | |
if ($GraphAPITokenRequestError) { | |
Write-Output "FAILED - Unable to retreive MS Graph API Authentication Token - $($GraphAPITokenRequestError)" | |
Break | |
} | |
} | |
# SET THE HEADER FOR ALL MS GRAPH API REQUESTS | |
function Set-GraphAPIRequestHeader { | |
$script:graphAPIReqHeader = @{ | |
'Content-Type'='application/json' | |
Authorization = "Bearer $($script:GraphAPIAuthResult.access_token)" | |
Host = "graph.microsoft.com" | |
} | |
} | |
# SET THE BODY | |
# This script will query Active Directory for the latest Applocker GPO and extract it to a file for use in an Intune Configuration Policy | |
# Send email on failure | |
function sendmail ([string]$failure, [string]$emailBody) { | |
$emailAddress = "" | |
$cred = Get-AutomationPSCredential -Name "xxxxx" | |
Send-MailMessage -to $emailAddress -From "" -SmtpServer "smtp.office365.com" -Port 587 -UseSsl -Credential $cred -Subject $failure -Body $emailBody | |
} | |
# Specify the Object ID of the group who will be assigned this policy | |
$TargetGroupId = "Active Directory Object ID of the target group" | |
try { | |
$oldPolicy = get-applockerpolicy -domain -ldap 'LDAP://domain-controller/CN={ID of GPO used for Applocker},CN=Policies,CN=System,DC=DOMAIN,DC=ORG' -xml | |
} | |
catch { | |
#Write-Error $_ | |
$oldPolicy = get-applockerpolicy -domain -ldap 'LDAP://domain-controller/CN={ID of GPO used for Applocker}},CN=Policies,CN=System,DC=DOMAIN,DC=ORG' -xml | |
} | |
# Count from the beginning of the original Applocker Policy XML output to reach the point where the .exe rule collection portion begins | |
$countFromBeginning = 602 | |
# The number of characters from the end of the trimmed XML to trim off the policy not pertaining to the .exe rule collection | |
$countFromEnd = 6411 | |
# Extract out only the .exe rule collection portion of the policy into a new policy | |
if ($oldPolicy.length -gt 6411) { | |
$newPolicy = ($oldPolicy).Substring($countFromBeginning,$oldPolicy.length-$countFromEnd) | |
$newPolicy.length | |
$oldPolicy.length | |
} | |
else { | |
$emailBody = $oldPolicy | |
$failure = "Failed to retrieve old policy" | |
sendmail $failure $emailBody | |
break | |
} | |
# Clean up the new policy to escape any special characters and such | |
$newPolicy = $newPolicy -replace '"', '\"' | |
$newPolicy = $newPolicy.Replace("PROGRAMFILES%\", "PROGRAMFILES%\\") | |
$newPolicy = $newPolicy.Replace("c:\\program files (x86)\","c:\\program files (x86)\\") | |
$newPolicy = $newPolicy -replace '(\w)(\\)(\w)', '$1\\$3' | |
$newPolicy = $newPolicy.Replace("%OSDRIVE%\", "%OSDRIVE%\\") | |
$newPolicy = $newPolicy.Replace("\*", "\\*") | |
$newPolicy = $newPolicy -replace '(\w)(\\\\")', '$1\\\"' | |
$newPolicy = $newPolicy -replace '(\w)(:\\)(\w)', '$1:\\$3' | |
$newPolicy = $newPolicy -replace '(\*)(\\)(\w)', '$1\\$3' | |
$newPolicy = $newPolicy.Replace("REMOVABLE%\", "REMOVABLE%\\") | |
$newPolicy = $newPolicy.Replace("\\DOMAIN.ORG", "\\\\DOMAIN.ORG") | |
$newPolicy = $newPolicy.Replace("\\DOMAIN.org", "\\\\DOMAIN.org") | |
$newPolicy = $newPolicy.Replace("%HOT%\", "%HOT%\\") | |
# Piece together the JSON like a Frankenstein monster | |
$policyName = "ApplockerEnforce " + (Get-Date) | |
$bodyPartOne = '{ | |
"@odata.context": "https://graph.microsoft.com/v1.0/$metadata#deviceManagement/deviceConfigurations/$entity", | |
"@odata.type": "#microsoft.graph.windows10CustomConfiguration", | |
"id": "", | |
"lastModifiedDateTime": "", | |
"createdDateTime": "", | |
"description": "", | |
"displayName":"' | |
$bodyPartTwo = '", | |
"version": 1, | |
"omaSettings": [ | |
{ | |
"@odata.type": "#microsoft.graph.omaSettingString", | |
"displayName": "EXE", | |
"description": null, | |
"omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Native/EXE/Policy", | |
"value": "' | |
$bodyPartThree = $newPolicy | |
$bodyPartFour = '"}, | |
{ | |
"@odata.type": "#microsoft.graph.omaSettingString", | |
"displayName": "MSI", | |
"description": null, | |
"omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Native/MSI/Policy", | |
"value": " <RuleCollection Type=\"Msi\" EnforcementMode=\"AuditOnly\">\n </RuleCollection>" | |
}, | |
{ | |
"@odata.type": "#microsoft.graph.omaSettingString", | |
"displayName": "SCRIPT", | |
"description": null, | |
"omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Native/Script/Policy", | |
"value": " <RuleCollection Type=\"Script\" EnforcementMode=\"AuditOnly\">\n <FilePathRule Id=\"06dce67b-934c-454f-a263-2515c8796a5d\" Name=\"(Default Rule) All scripts located in the Program Files folder\" Description=\"Allows members of the Everyone group to run scripts that are located in the Program Files folder.\" UserOrGroupSid=\"S-1-1-0\" Action=\"Allow\">\n <Conditions>\n <FilePathCondition Path=\"%PROGRAMFILES%\\*\" />\n </Conditions>\n </FilePathRule></Conditions>\n </FileHashRule>\n </RuleCollection>" | |
}, | |
{ | |
"@odata.type": "#microsoft.graph.omaSettingString", | |
"displayName": "APPX", | |
"description": null, | |
"omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Native/StoreApps/Policy", | |
"value": " <RuleCollection Type=\"Appx\" EnforcementMode=\"AuditOnly\">\n <FilePublisherRule Id=\"a9e18c21-ff8f-43cf-b9fc-db40eed693ba\" Name=\"(Default Rule) All signed packaged apps\" Description=\"Allows members of the Everyone group to run packaged apps that are signed.\" UserOrGroupSid=\"S-1-1-0\" Action=\"Allow\">\n <Conditions>\n <FilePublisherCondition PublisherName=\"*\" ProductName=\"*\" BinaryName=\"*\">\n <BinaryVersionRange LowSection=\"0.0.0.0\" HighSection=\"*\" />\n </FilePublisherCondition>\n </Conditions>\n </FilePublisherRule>\n </RuleCollection>" | |
}, | |
{ | |
"@odata.type": "#microsoft.graph.omaSettingString", | |
"displayName": "DLL", | |
"description": null, | |
"omaUri": "./Vendor/MSFT/AppLocker/ApplicationLaunchRestrictions/Native/DLL/Policy", | |
"value": "<RuleCollection Type=\"Dll\" EnforcementMode=\"NotConfigured\" />" | |
} | |
] | |
}' | |
$body = $bodyPartOne + $policyName + $bodyPartTwo + $bodyPartThree + $bodyPartFour | |
# Check authentication token status | |
function Invoke-GraphAPIAuthTokenCheck { | |
$currentDateTimePlusTen = (Get-Date).AddMinutes(10) | |
if ($script:GraphAPIAuthResult) { | |
if (!($currentDateTimePlusTen -le $script:GraphAPIAuthResult.expiration_time)) { | |
Connect-MSGraphAPI | |
Set-GraphAPIRequestHeader | |
} else { | |
Set-GraphAPIRequestHeader | |
} | |
} else { | |
Connect-MSGraphAPI | |
Invoke-GraphAPIAuthTokenCheck | |
} | |
} | |
# Call the function | |
Invoke-GraphAPIAuthTokenCheck | |
# We have a token so now let's do stuff with it | |
$headers = $script:graphAPIReqHeader | |
$graphApiVersion = "v1.0" | |
$DCP_resource = "deviceManagement/deviceConfigurations" | |
# The Graph request to create a device configuration policy | |
$uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" | |
try { | |
$request = (Invoke-WebRequest -Uri "$uri" -Method "POST" -Headers $headers -Body $body -outfile "c:\temp\output.txt" -PassThru -UseBasicParsing -ErrorAction Stop | convertFrom-JSON) | |
} | |
catch { | |
$failure = "Create new policy failed" | |
$emailBody = "The new policy failed to create. Re-add the executable to the Applocker directory." | |
sendmail $failure $emailBody | |
break | |
} | |
# Get the value of the device configuration policy we are replacing and delete it | |
$graphApiVersion = "beta" | |
try { | |
$oldConfigurationPolicyId = Get-AutomationVariable -Name "PolicyId" | |
} | |
catch { | |
$failure = "Configuration Policy ID does not exist" | |
$emailBody = "The ID of the previous policy could not be retrieved from the automation variable storage" | |
sendmail $failure $emailBody | |
break | |
} | |
# Copy the ID of the configuration policy being replaced to a variable | |
Set-AutomationVariable -Name "oldPolicyId" -Value "$oldConfigurationPolicyId" | |
# The ID of the newly created configuration policy | |
try { | |
$configurationPolicyId = $request.id | |
# Store the ID of the new policy for deletion after the next update | |
Set-AutomationVariable -Name "PolicyId" -Value $configurationPolicyId | |
} | |
catch { | |
$failure = "Could not store the value of the new policy in the automation accould variable storage. \n This will result in the new policy not being deleted after it is replaced on the next run." | |
} | |
# Now that the policy is created and we have the ID of it we need to assign it to the group specified in the TargetGroupID | |
$DCP_resource = "deviceManagement/deviceConfigurations/$configurationPolicyId/assign" | |
$uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" | |
# Piece together the JSON for assigning the configuration policy | |
$bodyPartOne = ' | |
{ | |
"assignments": [ | |
{ | |
"target":{ | |
"@odata.type": "#microsoft.graph.groupAssignmentTarget", | |
"groupId": "' | |
$bodyPartTwo = '" | |
}} | |
] | |
}' | |
$body = $bodyPartOne + $TargetGroupId + $bodyPartTwo | |
# Assign the new policy to the group | |
$uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" | |
try { | |
Invoke-WebRequest -Uri "$uri" -Headers $headers -Method "POST" -Body $body -UseBasicParsing -ErrorAction Stop -TimeoutSec 30 | |
} | |
catch { | |
$failure = "Assignment of new policy failed" | |
$emailBody = "The new configuration policy could not be assigned to the group. You will need to manually assign the profile in the Intune console." | |
sendmail $failure $emailBody | |
} | |
# Now it's safe to delete the old policy | |
$DCP_resource = "deviceManagement/deviceConfigurations/$oldConfigurationPolicyId" | |
$uri = "https://graph.microsoft.com/$graphApiVersion/$($DCP_resource)" | |
try { | |
Invoke-WebRequest -Uri "$uri" -Method "DELETE" -Headers $headers -UseBasicParsing -ErrorAction Stop | |
} | |
catch { | |
$failure = "Unable to delete old policy. You will need to manually delete it in the Intune console." | |
} |
0congloKlib-ze Jared Ellenberg https://wakelet.com/wake/8jcO27QU0VFWEeg_g5J21
ReplyDeletecoabestlugoo
Great tips and very easy to understand. This will definitely be very useful for me when I get a chance to start my blog. how to drag click
ReplyDeletetersdicam_ho Corey Pickell Avast Premier
ReplyDeleteFixMeStick
FonePaw
viarigsejee