Peter Hinchley

Learning in Public

✪ An Approach for Managing Microsoft AppLocker Policies

AppLocker is a software whitelisting product from Microsoft that ships with Windows. It can be used to restrict the software that will execute on a computer.

Overview of Policies

AppLocker policies are typically created and deployed using Group Policy. If multiple policies are deployed to a single computer, these policies are merged into a single "effective" policy (more on this later).

AppLocker policies are composed of rules that are organised into rule collections. Each collection contains rules targeting a specific type of executable code. There are five collections:

Each collection can be separately configured in one of three modes: disabled (ignore all rules within the collection), audit (do not prevent code from executing, but log rule violations), or enforced (prevent code from executing and log violations).

Each AppLocker rule includes a unique GUID identifier, a name, and a description. A rule also identifies the SID of the user or group that is to be targeted, and an action of either allow or deny (where allow is used to allow code to execute, and deny is used to prevent execution). Finally, a rule also includes a condition to identify the affected executable code (i.e. the scope). There are three types of conditions:

The path and hash conditions are straight forward, and each consist of a single value (i.e. the hash or path), but a total of four values are required to build a publisher rule. The values, listed with decreasing specificity, are: file version, file name, product name and publisher. Each of these values can be specifically defined, or generalised by using an asterisk wildcard. For example, by using a wildcard for product name, file name and version, you could "trust" all software published by a single vendor.

Whitelisting Approach

There are many approaches to application whitelisting. As to what is "best" for you will depend upon the risks you are seeking to mitigate, and the effort you are willing to expend on creating and maintaining an effective policy. However, here are a few high-level approaches to whitelisting:

  1. Limit users and administrators to running explicitly approved software.
  2. Limit users and administrators to running any software in approved locations (e.g. c:\windows and c:\program files).
  3. Limit users to running explicitly approved software, but allow local administrators to run any software.
  4. Limit users to running any software in locations that cannot be modified without administrator privileges (similar to option 2).
  5. Allow users to run any software except programs that are specifically blocked (blacklisting).

Having selected a suitable approach, you next need to decide how the rules will be enforced. For example, you could restrict execution by:

  1. Hash.
  2. Explicit path.
  3. Top level folder.
  4. Publisher certificate (with various levels of granularity).

Hash and publisher certificate (with all fields qualified) are the two most secure options (as both are independent of modifiable properties of a file). However, these two options also incur the greatest level of administrative effort (as any change to a file will require a corresponding change to the matching AppLocker rule).

Although whitelisting a top level folder (and everything therein) requires minimum effort, it won't prevent users from executing "inappropriate" software that is "incorrectly" installed into an approved location (e.g. an application manually installed by an administrator onto a shared computer becomes accessible to any user of the system).

Similarly, although using a publisher certificate that is scoped only to the publisher name may alleviate the need to modify AppLocker rules to support new product versions, it also opens the possibility that a user will be able to execute any software from the vendor, including software that may not be deemed suitable for your corporate environment.

As I said, there is no "right or wrong" approach. The key is to understand the risks to be mitigated, the support overheads you can sustain, and the agility required of your organisation. However, in this article, I will describe the steps required to implement a whitelisting policy based on the following principles:

Automating Policy Creation

Before describing the procedure for creating a new AppLocker policy, I will lay out a few assumptions:

These are the steps we will use to create a new AppLocker policy based on the approach outlined above:

  1. Create a default policy that implements a basic ruleset allowing administrators to run any software, and standard users to run software under c:\windows.
  2. Amend the default rules to prevent non-privileged users from executing software in "user-writable" locations.
  3. Create another policy to include the rules required to enable all software included within the SOE (i.e. a baseline that includes core applications like Microsoft Office).
  4. Create a new policy for each application that is authorised for optional installation (i.e. approved software that isn't baked into the SOE). Where possible, I recommend performing this step on a virtual machine. This will allow you to revert to a clean snapshot before processing each application.
  5. Create new "ad hoc" policies to address files that weren't successfully processed during any of the previous steps.
  6. Merge (and optimise) all of the policies created in the previous steps into a single policy that can be imported into Group Policy.

So let's work through this one step at a time. Firstly, to create the default policy:

  1. From any Windows 10 computer that does not already include an AppLocker policy, run gpedit.msc as an administrator.
  2. Expand Local Computer Policy, Computer Configuration, Windows Settings, Security Settings, Application Control Policies, AppLocker.
  3. Right click the AppLocker node and select Properties.
  4. Select the Advanced tab, select the option Enable the DLL rule collection, and click OK.
  5. Right click Executable Rules, and select Create Default Rules.
  6. Delete the rule named (Default Rule) All files located in the Program Files folder.
  7. Repeat steps 5 and 6 for Windows Installer Rules, Script Rules, DLL Rules, and Packaged App Rules. Note: Step 6 isn't necessary for Windows Installer Rules and Packaged App Rules.
  8. Right click the AppLocker node and select Export Policy.
  9. Save the policy to a network share which we will henceforth refer to as the AppLocker "repository". e.g. \\server\applocker\default.xml.

The next step is to modify this policy to exclude all user-writable locations. To help, we will use a script to scan the file system for folders with permissions that grant write access to standard users.

Create a script in your repository named UserWriteableLocations.ps1 with the following content:

# Paths that we've already excluded via AppLocker.
$exclusions = @()

# Paths to process.
$paths = @(
  "C:\Windows"
)

# Setup log.
$log = "$PSScriptRoot\UserWritableLocations.log"

$FSR = [System.Security.AccessControl.FileSystemRights]

# Unfortunately the FileSystemRights enum doesn't contain all the values from the Win32 API. Urgh.
$GenericRights = @{
  GENERIC_READ    = [int]0x80000000;
  GENERIC_WRITE   = [int]0x40000000;
  GENERIC_EXECUTE = [int]0x20000000;
  GENERIC_ALL     = [int]0x10000000;
  FILTER_GENERIC  = [int]0x0FFFFFFF;
}

# ... so we need to map them ourselves.
$MappedGenericRights = @{
  FILE_GENERIC_READ    = $FSR::ReadAttributes -bor $FSR::ReadData -bor $FSR::ReadExtendedAttributes -bor $FSR::ReadPermissions -bor $FSR::Synchronize
  FILE_GENERIC_WRITE   = $FSR::AppendData -bor $FSR::WriteAttributes -bor $FSR::WriteData -bor $FSR::WriteExtendedAttributes -bor $FSR::ReadPermissions -bor $FSR::Synchronize
  FILE_GENERIC_EXECUTE = $FSR::ExecuteFile -bor $FSR::ReadPermissions -bor $FSR::ReadAttributes -bor $FSR::Synchronize
  FILE_GENERIC_ALL     = $FSR::FullControl
}

Function Map-GenericRightsToFileSystemRights([System.Security.AccessControl.FileSystemRights]$Rights) {
  $MappedRights = New-Object -TypeName $FSR

  if ($Rights -band $GenericRights.GENERIC_EXECUTE) {
    $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_EXECUTE
  }

  if ($Rights -band $GenericRights.GENERIC_READ) {
   $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_READ
  }

  if ($Rights -band $GenericRights.GENERIC_WRITE) {
    $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_WRITE
  }

  if ($Rights -band $GenericRights.GENERIC_ALL) {
    $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_ALL
  }

  return (($Rights -band $GenericRights.FILTER_GENERIC) -bor $MappedRights) -as $FSR
}

# These are the rights from the FileSystemRights enum we care about.
$WriteRights = @('WriteData', 'CreateFiles', 'CreateDirectories', 'WriteExtendedAttributes', 'WriteAttributes', 'Write', 'Modify', 'FullControl')

# Helper function to match against a list of patterns.
function notlike($string, $patterns) {
  foreach ($pattern in $patterns) { if ($string -like $pattern) { return $false } }
  return $true
}

# The hard work...
function scan($path, $log) {
  $cache = @()
  gci $path -recurse -exclude $exclusions -force -ea silentlycontinue |
  ? {($_.psiscontainer) -and (notlike $_.fullname $exclusions)} | %{
    trap { continue }
    $directory = $_.fullname
    (get-acl $directory -ea silentlycontinue).access |
    ? {$_.isinherited -eq $false} |
    ? {$_.identityreference -match ".*USERS|EVERYONE"} | %{
      (map-genericrightstofilesystemrights $_.filesystemrights).tostring().split(",") | %{
        if ($writerights -contains $_.trim()) {
          if ($cache -notcontains $directory) { $cache += $directory }
        }
      }
    }
  }
  return $cache
}

# Start scanning.
$paths | %{ scan $_ $log } | out-file $log 

Now run the script as an administrator:

powershell.exe -file \\server\applocker\userwritablelocation.ps1

The results will be written to the repository as \\server\applocker\UserWritableLocations.log.

Note: The above script recursively searches for user-writable locations under c:\windows, however, it can easily be extended to scan other locations by adding the appropriate folders to the $paths variable (e.g. c:\program files (x86) and c:\program files).

Out of interest, these are the user-writable sub-folders under c:\windows on a default installation of Windows 10 Long Term Service Branch:

C:\Windows\Tasks
C:\Windows\Temp
C:\Windows\tracing
C:\Windows\PLA\Reports
C:\Windows\PLA\Rules
C:\Windows\PLA\Templates
C:\Windows\PLA\Reports\en-US
C:\Windows\PLA\Rules\en-US
C:\Windows\Registration\CRMLog
C:\Windows\System32\FxsTmp
C:\Windows\System32\Tasks
C:\Windows\System32\Com\dmp
C:\Windows\System32\LogFiles\WMI
C:\Windows\System32\Microsoft\Crypto\RSA\MachineKeys
C:\Windows\System32\spool\PRINTERS
C:\Windows\System32\spool\SERVERS
C:\Windows\System32\spool\drivers\color
C:\Windows\System32\Tasks\Microsoft\Windows\PLA
C:\Windows\System32\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update
C:\Windows\System32\Tasks\Microsoft\Windows\PLA\System
C:\Windows\SysWOW64\FxsTmp
C:\Windows\SysWOW64\Tasks
C:\Windows\SysWOW64\Com\dmp
C:\Windows\SysWOW64\Tasks\Microsoft\Windows\PLA
C:\Windows\SysWOW64\Tasks\Microsoft\Windows\RemoteApp and Desktop Connections Update
C:\Windows\SysWOW64\Tasks\Microsoft\Windows\PLA\System

We will now create another script in the repository named InsertUserWritableLocations.ps1 to update the default policy with the output of the previous script.

# The input file, created from UserWritableLocations.ps1.
$input = "$PSScriptRoot\UserWritableLocations.log"

# The default locations trusted by AppLocker.
$trusted = @{
  "C:\Windows" = @(
    "(Default Rule) All files located in the Windows folder",
    "(Default Rule) All scripts located in the Windows folder",
    "(Default Rule) Microsoft Windows DLLs"
  );
  "C:\Program Files" = @(
    "(Default Rule) All files located in the Program Files folder",
    "(Default Rule) All scripts located in the Program Files folder",
    "(Default Rule) All DLLs located in the Program Files folder"
  )
}

# First run UserWritableLocations.ps1 to create an input file.
if (-not (test-path $input)) { write-host "File not found: $input"; exit }

# Categorise user-writable locations.
$content = get-content $input
$untrusted = @{
  'C:\Program Files' = @($content | ? { $_ -like 'C:\Program Files\*' });
  'C:\Windows'       = @($content | ? { $_ -like 'C:\Windows\*' });
}

# Load the default AppLocker policy.
[xml]$policy = get-content "$PSScriptRoot\Default.xml"

# Add path exceptions to the AppLocker policy.
$trusted.keys | %{
  $folder = $_
  $trusted.item($folder) | %{
    $rule = $policy.selectsinglenode("//FilePathRule[@Name='$_']")
    if (! $rule) { return }
    $exceptions = $policy.createelement("Exceptions")
    [void]$rule.appendchild($exceptions)
    $untrusted.item($folder) | %{
      $exception = $policy.createelement("FilePathCondition")
      $exception.setattribute("Path", "$_\*")
      [void]$exceptions.appendchild($exception)
    }
  }
}

# Save the modified AppLocker policy.
$policy.save("$PSScriptRoot\default.xml")

And now, to insert the user-writable locations into the default policy:

powershell.exe -file \\server\applocker\insertuserwritablelocation.ps1

Note: I have intentionally configured this script to process exceptions for c:\windows and c:\program files. We only need support for c:\windows, but if you decided to keep the default rules for c:\program files, and you also scanned this folder for user-writable locations, the above script has you covered.

The remaining steps in our process will be managed via a single script named AppLocker.ps1. Add this script to the repository with the following content:

<# examples:
# generate a baseline policy.
powershell.exe -file applocker.ps1 -baseline -output c:\policies\baseline.xml

# generate an application-specific policy.
powershell.exe -file applocker.ps1 -application -in c:\policies\baseline.xml -out c:\policies\application.xml

# generate an adhoc policy.
powershell.exe -file applocker.ps1 -adhoc -in c:\path -filter *.* -out c:\policies\adhoc.xml

# merge policies.
powershell.exe -file applocker.ps1 -merge -in c:\policies -out c:\policies\merged.xml
#>

<#  expected usage:
1. run the script to generate a baseline. e.g.
   powershell.exe -file applocker.ps1 -baseline -out c:\policies\baseline.xml
2. install an application.
3. rerun the script to generate an application-specific policy. e.g.
   powershell.exe -file applocker.ps1 -application -in c:\policies\baseline.xml -out c:\policies\someapp.xml
4. rerun the script to merge applocker policies under c:\policies. e.g.
   powershell.exe -file applocker.ps1 -merge -in c:\policies -out c:\policies\soe.xml
#>

param(
  [switch]$baseline,
  [switch]$application,
  [switch]$adhoc,
  [switch]$merge,
  [string]$in,
  [string]$filter,
  [string]$out
)

function log($message)   { write-host -foregroundcolor green $message }
function error($message) { write-host -foregroundcolor magenta $message; exit }

# check baseline switches.
if ($baseline) {
  if ($out -eq $null) { error 'Usage: powershell.exe -file applocker.ps1 -baseline -out c:\policies\baseline.xml' }
  if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}

# check application switches.
if ($application) {
  if ($in -eq $null -or $out -eq $null) { error 'powershell.exe -file applocker.ps1 -application -in c:\policies\baseline.xml -out c:\policies\application.xml' }
  if (! (test-path $in)) { error 'The baseline policy does not exist.' }
  if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}

# check adhoc switches.
if ($adhoc) {
  if ($in -eq $null -or $filter -eq $null -or $out -eq $null) { error 'powershell.exe -file applocker.ps1 -adhoc -in c:\path -filter *.* -out c:\policies\adhoc.xml' }
  if (! (test-path $in)) { error 'The input folder does not exist.' }
  if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}

# check merge switches.
if ($merge) {
  if ($in -eq $null -or $out -eq $null) { error 'powershell.exe -file applocker.ps1 -merge -in c:\policies -out c:\policies\merged.xml' }
  if (! (test-path $in)) { error 'The input folder does not exist.' }
  if (! (test-path (split-path $out -parent))) { error 'The output folder does not exist.' }
}

# we will trust all products published by the following vendors.
$vendors = "O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US", "O=CITRIX SYSTEMS, INC., L=SANTA CLARA, S=CALIFORNIA, C=US"

# directories to scan.
$paths = @('C:\Program Files', 'C:\Program Files (x86)')

# rule collection types.
$types = 'Appx', 'Dll', 'Exe', 'Msi', 'Script'

# empty applocker policy.
$xml = [xml]@'
<AppLockerPolicy Version="1">
<RuleCollection Type="Appx" EnforcementMode="Enabled" />
<RuleCollection Type="Dll" EnforcementMode="Enabled" />
<RuleCollection Type="Exe" EnforcementMode="Enabled" />
<RuleCollection Type="Msi" EnforcementMode="Enabled" />
<RuleCollection Type="Script" EnforcementMode="Enabled" />
</AppLockerPolicy>
'@

# empty publisher rule.
$publisher = [xml]@'
<FilePublisherRule Id="" Name="" Description="" UserOrGroupSid="S-1-1-0" Action="Allow">
  <Conditions>
    <FilePublisherCondition PublisherName="" ProductName="*" BinaryName="*">
      <BinaryVersionRange LowSection="*" HighSection="*" />
    </FilePublisherCondition>
  </Conditions>
</FilePublisherRule>
'@

# empty file hash rule.
$hash = [xml]@'
<FileHashRule Id="" Name="Windows 10 Hash Rules" Description="" UserOrGroupSid="S-1-1-0" Action="Allow">
  <Conditions>
    <FileHashCondition />
  </Conditions>
</FileHashRule>
'@

# return the "real" extension of a file.
function get-filetype($path) {
  $extension = [io.path]::getextension($path)
  $extensions = @('.dll', '.exe', '.js', '.msi', '.ps1', '.vbs')

  # extensions of file types that are actually dlls.
  $imposterdlls = @('.api', '.8bx')

  if ($extensions -contains $extension) { return $extension }

  # a hack for files which are dll's without a pe header.
  if ($imposterdlls -contains $extension) { return '.dll' }

  $bytes = get-content $path -readcount 0 -encoding byte
  $offset = $bytes[0x3c]
  $signature = [char[]] $bytes[$offset..($offset + 3)]

  if ([string]::join('', $signature) -eq "PE`0`0") {
    $header = $offset + 4
    $data = [bitconverter]::toint32($bytes, $header + 18)
    if ($data -band 0x2000) { return '.dll' } else { return '.exe' }
  }

  return $extension
}

# insert a rule based on $template, with a rule type of $class, under $parent, into $policy.
function insert-rule($policy, $parent, $template, $class) {
  $rule = $template.clone()
  $rule.$class.id = [string]([guid]::newguid().guid)
  return $parent.appendchild($policy.importnode($rule.$class, $true))
}

# insert $hashes into $policy under $parent.
function insert-hashes($policy, $hashes, $parent) {
  $hashes | %{ [void]$parent.conditions.firstchild.appendchild($policy.importnode($_, $true)) }
}

# insert $publishers into $policy under $parent.
function insert-publishers($policy, $publishers, $parent) {
  $publishers | %{
    $_.conditions.filepublishercondition.binaryname = '*'
    $_.conditions.filepublishercondition.binaryversionrange.lowsection = '*'
    $_.conditions.filepublishercondition.binaryversionrange.highsection = '*'
    [void]$parent.appendchild($policy.importnode($_, $true))
  }
}

# insert $paths into $policy under $parent.
function insert-paths($policy, $paths, $parent) {
  $paths | %{ [void]$parent.appendchild($policy.importnode($_, $true)) }
}

# insert conditions ($hashes and $publishers) into $policy under $parent.
function insert-conditions($policy, $parent, $hashes, $publishers) {
  if ($hashes.count) {
    $node = insert-rule $policy $parent $hash 'filehashrule'
    insert-hashes $policy $hashes $node
  }

  if ($publishers.count) { insert-publishers $policy $publishers $parent }
}

# merge policy $one with $two and return union.
function merge-policies($one, $two) {
  $target = $xml.clone()

  foreach ($type in $types) {
    $hashes = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") +
                $two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data -unique)

    $publishers = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") +
                    $two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | sort-object -property name -unique)

    # this script will never generate path rules, but we need to cater for merging with a policy that does.
    $paths = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePathRule") +
               $two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePathRule") | sort-object -property name -unique)

    $parent = $target.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
    insert-conditions $target $parent $hashes $publishers
    insert-paths $target $paths $parent
  }

  return $target
}

# remove duplicate conditions between $one and $two and return result.
function remove-duplicates($one, $two) {
  $target = $xml.clone()

  foreach ($type in $types) {
    $h1 = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data)
    $h2 = @($two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data)
    $hashes = @(); compare-object -referenceobject $h1 -differenceobject $h2 -property data -passthru | ? { $_.sideindicator -eq '<=' } | %{ $hashes += $_ }

    $p1 = @($one.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | sort-object -property name)
    $p2 = @($two.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | sort-object -property name)
    $publishers = @(); compare-object -referenceobject $p1 -differenceobject $p2 -property name -passthru | ? { $_.sideindicator -eq '<=' } | %{ $publishers += $_ }

    $parent = $target.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
    insert-conditions $target $parent $hashes $publishers
  }

  return $target
}

# applocker cannot handle more than 1,500 or so hashes in a single rule. let's break into blocks of 1000.
function split-hashes($applocker) {
  foreach ($type in $types) {
    $collection = $applocker.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")
    $hashes = @($applocker.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data)

    if ([math]::floor($hashes.count) -gt 0) {
      [void]$collection.removechild($collection.filehashrule)
    }

    $min = 0; while ($min -lt $hashes.count) {
      $max = $min + 999
      insert-conditions $applocker $collection $hashes[$min..$max] $null
      $min = $max + 1
    }
  }
}

# initialise policy.
$applocker = $xml.clone()

# initialise rules array.
$rules = @()

# if merging policies.
if ($merge) {
  $applocker = $xml.clone()

  get-childitem $in\*.xml -exclude (split-path $out -leaf) | %{
    log "Merging AppLocker policy $($_.name)."
    $applocker = merge-policies $applocker ([xml](get-content $_.fullname -encoding utf8))
  }

  log "Chunking hashes into blocks of 1,000."
  split-hashes $applocker

  log "Saving the merged policy."
  $applocker.save($out)
  return
}

# if generating an adhoc policy.
if ($adhoc) {
  log "Creating adhoc AppLocker rules for $filter in folder $in."

  $rules +=  get-childitem -path $in -filter $filter -recurse | select -expandproperty fullname | get-applockerfileinformation -ea silentlycontinue
  $rules | %{
    $_.path = $_.path -replace '%OSDRIVE%', 'C:'
    $extension = [io.path]::getextension($_.path)
    $_.path = $_.path -replace $extension, (get-filetype $_.path)
  }

  $policy = [xml]($rules | new-applockerpolicy -ruletype publisher, hash -user everyone -xml -ignoremissingfileinformation)
} else {
  log "Creating AppLocker rules for the requested paths."
  $paths | %{ $rules += get-applockerfileinformation -directory $_ -recurse -ea silentlycontinue }
  $policy = [xml]($rules | new-applockerpolicy -ruletype publisher, hash -user everyone -xml -ignoremissingfileinformation)
}

log "Optimising rules."
foreach ($type in $types) {
  $parent = $applocker.applockerpolicy.selectsinglenode("//RuleCollection[@Type='$type']")

  $hashes = @($policy.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']//FileHash") | sort-object -property data -unique)
  $publishers = @($policy.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule") | %{
    $_.name = "{0} - {1}" -f $_.conditions.filepublishercondition.publishername, $_.conditions.filepublishercondition.productname; $_
  } | sort-object -property name -unique)

  insert-conditions $applocker $parent $hashes $publishers
}

log "Consolidating file publisher conditions for trusted vendors."
foreach ($vendor in $vendors) {
  $node = $publisher.clone()
  $node.filepublisherrule.name = [string]"$vendor - *"
  $node.filepublisherrule.conditions.filepublishercondition.publishername = [string]$vendor

  foreach ($type in $types) {
    $parent = $false

    $applocker.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule[contains(@Name, '$vendor')]") | %{
      $parent = $_.parentnode
      [void]($parent.removechild($_))
    }

    if ($parent -ne $false) {
      $node.filepublisherrule.id = [string]([guid]::newguid().guid)
      [void]$parent.appendchild($applocker.importnode($node.filepublisherrule, $true))
    }
  }
}

if ($application) {
  log "Comparing the new policy with the baseline and removing duplicate conditions."
  $applocker = remove-duplicates $applocker ([xml](get-content $in -encoding utf8))
}

log "Removing publisher rule wildcards."
foreach ($type in $types) {
  $applocker.applockerpolicy.selectnodes("//RuleCollection[@Type='$type']/FilePublisherRule/Conditions/FilePublisherCondition/BinaryVersionRange") | %{
    $_.LowSection  = "*"
    $_.HighSection = "*"
  }
}

log "Saving the policy."
$applocker.save($out)

In this step, we will use the above script to scan c:\program files and c:\program files (x86) and generate a "baseline" policy based first on publisher certificate (qualified to product), falling back to hash when a certificate cannot be used.

As a reminder, this step should be performed on a system immediately after the deployment of the SOE (i.e. before any "optional" software is deployed).

To generate the baseline, we will use the baseline switch, specifying the output file path of \\server\applocker\baseline.xml using the out switch:

powershell.exe -file \\server\applocker\applocker.ps1 -baseline -out \\server\applocker\baseline.xml

Having generated a baseline policy for the SOE, it is now time to create a separate policy for each corporate application. If you are using a virtual machine, at this point you would take a snapshot, and then install an approved application that is not already in the SOE (e.g. Microsoft Visio). To generate a policy specific to the application, use the command shown below. This will effectively generate a delta policy that includes rules for all files installed by the application (it does this by generating a new baseline, comparing this with the previous baseline, and exporting the differences). In this instance we use the application switch to indicate we are generating an application-specific policy, the in switch to identify the path to the previously created baseline, and the out switch to specify the name of the resultant application policy.

powershell.exe -file \\server\applocker\applocker.ps1 -application -in \\server\applocker\baseline.xml -out c:\temp\viso.xml

At this point, you would restore the virtual machine snapshot, and then repeat the process for another application.

In some cases you might need to create AppLocker rules for files that aren't deployed via an installer. As an example, there may be a small collection of files dynamically created within a temporary directory when a program is executed (and which aren't present in the Program Files directory when the application is installed). In this scenario, you should collect the relevant files, copy them into a temporary location, and then use the AppLocker script to generate an "adhoc" policy. For example, assuming you have copied a collection of DLL files into a folder named c:\adhoc, you can call the script with the adhoc switch, with the in parameter set to the directory containing the files, the out parameter set to the path of the resultant policy, and filter set to a mask that identifies the files to be scanned (this could be *.* if multiple files types are to be included):

powershell.exe -file \\server\applocker\applocker.ps1 -adhoc -in c:\adhoc -filter *.dll -out \\server\applocker\adhoc.xml

The "adhoc" switch is also useful when generating an AppLocker policy for an application that installs executable files with non-standard file extensions. Adobe Acrobat, for example, uses DLLs with an extension of .8bx. These file types will not be processed by the native AppLocker PowerShell cmdlets which are used when the script is called with the application switch, and hence won't be picked up in an application-specific policy. We can circumvent this limitation by using he adhoc switch to generate a policy that specifically targets this file type.

If you review the source code of the AppLocker script, you will notice the function get-filetype is used to determine the "real" file type of a file with a non-standard extension. The function attempts to do this by inspecting the file's PE header, but if the file doesn't actually conform to the portable executable file format, you may have to resort to explicitly forcing a specific return type. This is exactly what I have done for files with an extension of .api and .8bx.

The final step in the process is to merge the various policies together into a single consolidated policy. Assuming you have collated all the policies into a common repository accessed via \\server\applocker, you can call the script with the merge switch, specifying the source folder using the in parameter, and the name of the final policy using the out parameter:

powershell.exe -file \\server\applocker\applocker.ps1 -merge -in \\server\applocker -out \\server\applocker\soe.xml

The script will optimise the merged polices to produce the smallest possible output. Note: The merge process assumes that all of the source policies were generated with the AppLocker script. Do not attempt to merge an AppLocker policy generated via another method.

You can now import the consolidated AppLocker policy (soe.xml) into group policy, and link it to the appropriate organisational unit.

A couple of additional notes on this process: