Infrastructure-as-code framework for Active Directory objects and Group Policy. Sanitized from production deployment for public sharing.
273 lines
9.4 KiB
PowerShell
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
|
|
}
|