Generate Windows Defender Application Control (WDAC) policies

This service has been retired. In my opinion, WDAC is not suited for application whitelisting, and I have stopped using WDAC on my machines some time ago.

For most scenarios, prefer AppLocker over WDAC. Note that AppLocker is now available for Home and Pro editions of Windows 10 and Windows 11 as well. AppLocker is also far easier to manage (via secpol.msc) than WDAC.

The PowerShell code that used to form the back end of this service is shown below. That code is no longer maintained, but you can still use it to create WDAC policies.

Set-StrictMode -Version 'Latest';
$ErrorActionPreference = 'Stop';

function Assert-FileRulesElement {
	[CmdletBinding()]
	param(
		[Parameter( Mandatory )]
		[xml]
		$Document
	);
	
	$rules = $Document.SelectSingleNode( '/pol:SiPolicy/pol:FileRules', $nsmgr );
	if( -not $rules ) {
		$rules = $Document.CreateElement( 'FileRules', $namespaceUrn );
		[void] $Document.DocumentElement.AppendChild( $rules );
	}
	return $rules;
}

function Assert-FileRulesRefElement {
	[CmdletBinding()]
	param(
		[Parameter( Mandatory )]
		[xml]
		$Document,
	
		[Parameter( Mandatory )]
		[System.Xml.XmlElement]
		$ScenarioElement
	);
	
	$signers = $ScenarioElement.SelectSingleNode( 'pol:ProductSigners', $nsmgr );
	if( -not $signers ) {
		$signers = $Document.CreateElement( 'ProductSigners', $namespaceUrn );
		[void] $ScenarioElement.AppendChild( $signers );
	}
	$refs = $signers.SelectSingleNode( 'pol:FileRulesRef', $nsmgr );
	if( -not $refs ) {
		$refs = $Document.CreateElement( 'FileRulesRef', $namespaceUrn );
		[void] $signers.AppendChild( $refs );
	}
	return $refs;
}

function Validate-AgainstSchema {
	[CmdletBinding()]
	param(
		[Parameter()]
		[string]
		$LiteralPath
	);

	$settings = [System.Xml.XmlReaderSettings]::new();
	$settings.ValidationType = [System.Xml.ValidationType]::Schema;
	[void] $settings.Schemas.Add( $namespaceUrn, "${env:windir}\schemas\CodeIntegrity\cipolicy.xsd" );
	$reader = [System.Xml.XmlReader]::Create( $LiteralPath, $settings );
	try {
		while( $reader.Read()) { }
	} finally {
		$reader.Close();
	}
}

function Add-WindirPathRules {
	[CmdletBinding()]
	param(
		[Parameter( Mandatory )]
		[xml]
		$Document
	);

	$as = $Document.SelectSingleNode( "//pol:SigningScenario[@Value='12']/pol:ProductSigners/pol:AllowedSigners", $nsmgr );
	[void] $as.ParentNode.RemoveChild( $as );
	
	New-PathRule -Document $Document -Path '%WINDIR%\*' -Mode 'Allow';
	
	@(
		'%WINDIR%\debug\WIA\*';
		'%WINDIR%\PLA\Reports\*';
		'%WINDIR%\PLA\Rules\*';
		'%WINDIR%\PLA\Templates\*';
		'%WINDIR%\Registration\CRMLog\*';
		'%WINDIR%\System32\Com\dmp\*';
		'%WINDIR%\System32\FxsTmp\*';
		'%WINDIR%\System32\LogFiles\WMI\*';
		'%WINDIR%\System32\Microsoft\Crypto\RSA\MachineKeys\*';
		'%WINDIR%\System32\spool\drivers\color\*';
		'%WINDIR%\System32\spool\PRINTERS\*';
		'%WINDIR%\System32\spool\SERVERS\*';
		'%WINDIR%\System32\Tasks_Migrated\*';
		'%WINDIR%\System32\Tasks\*';
		'%WINDIR%\SysWOW64\Com\dmp\*';
		'%WINDIR%\SysWOW64\FxsTmp\*';
		'%WINDIR%\SysWOW64\Tasks\*';
		'%WINDIR%\Tasks\*';
		'%WINDIR%\Temp\*';
		'%WINDIR%\tracing\*';
	) | ForEach-Object -Process {
		New-PathRule -Document $Document -Path $_ -Mode 'Deny';
	};
}

function New-DriverFile {
	[CmdletBinding()]
	param(
		[Parameter( Mandatory )]
		[string]
		$LiteralPath,

		[bool]
		$Executable
	);

	if( -not [System.IO.File]::Exists( $LiteralPath ) ) {
		throw "File '${LiteralPath}' does not exist.";
	}

	try {
		[Microsoft.SecureBoot.UserConfig.DriverFile] | Out-Null;
	} catch {
		Add-Type -LiteralPath "${env:windir}\Microsoft.Net\assembly\GAC_64\Microsoft.ConfigCI.Commands\v4.0_10.0.0.0__31bf3856ad364e35\Microsoft.ConfigCI.Commands.dll";
	}
	return [Microsoft.SecureBoot.UserConfig.DriverFile]::new( $LiteralPath, $true, $Executable );
}

function New-PathRule {
	[CmdletBinding()]
	param(
		[Parameter( Mandatory )]
		[string]
		$Path,
	
		[Parameter( Mandatory )]
		[ValidateSet( 'Allow', 'Deny' )]
		$Mode,
		
		[Parameter( Mandatory )]
		[xml]
		$Document
	);
	
	$nextId = & {
		[int] $max = $Document.SelectNodes( '/pol:SiPolicy/pol:FileRules/pol:Allow/@ID | /pol:SiPolicy/pol:FileRules/pol:Deny/@ID', $nsmgr ) | ForEach-Object -Process {
			if( $_.Value -match '^ID_(ALLOW|DENY)_(A|D)_(?<counter>[0-9A-F]+)_1$' ) {
				[convert]::ToInt32( $Matches[ 'counter' ], 16 );
			}
		} | Sort-Object | Select-Object -Last 1;
		if( $max ) {
			return $max + 1;
		} else {
			return 1;
		}
	};
	
	$elementName = $Mode;
	$idTemplate = switch( $Mode ) {
		'Deny' {
			 'ID_DENY_D_{0:X}_1';
		}
		'Allow' {
			'ID_ALLOW_A_{0:X}_1';
		}
		default {
			throw "Unknown value '${_}'.";
		}
	};
	
	$rules = Assert-FileRulesElement -Document $Document;
	$rule = $Document.CreateElement( $elementName, $namespaceUrn );
	$id = $idTemplate -f $nextId;
	$rule.SetAttribute( 'ID', $id );
	if( $path -match ':|\*|\\|/|%OSDRIVE%|%WINDIR%|%SYSTEM32%' ) {
		$rule.SetAttribute( 'FilePath', $path );
	} else {
		$rule.SetAttribute( 'FileName', $path );
	}
	[void] $rules.AppendChild( $rule );
	
	$refs = Assert-FileRulesRefElement -Document $Document -ScenarioElement(
		$Document.SelectSingleNode( '//pol:SigningScenario[@Value="12"]', $nsmgr )
	);
	$ref = $Document.CreateElement( 'FileRuleRef', $namespaceUrn );
	$ref.SetAttribute( 'RuleID', $id );
	[void] $refs.AppendChild( $ref );
}

function New-WdacPolicy {
	<#
	.SYNOPSIS
		Creates a WDAC (Windows Defender Application Control) policy and exports it as .xml und .cip files.
	
	.DESCRIPTION
		Use this function to create Code Integrity (CI) policy files for Windows Defender Application Control (WDAC). Such
		policies facilitate whitelisting or blacklisting of applications and drivers and are hence a powerful mechanism to
		protect your Windows PC from malware. WDAC policies are effective on all editions of Windows 10 and 11, including
		Home and Pro.
		
		To apply a policy, place the .cip file in the 'C:\Windows\System32\CodeIntegrity\CiPolicies\Active' folder and
		restart your computer. You can also take the corresponding .xml file and augment it further.
	#>
	[CmdletBinding()]
	param(
		[string[]]
		# Select file names or file paths to create a Path rule. You can use one wildcard ('*'), either at the beginning or at
		# the end of the path, but not in the middle. You can also use the the '%OSDRIVE%', '%WINDIR%' and '%SYSTEM32%' macros.
		$AllowPath = @(
			'%OSDRIVE%\Program Files (x86)\*';
			'%OSDRIVE%\Program Files\*';
			'%OSDRIVE%\Users\Admin*';
		),
	
		[string[]]
		# Select file names or file paths to explicitly deny certain files.
		$DenyPath = @(),
		
		[string[]]
		# Select script or executable files to create a Hash rule.
		$AllowHash = @(),
		
		[string[]]
		# Select files that are digitally signed with a certificate to create a Publisher Rule based on that certificate.
		$AllowPublisher = @(),
		
		[ValidateSet( 'Enabled', 'Disabled' )]
		[string]
		# When set to 'Disabled', PowerShell will run in Full Language Mode.
		#
		# When set to 'Enabled', PowerShell will run in Constrained Language Mode.
		$ScriptEnforcement = 'Enabled',
		
		[ValidateSet( 'Audit', 'AuditOnBootFailure' , 'Enforce' )]
		[string]
		# When set to 'Enforce', the policy will actually block drivers and applications unless explicitly allowed by the
		# policy.
		#
		# When set to 'Audit', Windows will record an event in the 'Microsoft-Windows-CodeIntegrity/Operational' or
		# 'Microsoft-Windows-AppLocker/MSI and Script' event logs if drivers and applications would have been blocked.
		#
		# When set to 'AuditOnBootFailure', Windows will switch to 'Audit' mode if the policy blocks a system driver and thus
		# would prevent the system from booting. Otherwise, 'Enforce' mode will be used.
		#
		# Note that policies created by this function always use the 'Enabled:Advanced Boot Options Menu' setting. Therefore, 
		# when a system driver is blocked and Windows cannot boot, press F8 to enter the Startup Settings screen and then F7 
		# to Disable driver signature enforcement.
		$AuditMode = 'Audit',
	
		[ValidateSet( 'PublisherRules', 'PathRules' )]
		[string]
		# When set to 'PublisherRules', built-in publisher rules will also apply to user-mode CI (in addition to kernel-mode
		# CI). A path rule for '%WINDIR%' is not created as legitimate files in this folder are all digitally signed.
		#
		# When set to 'PathRules', all files in the '%WINDIR%' folder and its subfolders will be allowed, but known
		# user-writable folders such as '%WINDIR%\Temp' or '%WINDIR%\Debug\WIA' will be blocked. Built-in publisher rules will
		# not apply to user-mode CI.
		$WindirWhitelisting = 'PublisherRules',
		
		[string]
		$WorkingDirectory = ${env:TEMP},
		
		[guid]
		$PolicyGuid = [guid]::NewGuid()
	);

	if( $PSVersionTable.PSEdition -ne 'Desktop' ) {
		throw 'This function must be run in Windows PowerShell. It cannot be run in PowerShell Core.';
	}
	
	$xmlFile = "${WorkingDirectory}\${PolicyGuid}.xml";
	$cipFile = [System.IO.Path]::ChangeExtension( $xmlFile, '.cip' );
	Copy-Item -LiteralPath "${env:windir}\schemas\CodeIntegrity\ExamplePolicies\DefaultWindows_Enforced.xml" -Destination $xmlFile;

	Merge-CIPolicy -PolicyPaths $xmlFile -OutputFilePath $xmlFile -Rules $(
		@(
			foreach( $file in $AllowHash ) {
				$executable = $file -imatch '\.(exe|com|scr|dll|cpl|ocx|mui)$';
				New-DriverFile -LiteralPath $file -Executable $executable | New-CIPolicyRule -Level 'Hash' -Fallback 'None';
			}

			foreach( $file in $AllowPublisher ) {
				New-DriverFile -LiteralPath $file -Executable $true | New-CIPolicyRule -Level 'Publisher' -Fallback 'None';
			}
		) | ForEach-Object -Process {
			$_;
		};
	) | Out-Null;

	Set-RuleOption -FilePath $xmlFile -Option  0; # Enabled:UMCI
	Set-RuleOption -FilePath $xmlFile -Option  6; # Enabled:Unsigned System Integrity Policy
	Set-RuleOption -FilePath $xmlFile -Option  9; # Enabled:Advanced Boot Options Menu
	Set-RuleOption -FilePath $xmlFile -Option 16; # Enabled:Update Policy No Reboot
	Set-RuleOption -FilePath $xmlFile -Option 18; # Disabled:Runtime FilePath Rule Protection

	switch( $ScriptEnforcement ) {
		'Enabled' {
			# Nothing to do.
		}
		'Disabled' {
			Set-RuleOption -FilePath $xmlFile -Option 11; # Disabled:Script Enforcement
		}
		default {
			throw "Unknown value '${_}'.";
		}
	};
	
	switch ( $AuditMode ) {
		'Audit' {
			Set-RuleOption -FilePath $xmlFile -Option  3; # Enabled:Audit Mode
			Set-RuleOption -FilePath $xmlFile -Option 10; # Enabled:Boot Audit on Failure
		}
		'AuditOnBootFailure' {
			Set-RuleOption -FilePath $xmlFile -Option 10; # Enabled:Boot Audit on Failure
		}
		'Enforce' {
			# Nothing to do.
		}
		default {
			throw "Unknown value '${_}'.";
		}
	};

	$xml = [xml]::new();
	$xml.Load( $xmlFile );
	$namespaceUrn = 'urn:schemas-microsoft-com:sipolicy';
	$nsmgr = [System.Xml.XmlNamespaceManager]::new( $xml.NameTable );
	$nsmgr.AddNamespace( 'pol', $namespaceUrn );

	& {
		$guid = $PolicyGuid.ToString( 'B' );
		$xml.SelectSingleNode( '/pol:SiPolicy/pol:PolicyID', $nsmgr ).InnerText = $guid;
		$xml.SelectSingleNode( '/pol:SiPolicy/pol:BasePolicyID', $nsmgr ).InnerText = $guid;
	};
	
	$xml.SelectNodes( '//@ID | //@SignerId | //@RuleID', $nsmgr ) | ForEach-Object -Process {
		$_.Value = $_.Value -replace '_0$', ''; 
	};

	$xml.SelectNodes( '//comment()', $nsmgr ) | ForEach-Object -Process {
		[void] $_.ParentNode.RemoveChild( $_ );
	};
	
	switch( $WindirWhitelisting ) {
		'PublisherRules' {
			# Nothing to do.
		}
		'PathRules' {
			Add-WindirPathRules -Document $xml;
		}
		default {
			throw "Unknown value '${_}'.";
		}
	};
	
	$AllowPath | Where-Object -FilterScript {
		-not [string]::IsNullOrWhiteSpace( $_ );
	} | ForEach-Object -Process {
		New-PathRule -Document $xml -Path $_ -Mode 'Allow';
	};

	$DenyPath | Where-Object -FilterScript {
		-not [string]::IsNullOrWhiteSpace( $_ );
	} | ForEach-Object -Process {
		New-PathRule -Document $xml -Path $_ -Mode 'Deny';
	};
	
	& {
		$scenario = $xml.SelectSingleNode( '//pol:SigningScenario[@Value="131"]', $nsmgr );
		$scenario.SetAttribute( 'ID', 'ID_SIGNINGSCENARIO_KMCI' );
		$scenario.SetAttribute( 'FriendlyName', 'Kernel Mode Signing Scenario' );
	};
	
	& {
		$scenario = $xml.SelectSingleNode( '//pol:SigningScenario[@Value="12"]', $nsmgr );
		$scenario.SetAttribute( 'ID', 'ID_SIGNINGSCENARIO_UMCI' );
		$scenario.SetAttribute( 'FriendlyName', 'User Mode Signing Scenario' );
	};
	
	& {
		$xml.SelectNodes( '/pol:SiPolicy/pol:EKUs/pol:EKU | /pol:SiPolicy/pol:FileRules/*', $nsmgr ) | ForEach-Object -Process {
			$name = 'FriendlyName';
			if( [string]::IsNullOrWhiteSpace( $_.GetAttribute( $name ) ) ) {
				$_.RemoveAttribute( $name );
			}
		};
	};
	
	$xml.Save( $xmlFile );
	Validate-AgainstSchema -LiteralPath $xmlFile;
	ConvertFrom-CIPolicy -XmlFilePath $xmlFile -BinaryFilePath $cipFile | Out-Null;
	[pscustomobject] [ordered] @{
		PolicyGuid = $PolicyGuid;
		XmlFile = $xmlFile;
		CipFile = $cipFile;
	};
}

Export-ModuleMember -Function 'New-WdacPolicy';
wdac.psm1

Download and import the .psm1 module, then call the New-WdacPolicy function, like so:

Import-Module -Name '…\wdac.psm1';
$params = @{
    AllowHash = @(
        'C:\Foo.exe';
        'C:\Bar.ps1';
    );
    ScriptEnforcement = 'Enabled';
    AuditMode = 'Audit';
    WindirWhitelisting = 'PublisherRules';
};
New-WdacPolicy @params;