declarative-ad-framework/gpo/lib/GPOAppLocker.ps1
Damien Coles f172d00514 Initial release: Declarative AD Framework v2.1.0
Infrastructure-as-code framework for Active Directory objects and Group Policy.
Sanitized from production deployment for public sharing.
2026-02-19 17:02:42 +00:00

273 lines
9.4 KiB
PowerShell

# GPOAppLocker.ps1
# AppLocker policy management via GPO.
# Uses Set-AppLockerPolicy / Get-AppLockerPolicy with -LDAP parameter.
# Depends on: GPOCore.ps1
function ConvertTo-AppLockerXml {
<#
.SYNOPSIS
Converts an AppLockerPolicy hashtable into AppLocker XML format.
Generates unique GUIDs per rule and resolves user names to SIDs.
#>
param(
[Parameter(Mandatory)]
[hashtable]$AppLockerPolicy
)
$esc = [System.Security.SecurityElement]
$collectionMap = @{
Exe = 'Exe'
Msi = 'Msi'
Script = 'Script'
Appx = 'Appx'
Dll = 'Dll'
}
$collections = foreach ($collectionName in $AppLockerPolicy.Keys) {
$collection = $AppLockerPolicy[$collectionName]
$xmlType = $collectionMap[$collectionName]
if (-not $xmlType) {
Write-Host " [WARN] Unknown AppLocker collection: $collectionName" -ForegroundColor Yellow
continue
}
$enforcement = if ($collection.EnforcementMode -eq 'Enabled') {
'Enabled'
} else {
'AuditOnly'
}
$ruleXml = foreach ($rule in $collection.Rules) {
$ruleId = [Guid]::NewGuid().ToString()
$ruleName = if ($rule.Name) { $esc::Escape($rule.Name) } else { "$collectionName rule $ruleId" }
$ruleDesc = if ($rule.Description) { $esc::Escape($rule.Description) } else { '' }
$action = $rule.Action # Allow or Deny
# Resolve user to SID
$userSid = 'S-1-1-0' # Default: Everyone
try {
$ntAccount = New-Object System.Security.Principal.NTAccount($rule.User)
$userSid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]).Value
} catch {
# Well-known SIDs
if ($rule.User -eq 'Everyone') {
$userSid = 'S-1-1-0'
} else {
Write-Host " [WARN] Cannot resolve '$($rule.User)' to SID, using Everyone" -ForegroundColor Yellow
}
}
$conditionXml = switch ($rule.Type) {
'Publisher' {
$pub = $esc::Escape($rule.Publisher)
$prod = if ($rule.Product) { $esc::Escape($rule.Product) } else { '*' }
$bin = if ($rule.Binary) { $esc::Escape($rule.Binary) } else { '*' }
$lowVer = if ($rule.LowVersion) { $rule.LowVersion } else { '0.0.0.0' }
$highVer = if ($rule.HighVersion) { $rule.HighVersion } else { '*' }
@"
<Conditions>
<FilePublisherCondition PublisherName="$pub" ProductName="$prod" BinaryName="$bin">
<BinaryVersionRange LowSection="$lowVer" HighSection="$highVer"/>
</FilePublisherCondition>
</Conditions>
"@
}
'Path' {
$path = $esc::Escape($rule.Path)
@"
<Conditions>
<FilePathCondition Path="$path"/>
</Conditions>
"@
}
'Hash' {
$hash = $rule.Hash
$fileName = if ($rule.FileName) { $esc::Escape($rule.FileName) } else { '' }
$fileLength = if ($rule.SourceFileLength) { $rule.SourceFileLength } else { '0' }
@"
<Conditions>
<FileHashCondition>
<FileHash Type="SHA256" Data="$hash" SourceFileName="$fileName" SourceFileLength="$fileLength"/>
</FileHashCondition>
</Conditions>
"@
}
default {
Write-Host " [WARN] Unknown rule type: $($rule.Type)" -ForegroundColor Yellow
''
}
}
$elementName = switch ($rule.Type) {
'Publisher' { 'FilePublisherRule' }
'Path' { 'FilePathRule' }
'Hash' { 'FileHashRule' }
default { 'FilePublisherRule' }
}
@"
<$elementName Id="$ruleId" Name="$ruleName" Description="$ruleDesc" UserOrGroupSid="$userSid" Action="$action">
$conditionXml
</$elementName>
"@
}
@"
<RuleCollection Type="$xmlType" EnforcementMode="$enforcement">
$($ruleXml -join "`n")
</RuleCollection>
"@
}
return @"
<AppLockerPolicy Version="1">
$($collections -join "`n")
</AppLockerPolicy>
"@
}
function Set-GPOAppLockerPolicy {
<#
.SYNOPSIS
Applies an AppLocker policy to a GPO using Set-AppLockerPolicy.
Full overwrite semantics.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[hashtable]$AppLockerPolicy,
[string]$Domain = (Get-ADDomain).DNSRoot
)
Write-Host " Applying AppLocker policy..." -ForegroundColor Yellow
$xml = ConvertTo-AppLockerXml -AppLockerPolicy $AppLockerPolicy
# Get GPO GUID for LDAP path
$gpo = Get-GPO -Name $GPOName -Domain $Domain
$gpoGuid = $gpo.Id.ToString('B').ToUpper()
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
$ldapPath = "LDAP://CN=$gpoGuid,CN=Policies,CN=System,$domainDN"
# Write a temp file with the XML (Set-AppLockerPolicy requires -XmlPolicy file path or pipeline)
$tempFile = [System.IO.Path]::GetTempFileName()
try {
[System.IO.File]::WriteAllText($tempFile, $xml, [System.Text.UTF8Encoding]::new($true))
Set-AppLockerPolicy -XmlPolicy $tempFile -LDAP $ldapPath
Write-Host " [OK] AppLocker policy applied to $GPOName" -ForegroundColor Green
} finally {
Remove-Item $tempFile -Force -ErrorAction SilentlyContinue
}
# Report what was set
foreach ($coll in $AppLockerPolicy.Keys) {
$ruleCount = @($AppLockerPolicy[$coll].Rules).Count
$mode = $AppLockerPolicy[$coll].EnforcementMode
Write-Host " $coll`: $ruleCount rule(s), $mode" -ForegroundColor Green
}
}
function Compare-GPOAppLockerPolicy {
<#
.SYNOPSIS
Compares desired AppLocker policy against current GPO state.
Reports missing/extra collections and rules.
#>
param(
[Parameter(Mandatory)]
[string]$GPOName,
[Parameter(Mandatory)]
[hashtable]$AppLockerPolicy,
[string]$Domain = (Get-ADDomain).DNSRoot
)
$diffs = @()
Write-Host " Comparing AppLocker policy..." -ForegroundColor Yellow
# Get GPO GUID for LDAP path
$gpo = Get-GPO -Name $GPOName -Domain $Domain
$gpoGuid = $gpo.Id.ToString('B').ToUpper()
$domainDN = (Get-ADDomain -Server $Domain).DistinguishedName
$ldapPath = "LDAP://CN=$gpoGuid,CN=Policies,CN=System,$domainDN"
# Get current AppLocker policy
$currentXml = $null
try {
$currentXml = Get-AppLockerPolicy -Domain -LDAP $ldapPath -Xml -ErrorAction Stop
} catch {
# No policy set
}
if (-not $currentXml) {
Write-Host " [DRIFT] No AppLocker policy configured" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'AppLocker'
Collection = '(all)'
Status = 'No policy'
}
return $diffs
}
# Parse current XML
[xml]$currentDoc = $currentXml
foreach ($collectionName in $AppLockerPolicy.Keys) {
$desired = $AppLockerPolicy[$collectionName]
$desiredMode = if ($desired.EnforcementMode -eq 'Enabled') { 'Enabled' } else { 'AuditOnly' }
# Find matching collection in current XML
$currentCollection = $currentDoc.AppLockerPolicy.RuleCollection |
Where-Object { $_.Type -eq $collectionName }
if (-not $currentCollection) {
Write-Host " [DRIFT] Missing collection: $collectionName" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'AppLocker'
Collection = $collectionName
Status = 'Missing collection'
}
continue
}
# Check enforcement mode
if ($currentCollection.EnforcementMode -ne $desiredMode) {
Write-Host " [DRIFT] $collectionName enforcement: $($currentCollection.EnforcementMode) -> $desiredMode" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'AppLocker'
Collection = $collectionName
Status = "EnforcementMode: $($currentCollection.EnforcementMode) -> $desiredMode"
}
}
# Compare rule counts
$currentRuleCount = @($currentCollection.ChildNodes | Where-Object { $_.LocalName -like '*Rule' }).Count
$desiredRuleCount = @($desired.Rules).Count
if ($currentRuleCount -ne $desiredRuleCount) {
Write-Host " [DRIFT] $collectionName rule count: $currentRuleCount -> $desiredRuleCount" -ForegroundColor Red
$diffs += [PSCustomObject]@{
Type = 'AppLocker'
Collection = $collectionName
Status = "Rule count: $currentRuleCount -> $desiredRuleCount"
}
} else {
Write-Host " [OK] $collectionName`: $currentRuleCount rule(s), $($currentCollection.EnforcementMode)" -ForegroundColor Green
}
}
if ($diffs.Count -eq 0) {
Write-Host " [OK] AppLocker policy matches desired state" -ForegroundColor Green
} else {
Write-Host " [DRIFT] $($diffs.Count) AppLocker difference(s) found" -ForegroundColor Red
}
return $diffs
}