diff --git a/templates/AppLockerProject/.github/workflows/publish.yml b/templates/AppLockerProject/.github/workflows/publish.yml new file mode 100644 index 0000000..e61adb2 --- /dev/null +++ b/templates/AppLockerProject/.github/workflows/publish.yml @@ -0,0 +1,34 @@ + +on: + push: + branches: + - main + +jobs: + publish: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + - name: Install Prerequisites + run: .\build\prerequisites.ps1 -BuildWorker -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) + shell: powershell + - name: Validate Configuration Data + run: .\build\validate.ps1 -TestType ConfigurationData -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) -ProjectRoot $env:GITHUB_WORKSPACE + shell: powershell + - name: Build + run: .\build\build.ps1 -IncludeRsop -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) -SourcePath (Join-Path $env:GITHUB_WORKSPACE configurationdata) -OutputPath (Join-Path $env:GITHUB_WORKSPACE output) + shell: powershell + - uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: | + .\output\rsop + .\output\policies + - name: Publish + run: .\build\publish.ps1 + shell: powershell + - name: Validate Integration Tests + run: .\build\validate.ps1 -TestType Integration -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) -ProjectRoot $env:GITHUB_WORKSPACE + shell: powershell \ No newline at end of file diff --git a/templates/AppLockerProject/.github/workflows/validate.yml b/templates/AppLockerProject/.github/workflows/validate.yml new file mode 100644 index 0000000..b056ed6 --- /dev/null +++ b/templates/AppLockerProject/.github/workflows/validate.yml @@ -0,0 +1,24 @@ +on: [pull_request] + +jobs: + validate: + + runs-on: windows-latest + + steps: + - uses: actions/checkout@v1 + - name: Install Prerequisites + run: .\build\prerequisites.ps1 -BuildWorker -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) + shell: powershell + - name: Validate Configuration Data + run: .\build\validate.ps1 -TestType ConfigurationData -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) -ProjectRoot $env:GITHUB_WORKSPACE + shell: powershell + - name: Build + run: .\build\build.ps1 -IncludeRsop -DependencyPath (Join-Path $env:GITHUB_WORKSPACE build\requiredModules.psd1) -SourcePath (Join-Path $env:GITHUB_WORKSPACE configurationdata) -OutputPath (Join-Path $env:GITHUB_WORKSPACE output) + shell: powershell + - uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: | + .\output\rsop + .\output\policies diff --git a/templates/AppLockerProject/.gitignore b/templates/AppLockerProject/.gitignore new file mode 100644 index 0000000..f0d0b64 --- /dev/null +++ b/templates/AppLockerProject/.gitignore @@ -0,0 +1,2 @@ +output +testresults.xml \ No newline at end of file diff --git a/templates/AppLockerProject/PSMDInvoke.ps1 b/templates/AppLockerProject/PSMDInvoke.ps1 new file mode 100644 index 0000000..17f4c5c --- /dev/null +++ b/templates/AppLockerProject/PSMDInvoke.ps1 @@ -0,0 +1,5 @@ +param ( + $Path +) + +New-PSMDTemplate -ReferencePath "$PSScriptRoot" -OutPath $Path \ No newline at end of file diff --git a/templates/AppLockerProject/PSMDTemplate.ps1 b/templates/AppLockerProject/PSMDTemplate.ps1 new file mode 100644 index 0000000..b5ec0e6 --- /dev/null +++ b/templates/AppLockerProject/PSMDTemplate.ps1 @@ -0,0 +1,11 @@ +@{ + TemplateName = 'AppLockerProject' + Version = "1.0.0" + AutoIncrementVersion = $true + Tags = 'module','psframework', 'applocker' + Author = 'Jan-Hendrik Peters' + Description = 'PowerShell Framework based AppLocker CI template' + Exclusions = @("PSMDInvoke.ps1", ".PSMDDependency") # Contains list of files - relative path to root - to ignore when building the template + Scripts = @{ } + NoFolder = $true +} \ No newline at end of file diff --git a/templates/AppLockerProject/azurepipelines.yml b/templates/AppLockerProject/azurepipelines.yml new file mode 100644 index 0000000..363cb30 --- /dev/null +++ b/templates/AppLockerProject/azurepipelines.yml @@ -0,0 +1,91 @@ +trigger: + branches: + include: + - main + +jobs: + - job: Build + displayName: 'Build AppLocker Artifacts' + pool: + name: Default + steps: + - task: PowerShell@2 + name: prereq + displayName: 'Download prerequisites' + inputs: + filePath: '.\build\prerequisites.ps1' + arguments: '-DependencyPath (Join-Path $(Build.SourcesDirectory) build\requiredModules.psd1)' + - task: PowerShell@2 + name: validateyaml + displayName: Validate Configuration Data + inputs: + filePath: '.\build\validate.ps1' + arguments: '-TestType ConfigurationData -DependencyPath (Join-Path $(Build.SourcesDirectory) build\requiredModules.psd1) -ProjectRoot $(Build.SourcesDirectory)' + - task: PowerShell@2 + name: buildpolicy + displayName: Build policy XML + inputs: + filePath: '.\build\build.ps1' + arguments: '-IncludeRsop -DependencyPath (Join-Path $(Build.SourcesDirectory) build\requiredModules.psd1) -SourcePath (Join-Path $(Build.SourcesDirectory) configurationdata) -OutputPath (Join-Path $(Build.SourcesDirectory) output)' + - task: PublishBuildArtifacts@1 + displayName: 'Publish Policy XML Files' + inputs: + PathtoPublish: 'output/Policies' + ArtifactName: Policies + - task: PublishBuildArtifacts@1 + displayName: 'Publish Policy RSOP Files' + inputs: + PathtoPublish: 'output/Rsop' + ArtifactName: Rsop + - task: PublishTestResults@2 + displayName: 'Publish Configuration Data Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: 'tests/testresults.xml' + mergeTestResults: true + failTaskOnFailedTests: true + testRunTitle: 'Configuration Data Tests' + - deployment: Prod + dependsOn: Build + displayName: Prod Deployment + environment: Prod + pool: + name: Default + strategy: + runOnce: + deploy: + steps: + - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact: Rsop' + inputs: + buildType: 'current' + artifactName: Rsop + downloadPath: $(Build.SourcesDirectory) + - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact: Policies' + inputs: + buildType: 'current' + artifactName: Policies + downloadPath: $(Build.SourcesDirectory) + - task: PowerShell@2 + name: publishpolicies + displayName: Publish policies + inputs: + filePath: '.\build\publish.ps1' + arguments: '-DependencyPath (Join-Path $(Build.SourcesDirectory) build\requiredModules.psd1) -SourcePath (Join-Path $(Build.SourcesDirectory) configurationdata) -OutputPath (Join-Path $(Build.SourcesDirectory) output)' + - task: PowerShell@2 + name: validateintegration + displayName: Validate Integration + inputs: + filePath: '.\build\validate.ps1' + arguments: '-TestType Integration -DependencyPath (Join-Path $(Build.SourcesDirectory) build\requiredModules.psd1) -ProjectRoot $(Build.SourcesDirectory)' + - task: PublishTestResults@2 + displayName: 'Publish Integration Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: 'tests/testresults.xml' + mergeTestResults: true + failTaskOnFailedTests: true + testRunTitle: 'Integration Tests' diff --git a/templates/AppLockerProject/build/build.ps1 b/templates/AppLockerProject/build/build.ps1 new file mode 100644 index 0000000..5f3dc56 --- /dev/null +++ b/templates/AppLockerProject/build/build.ps1 @@ -0,0 +1,70 @@ + +param +( + [string] + $DependencyPath = (Resolve-Path "$PSScriptRoot\requiredModules.psd1").Path, + + [string] + $SourcePath = "$PSScriptRoot\..\configurationdata", + + [string] + $OutputPath = "$PSScriptRoot\..\output", + + [switch] + $IncludeRsop +) + +$psdependConfig = Import-PowerShellDataFile -Path $DependencyPath +$modPath = Resolve-Path -Path $psdependConfig.PSDependOptions.Target +$modOld = $env:PSModulePath +$pathSeparator = [System.IO.Path]::PathSeparator +$env:PSModulePath = "$modPath$pathSeparator$modOld" + +$SourcePath = Resolve-Path -Path $SourcePath -ErrorAction Stop +$OutputPath = if (-not (Resolve-Path -Path $OutputPath -ErrorAction SilentlyContinue)) +{ + (New-Item -Path $OutputPath -ItemType Directory -Force).FullName +} +else +{ + Resolve-Path -Path $OutputPath +} + +$rsopPath = Join-Path -Path $OutputPath -ChildPath rsop +$policyPath = Join-Path -Path $OutputPath -ChildPath policies +if (-not (Test-Path -Path $rsopPath)) +{ + $null = New-Item -Path $rsopPath -ItemType Directory -Force +} + +if (-not (Test-Path -Path $policyPath)) +{ + $null = New-Item -Path $policyPath -ItemType Directory -Force +} + +if (Get-DatumRsopCache) +{ + Clear-DatumRsopCache +} + +$datum = New-DatumStructure -DefinitionFile (Join-Path $SourcePath Datum.yml) +$rsops = Get-DatumRsop $datum (Get-DatumNodesRecursive -AllDatumNodes $Datum.AllNodes) +$rsops | Export-AlfXml -Path $policyPath + +if (-not $IncludeRsop) +{ + $env:PSModulePath = $modOld + return +} + +foreach ($rsop in $rsops) +{ + $domainPath = Join-Path -Path $rsopPath -ChildPath $rsop.Domain + if (-not (Test-Path -Path $domainPath)) + { + $null = New-Item -Path $domainPath -ItemType Directory -Force + } + $rsop | ConvertTo-Yaml -OutFile (Join-Path -Path $domainPath -ChildPath "$($rsop.PolicyName).yml") -Force +} + +$env:PSModulePath = $modOld diff --git a/templates/AppLockerProject/build/prerequisites.ps1 b/templates/AppLockerProject/build/prerequisites.ps1 new file mode 100644 index 0000000..188c977 --- /dev/null +++ b/templates/AppLockerProject/build/prerequisites.ps1 @@ -0,0 +1,28 @@ +param +( + [string] + $DependencyPath = (Resolve-Path "$PSScriptRoot\requiredModules.psd1").Path, + + [switch] + $BuildWorker +) + +$psdependConfig = Import-PowerShellDataFile -Path $DependencyPath + +if ($BuildWorker.IsPresent) +{ + $null = Get-PackageProvider -Name NuGet -ForceBootstrap + + Install-Module -Force -Name PackageManagement, PowerShellGet -Repository $psdependConfig.PSDependOptions.Parameters.Repository -Scope CurrentUser + + Remove-Module -Name PowerShellGet -ErrorAction SilentlyContinue -Force + Remove-Module -Name PackageManagement -ErrorAction SilentlyContinue -Force + Import-Module -Force -Name PowerShellGet + Import-Module -Force -Name PackageManagement + + $null = Install-WindowsFeature -Name GPMC +} + +Save-Module -Name PSDepend -Repository $psdependConfig.PSDependOptions.Parameters.Repository -Path $psdependConfig.PSDependOptions.Target -Force +Import-Module -Name (Join-Path -Path $psdependConfig.PSDependOptions.Target -ChildPath PSDepend\*\PSDepend.psd1 -Resolve) +Invoke-PSDepend -Path $DependencyPath -Force diff --git a/templates/AppLockerProject/build/publish.ps1 b/templates/AppLockerProject/build/publish.ps1 new file mode 100644 index 0000000..b9a39f0 --- /dev/null +++ b/templates/AppLockerProject/build/publish.ps1 @@ -0,0 +1,63 @@ +param +( + [string] + $DependencyPath = (Resolve-Path "$PSScriptRoot\requiredModules.psd1").Path, + + [string] + $OutputPath = (Resolve-Path "$PSScriptRoot\..\output").Path, + + [string] + $SourcePath = "$PSScriptRoot\..\configurationdata" +) + +$psdependConfig = Import-PowerShellDataFile -Path $DependencyPath +$modPath = Resolve-Path -Path $psdependConfig.PSDependOptions.Target +$modOld = $env:PSModulePath +$pathSeparator = [System.IO.Path]::PathSeparator +$env:PSModulePath = "$modPath$pathSeparator$modOld" +$datum = New-DatumStructure -DefinitionFile (Join-Path $SourcePath Datum.yml) +[hashtable[]] $rsops = Get-DatumRsop $datum (Get-DatumNodesRecursive -AllDatumNodes $Datum.AllNodes) + +foreach ($policy in (Get-ChildItem -Path (Join-Path -Path $OutputPath -ChildPath Policies) -Recurse -Filter *.xml)) +{ + $searcher = [adsisearcher]::new() + $searcher.Filter = "(&(objectClass=groupPolicyContainer)(displayName=$($policy.BaseName)))" + $policyFound = $searcher.FindOne() + + if (-not $policyFound) + { + $null = New-GPO -Name $policy.BaseName -Comment "Auto-updated applocker policy" -Domain $policy.Directory.Name + } + + $rsop = $rsops | Where-Object { $_['PolicyName'] -eq $policy.BaseName } + foreach ($link in $rsop.Links) + { + $param = @{ + Name = $rsop.PolicyName + Target = $link.OrgUnitDn + Domain = $policy.Directory.Name + Confirm = $false + } + + if ($rsop.ContainsKey('Enabled')) + { + $param['LinkEnabled'] = $link.Enabled + } + if ($rsop.ContainsKey('Enforced')) + { + $param['Enforced'] = $link.Enforced + } + if ($rsop.ContainsKey('Order')) + { + $param['Order'] = $link.Order + } + + Set-GPLink @param + } + + $policyFound = $searcher.FindOne() + + Set-AppLockerPolicy -XmlPolicy $policy.FullName -Ldap $policyFound.Path +} + +$env:PSModulePath = $modOld diff --git a/templates/AppLockerProject/build/requiredModules.psd1 b/templates/AppLockerProject/build/requiredModules.psd1 new file mode 100644 index 0000000..42084ea --- /dev/null +++ b/templates/AppLockerProject/build/requiredModules.psd1 @@ -0,0 +1,18 @@ +@{ + PSDependOptions = @{ + AddToPath = $false + Target = 'output\RequiredModules' + Parameters = @{ + Repository = 'PSGallery' + AllowPreRelease = $true + } + } + + 'powershell-yaml' = '0.4.7' + PSScriptAnalyzer = '1.21.0' + Pester = '5.4.1' + 'Sampler.DscPipeline' = '0.2.0-preview0015' # Unfortunately still in preview + Datum = '0.40.1' + 'Datum.InvokeCommand' = '0.3.0' + AppLockerFoundry = '1.1.5' +} diff --git a/templates/AppLockerProject/build/validate.ps1 b/templates/AppLockerProject/build/validate.ps1 new file mode 100644 index 0000000..5503c46 --- /dev/null +++ b/templates/AppLockerProject/build/validate.ps1 @@ -0,0 +1,37 @@ +[CmdletBinding()] +param +( + [string] + $DependencyPath = (Resolve-Path "$PSScriptRoot\requiredModules.psd1").Path, + + [string] + $ProjectRoot = (Resolve-Path "$PSScriptRoot\..").Path, + + [ValidateSet('Unit', 'ConfigurationData', 'Integration')] + [string] + $TestType +) + +$psdependConfig = Import-PowerShellDataFile -Path $DependencyPath +$modPath = Resolve-Path -Path $psdependConfig.PSDependOptions.Target +$modOld = $env:PSModulePath +$pathSeparator = [System.IO.Path]::PathSeparator +$env:PSModulePath = "$modPath$pathSeparator$modOld" + +Import-Module Pester -Force -ErrorAction Stop -MinimumVersion 5.0.0 + +$global:testroot = Join-Path $ProjectRoot tests +$po = [PesterConfiguration]::New() +$po.Run.Path = Join-Path $global:testroot $TestType +$po.Run.PassThru = $true +$po.Output.Verbosity = 'Detailed' +$po.TestResult.Enabled = $true +$po.TestResult.OutputPath = Join-Path $global:testroot 'testresults.xml' +$po.TestResult.OutputFormat = 'NUnit2.5' + +$result = Invoke-Pester -Configuration $po +$env:PSModulePath = $modOld + +if ($result.FailedCount -gt 0) { + throw "Pester tests failed" +} diff --git a/templates/AppLockerProject/configurationdata/Apps/Git.yml b/templates/AppLockerProject/configurationdata/Apps/Git.yml new file mode 100644 index 0000000..0f0de16 --- /dev/null +++ b/templates/AppLockerProject/configurationdata/Apps/Git.yml @@ -0,0 +1,30 @@ +Configurations: + - RuleCollections + +RuleCollections: + Exe: + EnforcementMode: AuditOnly + Rules: + - Name: Allow git.exe + Description: Allow all users to run git.exe + Path: '%PROGRAMFILES%\git\cmd\git.exe' + UserOrGroupSid: S-1-1-0 + Action: Allow + - Name: Allow specific hashed git + Description: Allow all users to run git.exe that has a specific hash + Type: SHA256 + Data: "0xC659F4712B60C25E86A927E0C0121C301075F2D0754506140F8B9812B5FDBA3C" + SourceFileName: git.exe + SourceFileLength: 45104 + UserOrGroupSid: S-1-1-0 + Action: Allow + - Name: Allow signed git + Description: Allow all users to run git.exe that was signed by a specific publisher + PublisherName: O=JOHANNES SCHINDELIN, S=NORDRHEIN-WESTFALEN, C=DE + ProductName: GIT + BinaryName: GIT.EXE + BinaryVersionRange: + LowSection: 2.40.1.1 + HighSection: 2.40.1.1 + UserOrGroupSid: S-1-1-0 + Action: Allow diff --git a/templates/AppLockerProject/configurationdata/Datum.yml b/templates/AppLockerProject/configurationdata/Datum.yml new file mode 100644 index 0000000..86cb28b --- /dev/null +++ b/templates/AppLockerProject/configurationdata/Datum.yml @@ -0,0 +1,82 @@ +ResolutionPrecedence: + - AllNodes\$($Node.Domain)\$($Node.PolicyName) + - '[x= { $Node.Apps | Foreach-Object {"Apps\$_"} } =]' + - Domains\$($Node.Domain) + - Generics\Windows + +DatumHandlersThrowOnError: true +DatumHandlers: + Datum.InvokeCommand::InvokeCommand: + SkipDuringLoad: true + +default_lookup_options: MostSpecific + +lookup_options: + Configurations: + merge_basetype_array: Unique + + RuleCollections: + merge_hash: deep + RuleCollections\Exe: + merge_hash: deep + RuleCollections\Exe\Rules: + merge_hash_array: UniqueKeyValTuples + merge_options: + tuple_keys: + - Name + RuleCollections\Exe\Rules\Exceptions: + merge_basetype_array: Unique + RuleCollections\Msi: + merge_hash: deep + RuleCollections\Msi\Rules: + merge_hash_array: UniqueKeyValTuples + merge_options: + tuple_keys: + - Name + RuleCollections\Msi\Rules\Exceptions: + merge_basetype_array: Unique + RuleCollections\Dll: + merge_hash: deep + RuleCollections\Dll\Rules: + merge_hash_array: UniqueKeyValTuples + merge_options: + tuple_keys: + - Name + RuleCollections\Dll\Rules\Exceptions: + merge_basetype_array: Unique + RuleCollections\Script: + merge_hash: deep + RuleCollections\Script\Rules: + merge_hash_array: UniqueKeyValTuples + merge_options: + tuple_keys: + - Name + RuleCollections\Script\Rules\Exceptions: + merge_basetype_array: Unique + RuleCollections\Appx: + merge_hash: deep + RuleCollections\Appx\Rules: + merge_hash_array: UniqueKeyValTuples + merge_options: + tuple_keys: + - Name + RuleCollections\Appx\Rules\Exceptions: + merge_basetype_array: Unique + +DatumStructure: + - StoreName: AllNodes + StoreProvider: Datum::File + StoreOptions: + Path: "./Policies" + - StoreName: Generics + StoreProvider: Datum::File + StoreOptions: + Path: "./Generics" + - StoreName: Domains + StoreProvider: Datum::File + StoreOptions: + Path: "./Domains" + - StoreName: Apps + StoreProvider: Datum::File + StoreOptions: + Path: "./Apps" diff --git "a/templates/AppLockerProject/configurationdata/Domains/\303\276domainfqdn\303\276.yml" "b/templates/AppLockerProject/configurationdata/Domains/\303\276domainfqdn\303\276.yml" new file mode 100644 index 0000000..cfcd043 --- /dev/null +++ "b/templates/AppLockerProject/configurationdata/Domains/\303\276domainfqdn\303\276.yml" @@ -0,0 +1,48 @@ +Configurations: + - RuleCollections + +RuleCollections: + Msi: + EnforcementMode: AuditOnly + Rules: + - Name: Allow DLLs (Admin) + Description: Allows members of the local Administrators group to load all DLLs. + Path: "*" + UserOrGroupSid: S-1-5-32-544 + Action: Allow + Dll: + EnforcementMode: AuditOnly + Rules: + - Name: Allow msi in CCMCache (Everyone) + Description: Allows everyone to run installer files in the SCCM cache. + Path: '%WINDIR%\ccmcache\*' + UserOrGroupSid: S-1-1-0 + Action: Allow + - Name: Allow DLLs (Everyone Windir) + Description: Allows Everyone to load DLLs located in the Windows folder. + Path: '%WINDIR%\*' + UserOrGroupSid: S-1-1-0 + Action: Allow + Exceptions: + - '%WINDIR%\SomeFolder' + - '%WINDIR%\AnotherFolder' + - Name: Allow DLLs (Everyone Windir) + Description: Allows Everyone to load DLLs located in the Program Files folder. + Path: '%PROGRAMFILES%\*' + UserOrGroupSid: S-1-1-0 + Action: Allow + - Name: Prohibit PowerShell 2 + Description: Explicitly deny signed DLLs needed for PowerShell v2 + Action: Deny + UserOrGroupSid: S-1-1-0 + PublisherName: O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US + ProductName: MICROSOFT (R) WINDOWS (R) OPERATING SYSTEM + BinaryName: SYSTEM.MANAGEMENT.AUTOMATION.DLL + BinaryVersionRange: + LowSection: "*" + HighSection: 9.9.9.9 + - Name: Prohibit PowerShell 2 unsigned JIT + Description: Explicitly deny signed DLLs needed for PowerShell v2 + Action: Deny + UserOrGroupSid: S-1-1-0 + Path: '%WINDIR%\assembly\NativeImages_v2.0.50727_32\System.Management.A#\*' diff --git a/templates/AppLockerProject/configurationdata/Generics/Windows.yml b/templates/AppLockerProject/configurationdata/Generics/Windows.yml new file mode 100644 index 0000000..c559f35 --- /dev/null +++ b/templates/AppLockerProject/configurationdata/Generics/Windows.yml @@ -0,0 +1,22 @@ +Configurations: + - RuleCollections + +RuleCollections: + Dll: + EnforcementMode: AuditOnly + Rules: + - Name: Prohibit PowerShell 2 + Description: Explicitly deny signed DLLs needed for PowerShell v2 + Action: Deny + UserOrGroupSid: S-1-1-0 + PublisherName: O=MICROSOFT CORPORATION, L=REDMOND, S=WASHINGTON, C=US + ProductName: MICROSOFT (R) WINDOWS (R) OPERATING SYSTEM + BinaryName: SYSTEM.MANAGEMENT.AUTOMATION.DLL + BinaryVersionRange: + LowSection: "*" + HighSection: 9.9.9.9 + - Name: Prohibit PowerShell 2 unsigned JIT + UserOrGroupSid: S-1-1-0 + Description: Explicitly deny signed DLLs needed for PowerShell v2 + Action: Deny + Path: '%WINDIR%\assembly\NativeImages_v2.0.50727_32\System.Management.A#\*' diff --git "a/templates/AppLockerProject/configurationdata/Policies/\303\276domainfqdn\303\276/Pol1.yml" "b/templates/AppLockerProject/configurationdata/Policies/\303\276domainfqdn\303\276/Pol1.yml" new file mode 100644 index 0000000..afe4b8d --- /dev/null +++ "b/templates/AppLockerProject/configurationdata/Policies/\303\276domainfqdn\303\276/Pol1.yml" @@ -0,0 +1,14 @@ +PolicyName: "[x={ $Node.Name }=]" +Domain: "[x={ $File.Directory.BaseName } =]" +Links: + - OrgUnitDn: "OU=Prod,DC=contoso,DC=com" + LinkOrder: 1 + Enforced: yes # unspecified, no + Enabled: no # unspecified, yes + - OrgUnitDn: "OU=Test,DC=contoso,DC=com" + LinkOrder: 1 + Enforced: yes # unspecified, no + Enabled: yes # unspecified, no +Apps: + - Git + - Obs diff --git a/templates/AppLockerProject/configurationdata/readme.md b/templates/AppLockerProject/configurationdata/readme.md new file mode 100644 index 0000000..309441e --- /dev/null +++ b/templates/AppLockerProject/configurationdata/readme.md @@ -0,0 +1,52 @@ +# Configuration Data + +The `configurationdata` directory contains your overall merging configuration `Datum.yml` as +individual folders described in your merging configuration's Resolution Precedence. This +template assumes: + +```yaml +ResolutionPrecedence: + - AllNodes\$($Node.PolicyName) + - '[x= { $Node.Apps | Foreach-Object {"Apps\$_"} } =]' + - Domains\$($Node.Domain) + - Generics\Windows +``` + +Generic Windows settings are applied first. Those are merged with all domain-specific +settings, taking into consideration which domain the policy is assigned to. Those are then +merged with all App-specific configurations that a policy should contain, and lastly +the policy itself adds its own specific settings like the policy name. + +## Apps + +The idea is to describe each app in a way that AppLocker knows about all required +binaries. Examine the sample App `Git` to learn more. + +## Domains + +The idea is to describe content that is relevant for each domain. + +## Policies + +Grouped by the domain, each policy should be a single yml file that contains +the Name, Domain, optional GPLinks and subscribed Apps for that policy. + +Through the use of Datum.InvokeCommand, we can run scripts during the build +process, or rather: When generating the RSOP for the build. + +```yaml +PolicyName: "[x={ $Node.Name }=]" +Domain: "[x={ $File.Directory.BaseName } =]" +Links: + - OrgUnitDn: "OU=Prod,DC=contoso,DC=com" + LinkOrder: 1 + Enforced: yes # unspecified, no + Enabled: no # unspecified, yes +Apps: + - Git +``` + +## Generics + +Currently only one generic configuration is recommended: Windows. This sample already +contains the recommended content and disables PowerShell 2. diff --git a/templates/AppLockerProject/readme.md b/templates/AppLockerProject/readme.md new file mode 100644 index 0000000..748c036 --- /dev/null +++ b/templates/AppLockerProject/readme.md @@ -0,0 +1,31 @@ +# þnameþ + +Add your project description here. Configuration data can be generated using +the build dependency `AppLockerFoundry`. + +```powershell +Get-ChildItem -Path "C:\Program Files" -Recurse -Filter *.exe | Get-AlfYamlFileInfo +``` + +## Build and release workflow + +The integrated build workflows for GitHub and Azure DevOps (Server) can be +used out-of-the-box, nearly. If you want to publish your policies in a domain +environment, you will ned to run your build worker with an account +that is capable of updating the required policy objects. This is +due to constraints with the AppLocker cmdlets and their missing capability to specify +alternative credentials. + +The ideal workflow should you need or want to build it your self would look like this: + +1. Ensure prerequisites `.\build\prerequisites.ps1` +1. Validate Configuration Data: `.\build\validate.ps1 -TestType ConfigurationData` +1. Build policies and optional RSOP from configuration data: `.\build\build.ps1 -IncludeRsop` +1. Validate Integration into environment: `.\build\validate.ps1 -TestType Integration` +1. Publish: `.\build\publish.ps1` + +## Advanced setup + +Most CI tools support a concept like Environments to which you can attach certain +gates like a manual approval or a scheduled release in a specific time frame. Play +around with this a little bit to further improve your AppLocker pipeline. diff --git a/templates/AppLockerProject/tests/ConfigurationData/FileIntegrity.tests.ps1 b/templates/AppLockerProject/tests/ConfigurationData/FileIntegrity.tests.ps1 new file mode 100644 index 0000000..7a4d32f --- /dev/null +++ b/templates/AppLockerProject/tests/ConfigurationData/FileIntegrity.tests.ps1 @@ -0,0 +1,28 @@ +BeforeDiscovery { + $yamlFiles = Get-ChildItem -Path "$global:testroot\..\configurationdata" -Filter *.ym*l -Recurse -File | Foreach-Object { + @{ + FullName = $_.FullName + BaseName = $_.BaseName + Name = $_.Name + } + } +} + +Describe "YAML file integrity" { + It " Convert from YAML without errors" -TestCases $yamlFiles { + { Get-Content -Raw -Path $FullName | ConvertFrom-Yaml -ErrorAction Stop } | Should -Not -Throw + } + + It " Contains only valid rule types" -TestCases $yamlFiles { + $types = 'Dll', 'Exe', 'Msi', 'Script', 'Appx' + + $content = Get-Content -Raw -Path $FullName | ConvertFrom-Yaml -ErrorAction SilentlyContinue + + if (-not $content.ContainsKey('RuleCollections')) + { + return + } + + $content.RuleCollections.Keys | Foreach-Object { $_ | Should -BeIn $types } + } +} diff --git a/templates/AppLockerProject/tests/ConfigurationData/Rsop.tests.ps1 b/templates/AppLockerProject/tests/ConfigurationData/Rsop.tests.ps1 new file mode 100644 index 0000000..b4998ba --- /dev/null +++ b/templates/AppLockerProject/tests/ConfigurationData/Rsop.tests.ps1 @@ -0,0 +1,15 @@ +BeforeDiscovery { + if (Get-DatumRsopCache) + { + Clear-DatumRsopCache + } + + $datum = New-DatumStructure -DefinitionFile (Join-Path "$global:testroot\..\configurationdata" Datum.yml) + [hashtable[]] $rsops = (Get-DatumRsop $datum (Get-DatumNodesRecursive -AllDatumNodes $Datum.AllNodes)).RuleCollections.Values.Rules +} + +Describe "RSOP correctness" { + It " Policy rule has SID" -TestCases $rsops { + $UserOrGroupSid | Should -Not -BeNullOrEmpty + } +} diff --git a/templates/AppLockerProject/tests/ConfigurationData/readme.md b/templates/AppLockerProject/tests/ConfigurationData/readme.md new file mode 100644 index 0000000..5373f6f --- /dev/null +++ b/templates/AppLockerProject/tests/ConfigurationData/readme.md @@ -0,0 +1,5 @@ +# Configuration Data Tests + +These tests are a bit special, as they don't really fit into unit or integration. These +tests are supposed to ensure the validitity of your configuration data that in the +end make up your policy definitions. \ No newline at end of file diff --git a/templates/AppLockerProject/tests/Integration/GpoExists.tests.ps1 b/templates/AppLockerProject/tests/Integration/GpoExists.tests.ps1 new file mode 100644 index 0000000..45b5ddb --- /dev/null +++ b/templates/AppLockerProject/tests/Integration/GpoExists.tests.ps1 @@ -0,0 +1,29 @@ +BeforeDiscovery { + if (Get-DatumRsopCache) + { + Clear-DatumRsopCache + } + + $policies = foreach ($file in (Get-ChildItem -Path (Resolve-Path "$global:testroot\..\configurationdata\Policies").Path -Recurse -Filter *.y*ml -File)) + { + @{ + Name = $file.BaseName + Domain = $file.Directory.Name + } + } +} + +Describe "Policy exists" { + It " Policy exists in " -TestCases $policies { + $gpo = Get-GPO -Name $Name -Domain $Domain + $ctx = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new('Domain', $Domain) + $domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($ctx) + $domainDn = $domain.GetDirectoryEntry().DistinguishedName + $appLockerGpo = try + { + Get-AppLockerPolicy -Domain -Ldap "LDAP://CN={$($gpo.Id)},CN=Policies,CN=System,$domainDn" -ErrorAction Stop + } + catch {} # Suppress exception from misbehaving cmdlet that does not like SilentlyContinue + $appLockerGpo.RuleCollections | Should -Not -BeNullOrEmpty + } +} diff --git a/templates/AppLockerProject/tests/Integration/readme.md b/templates/AppLockerProject/tests/Integration/readme.md new file mode 100644 index 0000000..5bc1cc7 --- /dev/null +++ b/templates/AppLockerProject/tests/Integration/readme.md @@ -0,0 +1,4 @@ +# Integration and QA tests + +Put your test scripts here that test in a test environment. For example, +this could include linking and validating your new policies. diff --git a/templates/AppLockerProject/tests/Unit/readme.md b/templates/AppLockerProject/tests/Unit/readme.md new file mode 100644 index 0000000..66cddf8 --- /dev/null +++ b/templates/AppLockerProject/tests/Unit/readme.md @@ -0,0 +1,3 @@ +# Unit tests + +Test your functional units here! \ No newline at end of file