mirror of https://github.com/ansible/ansible.git
Add support for Windows App Control/WDAC (#84898)
* Add support for Windows App Control/WDAC
Adds preview support for Windows App Control, formerly known as WDAC.
This is a tech preview feature and is designed to test out improvements
needed in future versions of Ansible.
* Use psd1 and parse it through the Ast to avoid any unexpected execution results
* Add tests for various manifest permutations
* Ignore test shebang failure
* Apply suggestions from code review
Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>
* Use more flexible test expectations
* Add type annotations for shell functions
---------
Co-authored-by: Matt Davis <6775756+nitzmahone@users.noreply.github.com>
(cherry picked from commit 75f7b2267d)
pull/85255/head
parent
895af10b99
commit
880b584124
@ -0,0 +1,9 @@
|
|||||||
|
minor_changes:
|
||||||
|
- >-
|
||||||
|
windows - Added support for ``#AnsibleRequires -Wrapper`` to request a PowerShell module be run through the
|
||||||
|
execution wrapper scripts without any module utils specified.
|
||||||
|
- >-
|
||||||
|
windows - Added support for running signed modules and scripts with a Windows host protected by Windows App
|
||||||
|
Control/WDAC. This is a tech preview and the interface may be subject to change.
|
||||||
|
- >-
|
||||||
|
windows - Script modules will preserve UTF-8 encoding when executing the script.
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
# (c) 2025 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$Path
|
||||||
|
)
|
||||||
|
|
||||||
|
$userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
|
||||||
|
if ($Path -eq '~') {
|
||||||
|
$userProfile
|
||||||
|
}
|
||||||
|
elseif ($Path.StartsWith(('~\'))) {
|
||||||
|
Join-Path -Path $userProfile -ChildPath $Path.Substring(2)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$Path
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
# (c) 2025 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$Directory,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$Name
|
||||||
|
)
|
||||||
|
|
||||||
|
$path = [Environment]::ExpandEnvironmentVariables($Directory)
|
||||||
|
$tmp = New-Item -Path $path -Name $Name -ItemType Directory
|
||||||
|
$tmp.FullName
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
shippable/windows/group1
|
||||||
|
shippable/windows/smoketest
|
||||||
|
needs/target/collection
|
||||||
|
needs/target/setup_remote_tmp_dir
|
||||||
|
destructive # modifies files in the ansible installation dir
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ansible_collections.ns.col.plugins.module_utils.CSharpSigned
|
||||||
|
{
|
||||||
|
public class TestClass
|
||||||
|
{
|
||||||
|
public static string TestMethod(string input)
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ansible_collections.ns.col.plugins.module_utils.CSharpUnsigned
|
||||||
|
{
|
||||||
|
public class TestClass
|
||||||
|
{
|
||||||
|
public static string TestMethod(string input)
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
Function Test-PwshSigned {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Tests a signed collection pwsh util.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param ()
|
||||||
|
|
||||||
|
@{
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Test-PwshSigned
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
Function Test-PwshUnsigned {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Tests an unsigned collection pwsh util.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param ()
|
||||||
|
|
||||||
|
@{
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Export-ModuleMember -Function Test-PwshUnsigned
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
@{
|
||||||
|
test = 'inline_signed'
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $complex_args.input
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
@{
|
||||||
|
test = 'inline_signed_not_trusted'
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $complex_args.input
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
using namespace Ansible.Basic
|
||||||
|
using namespace System.Management.Automation.Language
|
||||||
|
using namespace Invalid.Namespace.That.Does.Not.Exist
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
|
||||||
|
$module = [AnsibleModule]::Create($args, @{ options = @{} })
|
||||||
|
|
||||||
|
$module.Result.module_using_namespace = [Parser].FullName
|
||||||
|
|
||||||
|
# Verifies the module is run in its own script scope
|
||||||
|
$var = 'foo'
|
||||||
|
$module.Result.script_var = $script:var
|
||||||
|
|
||||||
|
$missingUsingNamespace = $false
|
||||||
|
try {
|
||||||
|
# exec_wrapper does 'using namespace System.IO'. This ensures that this
|
||||||
|
# hasn't persisted to the module scope and it has it's own set of using
|
||||||
|
# types.
|
||||||
|
$null = [File]::Exists('test')
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$missingUsingNamespace = $true
|
||||||
|
}
|
||||||
|
$module.Result.missing_using_namespace = $missingUsingNamespace
|
||||||
|
|
||||||
|
$module.ExitJson()
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
@{
|
||||||
|
test = 'signed'
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $complex_args.input
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#AnsibleRequires -CSharpUtil ..module_utils.CSharpSigned
|
||||||
|
|
||||||
|
#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
|
||||||
|
#AnsibleRequires -PowerShell ..module_utils.PwshSigned
|
||||||
|
|
||||||
|
# Tests builtin C# util
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{ options = @{} })
|
||||||
|
|
||||||
|
# Tests builtin pwsh util
|
||||||
|
Add-CSharpType -AnsibleModule $module -References @'
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace ns.col.module_utils
|
||||||
|
{
|
||||||
|
public class InlineCSharp
|
||||||
|
{
|
||||||
|
public static string TestMethod(string input)
|
||||||
|
{
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
|
||||||
|
$module.Result.language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
$module.Result.builtin_powershell_util = [ns.col.module_utils.InlineCSharp]::TestMethod("value")
|
||||||
|
$module.Result.csharp_util = [ansible_collections.ns.col.plugins.module_utils.CSharpSigned.TestClass]::TestMethod("value")
|
||||||
|
$module.Result.powershell_util = Test-PwshSigned
|
||||||
|
|
||||||
|
$module.ExitJson()
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
if ($complex_args.should_fail) {
|
||||||
|
throw "exception here"
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
test = 'skipped'
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $complex_args.input
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil ..module_utils.CSharpUnsigned
|
||||||
|
|
||||||
|
@{
|
||||||
|
changed = $false
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
res = [ansible_collections.ns.col.plugins.module_utils.CSharpUnsigned.TestClass]::TestMethod("value")
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#AnsibleRequires -CSharpUtil ..module_utils.CSharpSigned
|
||||||
|
|
||||||
|
#AnsibleRequires -PowerShell Ansible.ModuleUtils.AddType
|
||||||
|
#AnsibleRequires -PowerShell ..module_utils.PwshSigned
|
||||||
|
|
||||||
|
@{
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
builtin_csharp = $null -ne ('Ansible.Basic.AnsibleModule' -as [type])
|
||||||
|
builtin_pwsh = [bool](Get-Command Add-CSharpType -ErrorAction SilentlyContinue)
|
||||||
|
collection_csharp = $null -ne ('ansible_collections.ns.col.plugins.module_utils.CSharpSigned.TestClass' -as [type])
|
||||||
|
collection_pwsh = [bool](Get-Command Test-PwshSigned -ErrorAction SilentlyContinue)
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -PowerShell ..module_utils.PwshUnsigned
|
||||||
|
|
||||||
|
@{
|
||||||
|
changed = $false
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
res = Test-PwshUnsigned
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
@{
|
||||||
|
test = 'unsupported'
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@{
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $args[0]
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
@{
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $args[0]
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
- name: run signed script
|
||||||
|
script: signed.ps1 café
|
||||||
|
register: signed_res
|
||||||
|
|
||||||
|
- name: run unsigned script
|
||||||
|
script: unsigned.ps1 café
|
||||||
|
register: unsigned_res
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
@{
|
||||||
|
test = 'ns.invalid_manifest.module'
|
||||||
|
language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
whoami = [Environment]::UserName
|
||||||
|
ünicode = $complex_args.input
|
||||||
|
} | ConvertTo-Json
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||||
|
#AnsibleRequires -CSharpUtil ansible_collections.ns.col.plugins.module_utils.CSharpSigned
|
||||||
|
#AnsibleRequires -PowerShell ansible_collections.ns.col.plugins.module_utils.PwshSigned
|
||||||
|
|
||||||
|
# Tests signed util in another trusted collection works
|
||||||
|
|
||||||
|
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{ options = @{} })
|
||||||
|
|
||||||
|
$module.Result.language_mode = $ExecutionContext.SessionState.LanguageMode.ToString()
|
||||||
|
$module.Result.csharp_util = [ansible_collections.ns.col.plugins.module_utils.CSharpSigned.TestClass]::TestMethod("value")
|
||||||
|
$module.Result.powershell_util = Test-PwshSigned
|
||||||
|
|
||||||
|
$module.ExitJson()
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
- name: create manifest file
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: '{{ manifest_file }}'
|
||||||
|
dest: '{{ local_tmp_dir }}/ansible_collections/ns/invalid_manifest/meta/powershell_signatures.psd1'
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: sign manifest file
|
||||||
|
ansible.builtin.script: >-
|
||||||
|
Set-ManifestSignature.ps1
|
||||||
|
-Path {{ local_tmp_dir ~ "/ansible_collections/ns/invalid_manifest/meta/powershell_signatures.psd1" | quote }}
|
||||||
|
-CertPath {{ local_tmp_dir ~ "/" ~ (cert_name | default("wdac-signing")) ~ ".pfx" | quote }}
|
||||||
|
-CertPass {{ cert_pw | quote }}
|
||||||
|
environment:
|
||||||
|
NO_COLOR: '1'
|
||||||
|
delegate_to: localhost
|
||||||
@ -0,0 +1,419 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
# 0.5.0 fixed BOM-less encoding issues with Unicode
|
||||||
|
#Requires -Modules @{ ModuleName = 'OpenAuthenticode'; ModuleVersion = '0.5.0' }
|
||||||
|
|
||||||
|
using namespace System.Collections.Generic
|
||||||
|
using namespace System.IO
|
||||||
|
using namespace System.Management.Automation
|
||||||
|
using namespace System.Management.Automation.Language
|
||||||
|
using namespace System.Security.Cryptography.X509Certificates
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$CollectionPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$CertPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$UntrustedCertPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$CertPass
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
Function New-AnsiblePowerShellSignature {
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Creates and signed Ansible content for App Control/WDAC.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
This function will generate the powershell_signatures.psd1 manifest and sign
|
||||||
|
it. The manifest file includes all PowerShell/C# module_utils and
|
||||||
|
PowerShell modules in the collection(s) specified. It will also create the
|
||||||
|
'*.authenticode' signature file for the exec_wrapper.ps1 used inside
|
||||||
|
Ansible itself.
|
||||||
|
|
||||||
|
.PARAMETER Certificate
|
||||||
|
The certificate to use for signing the content.
|
||||||
|
|
||||||
|
.PARAMETER Collection
|
||||||
|
The collection(s) to sign. This is set to ansible.builtin by default but
|
||||||
|
can be overriden to include other collections like ansible.windows.
|
||||||
|
|
||||||
|
.PARAMETER Skip
|
||||||
|
A list of plugins to skip by the fully qualified name. Plugins skipped will
|
||||||
|
not be included in the signed manifest. This means that modules will be run
|
||||||
|
in CLM mode and module_utils will be skipped entirely.
|
||||||
|
|
||||||
|
The values in the list should be the fully qualified name of the plugin as
|
||||||
|
referenced in Ansible. The value can also optionally include the extension
|
||||||
|
of the file if the FQN is ambigious, e.g. collection util that has both a
|
||||||
|
PowerShell and C# util of the same name.
|
||||||
|
|
||||||
|
Here are some examples for the various content types:
|
||||||
|
|
||||||
|
# Ansible Builtin Modules
|
||||||
|
'ansible.builtin.module_name'
|
||||||
|
|
||||||
|
# Ansible Builtin ModuleUtil
|
||||||
|
'Ansible.ModuleUtils.PowerShellUtil'
|
||||||
|
'Ansible.CSharpUtil'
|
||||||
|
|
||||||
|
# Collection Modules
|
||||||
|
'namespace.name.module_name'
|
||||||
|
|
||||||
|
# Collection ModuleUtils
|
||||||
|
'ansible_collections.namespace.name.plugins.module_utils.PowerShellUtil'
|
||||||
|
'ansible_collections.namespace.name.plugins.module_utils.PowerShellUtil.psm1'
|
||||||
|
|
||||||
|
'ansible_collections.namespace.name.plugins.module_utils.CSharpUtil'
|
||||||
|
'ansible_collections.namespace.name.plugins.module_utils.CSharpUtil.cs'
|
||||||
|
|
||||||
|
.PARAMETER Unsupported
|
||||||
|
A list of plugins to be marked as unsupported in the manifest and will
|
||||||
|
error when being run. List -Skip, the values here are the fully qualified
|
||||||
|
name of the plugin as referenced in Ansible.
|
||||||
|
|
||||||
|
.PARAMETER TimeStampServer
|
||||||
|
Optional authenticode timestamp server to use when signing the content.
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
Signs just the content included in Ansible.
|
||||||
|
|
||||||
|
$cert = [X509Certificate2]::new("wdac-cert.pfx", "password")
|
||||||
|
New-AnsiblePowerShellSignature -Certificate $cert
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
Signs just the content include in Ansible and the ansible.windows collection
|
||||||
|
|
||||||
|
$cert = [X509Certificate2]::new("wdac-cert.pfx", "password")
|
||||||
|
New-AnsiblePowerShellSignature -Certificate $cert -Collection ansible.builtin, ansible.windows
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
Signs just the content in the ansible.windows collection
|
||||||
|
|
||||||
|
$cert = [X509Certificate2]::new("wdac-cert.pfx", "password")
|
||||||
|
New-AnsiblePowerShellSignature -Certificate $cert -Collection ansible.windows
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
Signs content but skips the specified modules and module_utils
|
||||||
|
$skip = @(
|
||||||
|
# Skips the module specified
|
||||||
|
'namespace.name.module'
|
||||||
|
|
||||||
|
# Skips the module_utils specified
|
||||||
|
'ansible_collections.namespace.name.plugins.module_utils.PowerShellUtil'
|
||||||
|
'ansible_collections.namespace.name.plugins.module_utils.CSharpUtil'
|
||||||
|
|
||||||
|
# Skips signing the file specified
|
||||||
|
'ansible_collections.namespace.name.plugins.plugin_utils.powershell.file.ps1'
|
||||||
|
)
|
||||||
|
$cert = [X509Certificate2]::new("wdac-cert.pfx", "password")
|
||||||
|
New-AnsiblePowerShellSignature -Certificate $cert -Collection namespace.name -Skip $skip
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
This function requires Ansible to be installed and available in the PATH so
|
||||||
|
it can find the Ansible installation and collection paths.
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(
|
||||||
|
Mandatory
|
||||||
|
)]
|
||||||
|
[X509Certificate2]
|
||||||
|
$Certificate,
|
||||||
|
|
||||||
|
[Parameter(
|
||||||
|
ValueFromPipeline,
|
||||||
|
ValueFromPipelineByPropertyName
|
||||||
|
)]
|
||||||
|
[string[]]
|
||||||
|
$Collection = "ansible.builtin",
|
||||||
|
|
||||||
|
[Parameter(
|
||||||
|
ValueFromPipelineByPropertyName
|
||||||
|
)]
|
||||||
|
[string[]]
|
||||||
|
$Skip = @(),
|
||||||
|
|
||||||
|
[Parameter(
|
||||||
|
ValueFromPipelineByPropertyName
|
||||||
|
)]
|
||||||
|
[string[]]
|
||||||
|
$Unsupported = @(),
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]
|
||||||
|
$TimeStampServer
|
||||||
|
)
|
||||||
|
|
||||||
|
begin {
|
||||||
|
Write-Verbose "Attempting to get ansible-config dump"
|
||||||
|
$configRaw = ansible-config dump --format json --type base 2>&1
|
||||||
|
if ($LASTEXITCODE) {
|
||||||
|
$err = [ErrorRecord]::new(
|
||||||
|
[Exception]::new("Failed to get Ansible configuration, RC: ${LASTEXITCODE} - $configRaw"),
|
||||||
|
'FailedToGetAnsibleConfiguration',
|
||||||
|
[ErrorCategory]::NotSpecified,
|
||||||
|
$null)
|
||||||
|
$PSCmdlet.ThrowTerminatingError($err)
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $configRaw | ConvertFrom-Json
|
||||||
|
$collectionsPaths = @($config | Where-Object name -EQ 'COLLECTIONS_PATHS' | ForEach-Object value)
|
||||||
|
Write-Verbose "Collections paths to be searched: [$($collectionsPaths -join ":")]"
|
||||||
|
|
||||||
|
$signParams = @{
|
||||||
|
Certificate = $Certificate
|
||||||
|
HashAlgorithm = 'SHA256'
|
||||||
|
}
|
||||||
|
if ($TimeStampServer) {
|
||||||
|
$signParams.TimeStampServer = $TimeStampServer
|
||||||
|
}
|
||||||
|
|
||||||
|
$checked = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
||||||
|
|
||||||
|
Function New-HashEntry {
|
||||||
|
[OutputType([PSObject])]
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory, ValueFromPipeline)]
|
||||||
|
[FileInfo]
|
||||||
|
$File,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[AllowEmptyString()]
|
||||||
|
[string]
|
||||||
|
$PluginBase,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]
|
||||||
|
$Unsupported = @(),
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[AllowEmptyCollection()]
|
||||||
|
[string[]]
|
||||||
|
$Skip = @()
|
||||||
|
)
|
||||||
|
|
||||||
|
process {
|
||||||
|
$nameWithoutExt = [string]::IsNullOrEmpty($PluginBase) ? $File.BaseName : "$PluginBase.$($File.BaseName)"
|
||||||
|
$nameWithExt = "$nameWithoutExt$($File.Extension)"
|
||||||
|
|
||||||
|
$mode = 'Trusted'
|
||||||
|
if ($nameWithoutExt -in $Skip -or $nameWithExt -in $Skip) {
|
||||||
|
Write-Verbose "Skipping plugin '$nameWithExt' as it is in the supplied skip list"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elseif ($nameWithoutExt -in $Unsupported -or $nameWithExt -in $Unsupported) {
|
||||||
|
Write-Verbose "Marking plugin '$nameWithExt' as unsupported as it is in the unsupported list"
|
||||||
|
$mode = 'Unsupported'
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Hashing plugin '$nameWithExt'"
|
||||||
|
$hash = Get-FileHash -LiteralPath $File.FullName -Algorithm SHA256
|
||||||
|
[PSCustomObject]@{
|
||||||
|
Name = $nameWithExt
|
||||||
|
Hash = $hash.Hash
|
||||||
|
Mode = $mode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
process {
|
||||||
|
$newHashParams = @{
|
||||||
|
Skip = $Skip
|
||||||
|
Unsupported = $Unsupported
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($c in $Collection) {
|
||||||
|
try {
|
||||||
|
if (-not $checked.Add($c)) {
|
||||||
|
Write-Verbose "Skipping already processed collection $c"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaPath = $null
|
||||||
|
$pathsToSign = [List[FileInfo]]::new()
|
||||||
|
$hashedPaths = [List[PSObject]]::new()
|
||||||
|
|
||||||
|
if ($c -eq 'ansible.builtin') {
|
||||||
|
Write-Verbose "Attempting to get Ansible installation path"
|
||||||
|
$ansiblePath = python -c "import ansible; print(ansible.__file__)" 2>&1
|
||||||
|
if ($LASTEXITCODE) {
|
||||||
|
throw "Failed to find Ansible installation path, RC: ${LASTEXITCODE} - $ansiblePath"
|
||||||
|
}
|
||||||
|
|
||||||
|
$ansibleBase = Split-Path -Path $ansiblePath -Parent
|
||||||
|
$metaPath = [Path]::Combine($ansibleBase, 'config')
|
||||||
|
|
||||||
|
$execWrapper = Get-Item -LiteralPath ([Path]::Combine($ansibleBase, 'executor', 'powershell', 'exec_wrapper.ps1'))
|
||||||
|
$pathsToSign.Add($execWrapper)
|
||||||
|
|
||||||
|
$ansiblePwshContent = [PSObject[]]@(
|
||||||
|
# These are needed for Ansible and cannot be skipped
|
||||||
|
Get-ChildItem -Path ([Path]::Combine($ansibleBase, 'executor', 'powershell', '*.ps1')) -Exclude "bootstrap_wrapper.ps1" |
|
||||||
|
New-HashEntry -PluginBase "ansible.executor.powershell"
|
||||||
|
|
||||||
|
# Builtin utils are special where the filename is their FQN
|
||||||
|
Get-ChildItem -Path ([Path]::Combine($ansibleBase, 'module_utils', 'csharp', '*.cs')) |
|
||||||
|
New-HashEntry -PluginBase "" @newHashParams
|
||||||
|
Get-ChildItem -Path ([Path]::Combine($ansibleBase, 'module_utils', 'powershell', '*.psm1')) |
|
||||||
|
New-HashEntry -PluginBase "" @newHashParams
|
||||||
|
|
||||||
|
Get-ChildItem -Path ([Path]::Combine($ansibleBase, 'modules', '*.ps1')) |
|
||||||
|
New-HashEntry -PluginBase $c @newHashParams
|
||||||
|
)
|
||||||
|
$hashedPaths.AddRange($ansiblePwshContent)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Verbose "Attempting to get collection path for $c"
|
||||||
|
$namespace, $name, $remaining = $c.ToLowerInvariant() -split '\.'
|
||||||
|
if (-not $name -or $remaining) {
|
||||||
|
throw "Invalid collection name '$c', must be in the format 'namespace.name'"
|
||||||
|
}
|
||||||
|
|
||||||
|
$foundPath = $null
|
||||||
|
foreach ($path in $collectionsPaths) {
|
||||||
|
$collectionPath = [Path]::Combine($path, 'ansible_collections', $namespace, $name)
|
||||||
|
|
||||||
|
Write-Verbose "Checking if collection $c exists in '$collectionPath'"
|
||||||
|
if (Test-Path -LiteralPath $collectionPath) {
|
||||||
|
$foundPath = $collectionPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $foundPath) {
|
||||||
|
throw "Failed to find collection path for $c"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Using collection path '$foundPath' for $c"
|
||||||
|
|
||||||
|
$metaPath = [Path]::Combine($foundPath, 'meta')
|
||||||
|
|
||||||
|
$collectionPwshContent = [PSObject[]]@(
|
||||||
|
$utilPath = [Path]::Combine($foundPath, 'plugins', 'module_utils')
|
||||||
|
if (Test-Path -LiteralPath $utilPath) {
|
||||||
|
Get-ChildItem -LiteralPath $utilPath | Where-Object Extension -In '.cs', '.psm1' |
|
||||||
|
New-HashEntry -PluginBase "ansible_collections.$c.plugins.module_utils" @newHashParams
|
||||||
|
}
|
||||||
|
|
||||||
|
$modulePath = [Path]::Combine($foundPath, 'plugins', 'modules')
|
||||||
|
if (Test-Path -LiteralPath $modulePath) {
|
||||||
|
Get-ChildItem -LiteralPath $modulePath | Where-Object Extension -EQ '.ps1' |
|
||||||
|
New-HashEntry -PluginBase $c @newHashParams
|
||||||
|
}
|
||||||
|
)
|
||||||
|
$hashedPaths.AddRange($collectionPwshContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $metaPath)) {
|
||||||
|
Write-Verbose "Creating meta path '$metaPath'"
|
||||||
|
New-Item -Path $metaPath -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$manifest = @(
|
||||||
|
'@{'
|
||||||
|
' Version = 1'
|
||||||
|
' HashList = @('
|
||||||
|
foreach ($content in $hashedPaths) {
|
||||||
|
# To avoid encoding problems with Authenticode and non-ASCII
|
||||||
|
# characters, we escape them as Unicode code points. We also
|
||||||
|
# escape some ASCII control characters that can cause escaping
|
||||||
|
# problems like newlines.
|
||||||
|
$escapedName = [Regex]::Replace(
|
||||||
|
$content.Name,
|
||||||
|
'([^\u0020-\u007F])',
|
||||||
|
{ '\u{0:x4}' -f ([uint16][char]$args[0].Value) })
|
||||||
|
|
||||||
|
$escapedHash = [CodeGeneration]::EscapeSingleQuotedStringContent($content.Hash)
|
||||||
|
$escapedMode = [CodeGeneration]::EscapeSingleQuotedStringContent($content.Mode)
|
||||||
|
" # $escapedName"
|
||||||
|
" @{"
|
||||||
|
" Hash = '$escapedHash'"
|
||||||
|
" Mode = '$escapedMode'"
|
||||||
|
" }"
|
||||||
|
}
|
||||||
|
' )'
|
||||||
|
'}'
|
||||||
|
) -join "`n"
|
||||||
|
$manifestPath = [Path]::Combine($metaPath, 'powershell_signatures.psd1')
|
||||||
|
Write-Verbose "Creating and signing manifest for $c at '$manifestPath'"
|
||||||
|
Set-Content -LiteralPath $manifestPath -Value $manifest -NoNewline
|
||||||
|
|
||||||
|
Set-OpenAuthenticodeSignature -LiteralPath $manifestPath @signParams
|
||||||
|
|
||||||
|
$pathsToSign | ForEach-Object -Process {
|
||||||
|
$tempPath = Join-Path $_.DirectoryName "$($_.BaseName)_tmp.ps1"
|
||||||
|
$_ | Copy-Item -Destination $tempPath -Force
|
||||||
|
|
||||||
|
try {
|
||||||
|
Write-Verbose "Signing script '$($_.FullName)'"
|
||||||
|
Set-OpenAuthenticodeSignature -LiteralPath $tempPath @signParams
|
||||||
|
|
||||||
|
$signedContent = Get-Content -LiteralPath $tempPath -Raw
|
||||||
|
$sigIndex = $signedContent.LastIndexOf("`r`n# SIG # Begin signature block`r`n")
|
||||||
|
if ($sigIndex -eq -1) {
|
||||||
|
throw "Failed to find signature block in $($_.FullName)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ignore the first and last \r\n when extracting the signature
|
||||||
|
$sigIndex += 2
|
||||||
|
$signature = $signedContent.Substring($sigIndex, $signedContent.Length - $sigIndex - 2)
|
||||||
|
$sigPath = Join-Path $_.DirectoryName "$($_.Name).authenticode"
|
||||||
|
|
||||||
|
Write-Verbose "Creating signature file at '$sigPath'"
|
||||||
|
Set-Content -LiteralPath $sigPath -Value $signature -NoNewline
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$tempPath | Remove-Item -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$_.ErrorDetails = "Failed to process collection ${c}: $_"
|
||||||
|
$PSCmdlet.WriteError($_)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cert = [X509Certificate2]::new($CertPath, $CertPass)
|
||||||
|
$untrustedCert = [X509Certificate2]::new($UntrustedCertPath, $CertPass)
|
||||||
|
|
||||||
|
$sigParams = @{
|
||||||
|
Certificate = $cert
|
||||||
|
Collection = 'ansible.builtin', 'ansible.windows', 'ns.col', 'ns.module_util_ref'
|
||||||
|
Skip = @(
|
||||||
|
'ns.col.skipped'
|
||||||
|
'ns.col.inline_signed'
|
||||||
|
'ns.col.inline_signed_not_trusted'
|
||||||
|
'ns.col.unsigned_module_with_util'
|
||||||
|
'ansible_collections.ns.col.plugins.module_utils.CSharpUnsigned'
|
||||||
|
'ansible_collections.ns.col.plugins.module_utils.PwshUnsigned'
|
||||||
|
)
|
||||||
|
Unsupported = 'ns.col.unsupported'
|
||||||
|
}
|
||||||
|
New-AnsiblePowerShellSignature @sigParams
|
||||||
|
|
||||||
|
@(
|
||||||
|
"$CollectionPath/plugins/modules/inline_signed.ps1"
|
||||||
|
"$CollectionPath/roles/app_control_script/files/signed.ps1"
|
||||||
|
) | Set-OpenAuthenticodeSignature -Certificate $cert -HashAlgorithm SHA256
|
||||||
|
|
||||||
|
@(
|
||||||
|
"$CollectionPath/plugins/modules/inline_signed_not_trusted.ps1"
|
||||||
|
) | Set-OpenAuthenticodeSignature -Certificate $untrustedCert -HashAlgorithm SHA256
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env pwsh
|
||||||
|
|
||||||
|
# 0.5.0 fixed BOM-less encoding issues with Unicode
|
||||||
|
#Requires -Modules @{ ModuleName = 'OpenAuthenticode'; ModuleVersion = '0.5.0' }
|
||||||
|
|
||||||
|
using namespace System.Security.Cryptography.X509Certificates
|
||||||
|
|
||||||
|
[CmdletBinding()]
|
||||||
|
param (
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$Path,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$CertPath,
|
||||||
|
|
||||||
|
[Parameter(Mandatory)]
|
||||||
|
[string]
|
||||||
|
$CertPass
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$cert = [X509Certificate2]::new($CertPath, $CertPass)
|
||||||
|
Set-OpenAuthenticodeSignature -FilePath $Path -Certificate $cert -HashAlgorithm SHA256
|
||||||
@ -0,0 +1,146 @@
|
|||||||
|
- name: run App Control tests on Windows
|
||||||
|
hosts: windows
|
||||||
|
gather_facts: false
|
||||||
|
collections:
|
||||||
|
# This is important so that the temp ansible.windows is chosen over the ansible-test path
|
||||||
|
# which will not be signed
|
||||||
|
- ansible.windows
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: remove openauthenticode
|
||||||
|
shell: Uninstall-PSResource -Name OpenAuthenticode -Version 0.6.1
|
||||||
|
args:
|
||||||
|
executable: pwsh
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: make sure expected facts are set
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- ansible_install_dir is defined
|
||||||
|
- local_tmp_dir is defined
|
||||||
|
|
||||||
|
- name: get OS version
|
||||||
|
win_shell: (Get-Item -LiteralPath $env:SystemRoot\System32\kernel32.dll).VersionInfo.ProductVersion.ToString()
|
||||||
|
register: os_version
|
||||||
|
|
||||||
|
- name: setup and test block for 2019 and later
|
||||||
|
when:
|
||||||
|
- os_version.stdout | trim is version('10.0.17763', '>=') # 2019+
|
||||||
|
block:
|
||||||
|
- name: get test remote tmp dir
|
||||||
|
import_role:
|
||||||
|
name: ../setup_remote_tmp_dir
|
||||||
|
|
||||||
|
- name: get current user
|
||||||
|
win_shell: '[Environment]::UserName'
|
||||||
|
register: current_user_raw
|
||||||
|
|
||||||
|
- name: set current user fact
|
||||||
|
set_fact:
|
||||||
|
current_user: '{{ current_user_raw.stdout | trim }}'
|
||||||
|
|
||||||
|
- name: setup App Control
|
||||||
|
import_tasks: setup.yml
|
||||||
|
|
||||||
|
- name: run content before enabling App Control
|
||||||
|
import_tasks: test_not_enabled.yml
|
||||||
|
|
||||||
|
- name: enable App Control
|
||||||
|
win_shell: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$tmpPath = '{{ remote_tmp_dir }}'
|
||||||
|
|
||||||
|
$policyPath = Join-Path $tmpPath policy.xml
|
||||||
|
$certPath = Join-Path $tmpPath signing.cer
|
||||||
|
$policyName = 'Ansible_AppControl_Test'
|
||||||
|
|
||||||
|
Copy-Item "$env:windir\schemas\CodeIntegrity\ExamplePolicies\DefaultWindows_Enforced.xml" $policyPath
|
||||||
|
Set-CIPolicyIdInfo -FilePath $policyPath -PolicyName $policyName -PolicyId (New-Guid)
|
||||||
|
Set-CIPolicyVersion -FilePath $policyPath -Version "1.0.0.0"
|
||||||
|
|
||||||
|
Add-SignerRule -FilePath $policyPath -CertificatePath $certPath -User
|
||||||
|
Set-RuleOption -FilePath $policyPath -Option 0 # Enabled:UMCI
|
||||||
|
Set-RuleOption -FilePath $policyPath -Option 3 -Delete # Enabled:Audit Mode
|
||||||
|
Set-RuleOption -FilePath $policyPath -Option 11 -Delete # Disabled:Script Enforcement
|
||||||
|
Set-RuleOption -FilePath $policyPath -Option 19 # Enabled:Dynamic Code Security
|
||||||
|
|
||||||
|
# Using $tmpPath has this step fail
|
||||||
|
$policyBinPath = "$env:windir\System32\CodeIntegrity\SiPolicy.p7b"
|
||||||
|
$null = ConvertFrom-CIPolicy -XmlFilePath $policyPath -BinaryFilePath $policyBinPath
|
||||||
|
|
||||||
|
$ciTool = Get-Command -Name CiTool.exe -ErrorAction SilentlyContinue
|
||||||
|
$policyId = $null
|
||||||
|
if ($ciTool) {
|
||||||
|
$setInfo = & $ciTool --update-policy $policyBinPath *>&1
|
||||||
|
if ($LASTEXITCODE) {
|
||||||
|
throw "citool.exe --update-policy failed ${LASTEXITCODE}: $setInfo"
|
||||||
|
}
|
||||||
|
|
||||||
|
$policyId = & $ciTool --list-policies --json |
|
||||||
|
ConvertFrom-Json |
|
||||||
|
Select-Object -ExpandProperty Policies |
|
||||||
|
Where-Object FriendlyName -eq $policyName |
|
||||||
|
Select-Object -ExpandProperty PolicyID
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rc = Invoke-CimMethod -Namespace root\Microsoft\Windows\CI -ClassName PS_UpdateAndCompareCIPolicy -MethodName Update -Arguments @{
|
||||||
|
FilePath = $policyBinPath
|
||||||
|
}
|
||||||
|
if ($rc.ReturnValue) {
|
||||||
|
throw "PS_UpdateAndCompareCIPolicy Update failed $($rc.ReturnValue)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@{
|
||||||
|
policy_id = $policyId
|
||||||
|
path = $policyBinPath
|
||||||
|
} | ConvertTo-Json
|
||||||
|
register: policy_info_raw
|
||||||
|
|
||||||
|
- name: set policy info fact
|
||||||
|
set_fact:
|
||||||
|
policy_info: '{{ policy_info_raw.stdout | from_json }}'
|
||||||
|
|
||||||
|
- name: run content after enabling App Control
|
||||||
|
import_tasks: test_enabled.yml
|
||||||
|
|
||||||
|
- name: run invalid manifest tests
|
||||||
|
import_tasks: test_manifest.yml
|
||||||
|
|
||||||
|
always:
|
||||||
|
- name: disable policy through CiTool if present
|
||||||
|
win_shell: CiTool.exe --remove-policy {{ policy_info.policy_id }}
|
||||||
|
when:
|
||||||
|
- policy_info is defined
|
||||||
|
- policy_info.policy_id is truthy
|
||||||
|
|
||||||
|
- name: remove App Control policy
|
||||||
|
win_file:
|
||||||
|
path: '{{ policy_info.path }}'
|
||||||
|
state: absent
|
||||||
|
register: policy_removal
|
||||||
|
when:
|
||||||
|
- policy_info is defined
|
||||||
|
|
||||||
|
- name: reboot after removing policy file
|
||||||
|
win_reboot:
|
||||||
|
when: policy_removal is changed
|
||||||
|
|
||||||
|
- name: remove certificates
|
||||||
|
win_shell: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath 'Cert:\LocalMachine\Root\{{ cert_info.ca_thumbprint }}' -Force
|
||||||
|
Remove-Item -LiteralPath 'Cert:\LocalMachine\TrustedPublisher\{{ cert_info.thumbprint }}' -Force
|
||||||
|
when: cert_info is defined
|
||||||
|
|
||||||
|
- name: remove signed Ansible content
|
||||||
|
file:
|
||||||
|
path: '{{ ansible_install_dir }}/{{ item }}'
|
||||||
|
state: absent
|
||||||
|
loop:
|
||||||
|
- config/powershell_signatures.psd1
|
||||||
|
- executor/powershell/exec_wrapper.ps1.authenticode
|
||||||
|
delegate_to: localhost
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
if ! command -V pwsh; then
|
||||||
|
echo "skipping test since pwsh is not available"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
source ../collection/setup.sh
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
ANSIBLE_DIR="$( python -c "import pathlib, ansible; print(pathlib.Path(ansible.__file__).parent)" )"
|
||||||
|
|
||||||
|
ANSIBLE_COLLECTIONS_PATH="${WORK_DIR}" ANSIBLE_DISPLAY_TRACEBACK=error \
|
||||||
|
ansible-playbook "${TEST_DIR}/main.yml" \
|
||||||
|
--inventory "${TEST_DIR}/../../inventory.winrm" \
|
||||||
|
--extra-vars "local_tmp_dir=${WORK_DIR} ansible_install_dir=${ANSIBLE_DIR}" \
|
||||||
|
"${@}"
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
- name: setup test facts
|
||||||
|
set_fact:
|
||||||
|
cert_pw: "{{ 'password123!' + lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}"
|
||||||
|
|
||||||
|
- name: setup WDAC certificates
|
||||||
|
win_shell: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
$testPrefix = 'Ansible-WDAC'
|
||||||
|
$certPassword = ConvertTo-SecureString -String '{{ cert_pw }}' -Force -AsPlainText
|
||||||
|
$remoteTmpDir = '{{ remote_tmp_dir }}'
|
||||||
|
|
||||||
|
$enhancedKeyUsage = [Security.Cryptography.OidCollection]::new()
|
||||||
|
$null = $enhancedKeyUsage.Add('1.3.6.1.5.5.7.3.3') # Code Signing
|
||||||
|
$caParams = @{
|
||||||
|
Extension = @(
|
||||||
|
[Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true),
|
||||||
|
[Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new('KeyCertSign', $false),
|
||||||
|
[Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension ]::new($enhancedKeyUsage, $false)
|
||||||
|
)
|
||||||
|
CertStoreLocation = 'Cert:\CurrentUser\My'
|
||||||
|
NotAfter = (Get-Date).AddDays(1)
|
||||||
|
Type = 'Custom'
|
||||||
|
}
|
||||||
|
$ca = New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-Root"
|
||||||
|
|
||||||
|
$certParams = @{
|
||||||
|
CertStoreLocation = 'Cert:\CurrentUser\My'
|
||||||
|
KeyUsage = 'DigitalSignature'
|
||||||
|
TextExtension = @("2.5.29.37={text}1.3.6.1.5.5.7.3.3", "2.5.29.19={text}")
|
||||||
|
Type = 'Custom'
|
||||||
|
}
|
||||||
|
$cert = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Signed" -Signer $ca
|
||||||
|
$null = $cert | Export-PfxCertificate -Password $certPassword -FilePath "$remoteTmpDir\signing.pfx"
|
||||||
|
$cert.Export('Cert') | Set-Content -LiteralPath "$remoteTmpDir\signing.cer" -Encoding Byte
|
||||||
|
|
||||||
|
$certUntrusted = New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Untrusted"
|
||||||
|
$null = $certUntrusted | Export-PfxCertificate -Password $certPassword -FilePath "$remoteTmpDir\untrusted.pfx"
|
||||||
|
|
||||||
|
$caWithoutKey = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($ca.Export('Cert'))
|
||||||
|
$certWithoutKey = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($cert.Export('Cert'))
|
||||||
|
|
||||||
|
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($ca.Thumbprint)" -DeleteKey -Force
|
||||||
|
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($cert.Thumbprint)" -DeleteKey -Force
|
||||||
|
Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($certUntrusted.Thumbprint)" -DeleteKey -Force
|
||||||
|
|
||||||
|
$root = Get-Item Cert:\LocalMachine\Root
|
||||||
|
$root.Open('ReadWrite')
|
||||||
|
$root.Add($caWithoutKey)
|
||||||
|
$root.Dispose()
|
||||||
|
|
||||||
|
$trustedPublisher = Get-Item Cert:\LocalMachine\TrustedPublisher
|
||||||
|
$trustedPublisher.Open('ReadWrite')
|
||||||
|
$trustedPublisher.Add($certWithoutKey)
|
||||||
|
$trustedPublisher.Dispose()
|
||||||
|
|
||||||
|
@{
|
||||||
|
ca_thumbprint = $caWithoutKey.Thumbprint
|
||||||
|
thumbprint = $certWithoutKey.Thumbprint
|
||||||
|
untrusted_thumbprint = $certUntrusted.Thumbprint
|
||||||
|
} | ConvertTo-Json
|
||||||
|
register: cert_info_raw
|
||||||
|
become: true
|
||||||
|
become_method: runas
|
||||||
|
vars:
|
||||||
|
ansible_become_user: '{{ ansible_user }}'
|
||||||
|
ansible_become_pass: '{{ ansible_password | default(ansible_test_connection_password) }}'
|
||||||
|
|
||||||
|
- name: parse raw cert_info
|
||||||
|
set_fact:
|
||||||
|
cert_info: "{{ cert_info_raw.stdout | from_json }}"
|
||||||
|
|
||||||
|
- name: fetch signing certificates
|
||||||
|
fetch:
|
||||||
|
src: '{{ remote_tmp_dir }}\{{ item }}.pfx'
|
||||||
|
dest: '{{ local_tmp_dir }}/wdac-{{ item }}.pfx'
|
||||||
|
flat: yes
|
||||||
|
loop:
|
||||||
|
- signing
|
||||||
|
- untrusted
|
||||||
|
|
||||||
|
- name: install OpenAuthenticode
|
||||||
|
shell: |
|
||||||
|
if (-not (Get-Module -Name OpenAuthenticode -ListAvailable | Where-Object Version -ge '0.5.0')) {
|
||||||
|
$url = 'https://ansible-ci-files.s3.us-east-1.amazonaws.com/test/integration/targets/win_app_control/openauthenticode.0.6.1.nupkg'
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile '{{ local_tmp_dir }}/openauthenticode.0.6.1.nupkg'
|
||||||
|
|
||||||
|
Register-PSResourceRepository -Name AnsibleTemp -Trusted -Uri '{{ local_tmp_dir }}'
|
||||||
|
try {
|
||||||
|
Install-PSResource -Name OpenAuthenticode -Repository AnsibleTemp
|
||||||
|
} finally {
|
||||||
|
Unregister-PSResourceRepository -Name AnsibleTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
$true
|
||||||
|
} else {
|
||||||
|
$false
|
||||||
|
}
|
||||||
|
args:
|
||||||
|
executable: pwsh
|
||||||
|
register: open_auth_install
|
||||||
|
changed_when: open_auth_install.stdout | bool
|
||||||
|
notify: remove openauthenticode
|
||||||
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: sign Ansible content
|
||||||
|
script: >-
|
||||||
|
New-AnsiblePowerShellSignature.ps1
|
||||||
|
-CollectionPath {{ local_tmp_dir ~ "/ansible_collections/ns/col" | quote }}
|
||||||
|
-CertPath {{ local_tmp_dir ~ "/wdac-signing.pfx" | quote }}
|
||||||
|
-UntrustedCertPath {{ local_tmp_dir ~ "/wdac-untrusted.pfx" | quote }}
|
||||||
|
-CertPass {{ cert_pw | quote }}
|
||||||
|
-Verbose
|
||||||
|
environment:
|
||||||
|
NO_COLOR: '1'
|
||||||
|
delegate_to: localhost
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
@{
|
||||||
|
Version = 2
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = '{{ module_hash }}'
|
||||||
|
Mode = 'Trusted'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
'failure'
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
@{
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = '{{ module_hash }}'
|
||||||
|
Mode = 'Trusted'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = '{{ module_hash }} - extra'
|
||||||
|
Mode = 'Trusted'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = '{{ module_hash }}'
|
||||||
|
Mode = 'Other'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Mode = 'Trusted'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = '{{ module_hash }}'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = '{{ module_hash }}'
|
||||||
|
Mode = 'Trusted'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
@{
|
||||||
|
Version = 1
|
||||||
|
HashList = @(
|
||||||
|
@{
|
||||||
|
Hash = "$env:TEST_HASH"
|
||||||
|
Mode = 'Trusted'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,215 @@
|
|||||||
|
- name: run signed module
|
||||||
|
ns.col.signed:
|
||||||
|
input: café
|
||||||
|
register: signed_res
|
||||||
|
|
||||||
|
- name: assert run signed module
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- signed_res.language_mode == 'FullLanguage'
|
||||||
|
- signed_res.test == 'signed'
|
||||||
|
- signed_res.whoami == current_user
|
||||||
|
- signed_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run inline signed module
|
||||||
|
ns.col.inline_signed:
|
||||||
|
input: café
|
||||||
|
register: inline_signed_res
|
||||||
|
|
||||||
|
- name: assert run inline signed module
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- inline_signed_res.language_mode == 'FullLanguage'
|
||||||
|
- inline_signed_res.test == 'inline_signed'
|
||||||
|
- inline_signed_res.whoami == current_user
|
||||||
|
- inline_signed_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run inline signed module that is not trusted
|
||||||
|
ns.col.inline_signed_not_trusted:
|
||||||
|
input: café
|
||||||
|
register: inline_signed_not_trusted_res
|
||||||
|
|
||||||
|
- name: assert run inline signed module
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- inline_signed_not_trusted_res.language_mode == 'ConstrainedLanguage'
|
||||||
|
- inline_signed_not_trusted_res.test == 'inline_signed_not_trusted'
|
||||||
|
- inline_signed_not_trusted_res.whoami == current_user
|
||||||
|
- inline_signed_not_trusted_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run signed module to test exec wrapper scope
|
||||||
|
ns.col.scope:
|
||||||
|
register: scoped_res
|
||||||
|
|
||||||
|
- name: assert run signed module to test exec wrapper scope
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- scoped_res.missing_using_namespace == True
|
||||||
|
- scoped_res.module_using_namespace == 'System.Management.Automation.Language.Parser'
|
||||||
|
- scoped_res.script_var == 'foo'
|
||||||
|
|
||||||
|
- name: run module marked as skipped
|
||||||
|
ns.col.skipped:
|
||||||
|
input: café
|
||||||
|
register: skipped_res
|
||||||
|
|
||||||
|
- name: assert run module marked as skipped
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- skipped_res.language_mode == 'ConstrainedLanguage'
|
||||||
|
- skipped_res.test == 'skipped'
|
||||||
|
- skipped_res.whoami == current_user
|
||||||
|
- skipped_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run module marked as skipped with a failure
|
||||||
|
ns.col.skipped:
|
||||||
|
should_fail: true
|
||||||
|
register: skipped_fail_res
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: assert run module marked as skipped with a failure
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- skipped_fail_res is failed
|
||||||
|
- >-
|
||||||
|
skipped_fail_res.msg == "Unhandled exception while executing module: exception here"
|
||||||
|
- skipped_fail_res.exception is search("At .*\\\\ansible_collections\.ns\.col\.plugins\.modules\.skipped-.*\.ps1")
|
||||||
|
|
||||||
|
- name: run module marked as unsupported
|
||||||
|
ns.col.unsupported:
|
||||||
|
register: unsupported_res
|
||||||
|
failed_when:
|
||||||
|
- unsupported_res.failed == False
|
||||||
|
- >-
|
||||||
|
unsupported_res.msg is not contains("Provided script for 'ansible_collections.ns.col.plugins.modules.unsupported.ps1' is marked as unsupported in CLM mode.")
|
||||||
|
|
||||||
|
- name: run module with signed utils
|
||||||
|
ns.col.signed_module_util:
|
||||||
|
register: signed_util_res
|
||||||
|
|
||||||
|
- name: assert run module with signed utils
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- signed_util_res.language_mode == 'FullLanguage'
|
||||||
|
- signed_util_res.builtin_powershell_util == 'value'
|
||||||
|
- signed_util_res.csharp_util == 'value'
|
||||||
|
- signed_util_res.powershell_util.language_mode == 'FullLanguage'
|
||||||
|
|
||||||
|
- name: run module with unsigned C# util
|
||||||
|
ns.col.unsigned_csharp_util:
|
||||||
|
register: unsigned_csharp_util_res
|
||||||
|
failed_when:
|
||||||
|
- unsigned_csharp_util_res.failed == False
|
||||||
|
- >-
|
||||||
|
unsigned_csharp_util_res.msg is not contains("C# module util 'ansible_collections.ns.col.plugins.module_utils.CSharpUnsigned.cs' is not trusted and cannot be loaded.")
|
||||||
|
|
||||||
|
- name: run module with unsigned Pwsh util
|
||||||
|
ns.col.unsigned_pwsh_util:
|
||||||
|
register: unsigned_pwsh_util_res
|
||||||
|
failed_when:
|
||||||
|
- unsigned_pwsh_util_res.failed == False
|
||||||
|
- >-
|
||||||
|
unsigned_pwsh_util_res.msg is not contains("PowerShell module util 'ansible_collections.ns.col.plugins.module_utils.PwshUnsigned.cs' is not trusted and cannot be loaded.")
|
||||||
|
|
||||||
|
- name: run unsigned module with utils
|
||||||
|
ns.col.unsigned_module_with_util:
|
||||||
|
register: unsigned_module_with_util_res
|
||||||
|
failed_when:
|
||||||
|
- unsigned_module_with_util_res.failed == False
|
||||||
|
- >-
|
||||||
|
unsigned_module_with_util_res.msg is not contains("Cannot run untrusted PowerShell script 'ns.col.unsigned_with_util.ps1' in ConstrainedLanguage mode with module util imports.")
|
||||||
|
|
||||||
|
- name: run script role
|
||||||
|
import_role:
|
||||||
|
name: ns.col.app_control_script
|
||||||
|
|
||||||
|
- name: assert script role result
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- signed_res.rc == 0
|
||||||
|
- (signed_res.stdout | from_json).language_mode == 'FullLanguage'
|
||||||
|
- (signed_res.stdout | from_json).whoami == current_user
|
||||||
|
- (signed_res.stdout | from_json).ünicode == 'café'
|
||||||
|
- unsigned_res.rc == 0
|
||||||
|
- (unsigned_res.stdout | from_json).language_mode == 'ConstrainedLanguage'
|
||||||
|
- (unsigned_res.stdout | from_json).whoami == current_user
|
||||||
|
- (unsigned_res.stdout | from_json).ünicode == 'café'
|
||||||
|
|
||||||
|
- name: signed module with become
|
||||||
|
ns.col.signed:
|
||||||
|
input: café
|
||||||
|
become: true
|
||||||
|
become_method: runas
|
||||||
|
become_user: SYSTEM
|
||||||
|
register: become_res
|
||||||
|
|
||||||
|
- name: assert run signed module with become
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- become_res.language_mode == 'FullLanguage'
|
||||||
|
- become_res.test == 'signed'
|
||||||
|
- become_res.whoami == 'SYSTEM'
|
||||||
|
- become_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: signed module with async
|
||||||
|
ns.col.signed:
|
||||||
|
input: café
|
||||||
|
async: 60
|
||||||
|
poll: 3
|
||||||
|
register: async_res
|
||||||
|
|
||||||
|
- name: assert run signed module with async
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- async_res.language_mode == 'FullLanguage'
|
||||||
|
- async_res.test == 'signed'
|
||||||
|
- async_res.whoami == current_user
|
||||||
|
- async_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: copy file
|
||||||
|
win_copy:
|
||||||
|
src: New-AnsiblePowerShellSignature.ps1
|
||||||
|
dest: '{{ remote_tmp_dir }}/New-AnsiblePowerShellSignature.ps1'
|
||||||
|
register: copy_res
|
||||||
|
|
||||||
|
- name: get remote hash of copied file
|
||||||
|
win_stat:
|
||||||
|
path: '{{ remote_tmp_dir }}/New-AnsiblePowerShellSignature.ps1'
|
||||||
|
get_checksum: true
|
||||||
|
register: copy_stat
|
||||||
|
|
||||||
|
- name: assert copy file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- copy_res.checksum == copy_stat.stat.checksum
|
||||||
|
|
||||||
|
- name: fetch file
|
||||||
|
fetch:
|
||||||
|
src: '{{ remote_tmp_dir }}/New-AnsiblePowerShellSignature.ps1'
|
||||||
|
dest: '{{ local_tmp_dir }}/New-AnsiblePowerShellSignature.ps1'
|
||||||
|
flat: true
|
||||||
|
register: fetch_res
|
||||||
|
|
||||||
|
- name: get local hash of fetch file
|
||||||
|
stat:
|
||||||
|
path: '{{ local_tmp_dir }}/New-AnsiblePowerShellSignature.ps1'
|
||||||
|
get_checksum: true
|
||||||
|
delegate_to: localhost
|
||||||
|
register: fetch_stat
|
||||||
|
|
||||||
|
- name: assert fetch file
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- fetch_res.checksum == copy_stat.stat.checksum
|
||||||
|
- fetch_res.checksum == fetch_stat.stat.checksum
|
||||||
|
|
||||||
|
- name: run signed module with signed module util in another collection
|
||||||
|
ns.module_util_ref.module:
|
||||||
|
register: cross_util_res
|
||||||
|
|
||||||
|
- name: assert run signed module with signed module utils in another collection
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cross_util_res.language_mode == 'FullLanguage'
|
||||||
|
- cross_util_res.csharp_util == 'value'
|
||||||
|
- "cross_util_res.powershell_util == {'language_mode': 'FullLanguage'}"
|
||||||
@ -0,0 +1,154 @@
|
|||||||
|
# These tests verify various failure conditions that will invalidate a signed manifest
|
||||||
|
|
||||||
|
- name: get hash of collection module
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: '{{ local_tmp_dir }}/ansible_collections/ns/invalid_manifest/plugins/modules/module.ps1'
|
||||||
|
get_checksum: true
|
||||||
|
checksum_algorithm: sha256
|
||||||
|
delegate_to: localhost
|
||||||
|
register: module_hash_raw
|
||||||
|
|
||||||
|
- name: set module hash var
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
module_hash: '{{ module_hash_raw.stat.checksum | upper }}'
|
||||||
|
|
||||||
|
- name: create manifest with untrusted signature
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_ok.psd1
|
||||||
|
cert_name: wdac-untrusted
|
||||||
|
|
||||||
|
- name: run module with untrusted signed manifest
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': script is not signed or not trusted to run.")
|
||||||
|
|
||||||
|
- name: create manifest with no Hashtable
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_no_hashtable.psd1
|
||||||
|
|
||||||
|
- name: run module with no Hashtable
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting a single hashtable in the signed manifest.")
|
||||||
|
|
||||||
|
- name: create manifest with no Version
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_no_version.psd1
|
||||||
|
|
||||||
|
- name: run module with no Version
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting hash list to contain 'Version' key.")
|
||||||
|
|
||||||
|
- name: create manifest with invalid Version
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_invalid_version.psd1
|
||||||
|
|
||||||
|
- name: run module with invalid Version
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': unsupported hash list Version 2, expecting 1.")
|
||||||
|
|
||||||
|
- name: create manifest with no HashList
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_no_hashlist.psd1
|
||||||
|
|
||||||
|
- name: run module with no HashList
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting hash list to contain 'HashList' key.")
|
||||||
|
|
||||||
|
- name: create manifest with no Hash subkey
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_no_hash_subkey.psd1
|
||||||
|
|
||||||
|
- name: run module with no Hash subkey
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting hash list to contain hashtable with Hash key with a value of a SHA256 strings.")
|
||||||
|
|
||||||
|
- name: create manifest with invalid Hash subkey value
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_invalid_hash_subkey.psd1
|
||||||
|
|
||||||
|
- name: run module with invalid Hash subkey value
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting hash list to contain hashtable with Hash key with a value of a SHA256 strings.")
|
||||||
|
|
||||||
|
- name: create manifest with no Mode subkey
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_no_mode_subkey.psd1
|
||||||
|
|
||||||
|
- name: run module with no Mode subkey
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting hash list entry for " ~ module_hash ~ " to contain a mode of 'Trusted' or 'Unsupported' but got ''.")
|
||||||
|
|
||||||
|
- name: create manfiest with invalid Mode subkey value
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_invalid_mode_subkey.psd1
|
||||||
|
|
||||||
|
- name: run module with invalid Mode subkey value
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not contains("Failed to process signed manifest 'ansible_collections.ns.invalid_manifest.meta.powershell_signatures.psd1': expecting hash list entry for " ~ module_hash ~ " to contain a mode of 'Trusted' or 'Unsupported' but got 'Other'.")
|
||||||
|
|
||||||
|
- name: create manifest with unsafe expressions
|
||||||
|
ansible.builtin.import_tasks: create_manifest.yml
|
||||||
|
vars:
|
||||||
|
manifest_file: manifest_v1_unsafe_expression.psd1
|
||||||
|
|
||||||
|
- name: run module with unsafe expressions
|
||||||
|
ns.invalid_manifest.module:
|
||||||
|
input: café
|
||||||
|
register: res
|
||||||
|
failed_when:
|
||||||
|
- res.failed == False
|
||||||
|
- >-
|
||||||
|
res.msg is not search("failure during exec_wrapper: Failed to process signed manifest 'ansible_collections\.ns\.invalid_manifest\.meta\.powershell_signatures.psd1':.*Cannot generate a Windows PowerShell object for a ScriptBlock evaluating dynamic expressions")
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
# When App Control is not enabled we expect modules even if signed or unsigned should
|
||||||
|
# run without any changes in FullLanguageMode.
|
||||||
|
|
||||||
|
- name: run signed module
|
||||||
|
ns.col.signed:
|
||||||
|
input: café
|
||||||
|
register: signed_res
|
||||||
|
|
||||||
|
- name: assert run signed module
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- signed_res.language_mode == 'FullLanguage'
|
||||||
|
- signed_res.test == 'signed'
|
||||||
|
- signed_res.whoami == current_user
|
||||||
|
- signed_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run inline signed module
|
||||||
|
ns.col.inline_signed:
|
||||||
|
input: café
|
||||||
|
register: inline_signed_res
|
||||||
|
|
||||||
|
- name: assert run inline signed module
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- inline_signed_res.language_mode == 'FullLanguage'
|
||||||
|
- inline_signed_res.test == 'inline_signed'
|
||||||
|
- inline_signed_res.whoami == current_user
|
||||||
|
- inline_signed_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run inline signed module that is not trusted
|
||||||
|
ns.col.inline_signed_not_trusted:
|
||||||
|
input: café
|
||||||
|
register: inline_signed_not_trusted_res
|
||||||
|
|
||||||
|
- name: assert run inline signed module
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- inline_signed_not_trusted_res.language_mode == 'FullLanguage'
|
||||||
|
- inline_signed_not_trusted_res.test == 'inline_signed_not_trusted'
|
||||||
|
- inline_signed_not_trusted_res.whoami == current_user
|
||||||
|
- inline_signed_not_trusted_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run signed module to test exec wrapper scope
|
||||||
|
ns.col.scope:
|
||||||
|
register: scoped_res
|
||||||
|
|
||||||
|
- name: assert run signed module to test exec wrapper scope
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- scoped_res.missing_using_namespace == True
|
||||||
|
- scoped_res.module_using_namespace == 'System.Management.Automation.Language.Parser'
|
||||||
|
- scoped_res.script_var == 'foo'
|
||||||
|
|
||||||
|
- name: run module marked as skipped
|
||||||
|
ns.col.skipped:
|
||||||
|
input: café
|
||||||
|
register: skipped_res
|
||||||
|
|
||||||
|
- name: assert run module marked as skipped
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- skipped_res.language_mode == 'FullLanguage'
|
||||||
|
- skipped_res.test == 'skipped'
|
||||||
|
- skipped_res.whoami == current_user
|
||||||
|
- skipped_res.ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run module marked as unsupported
|
||||||
|
ns.col.unsupported:
|
||||||
|
register: unsupported_res
|
||||||
|
|
||||||
|
- name: assert run module marked as unsupported
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- unsupported_res.language_mode == 'FullLanguage'
|
||||||
|
- unsupported_res.test == 'unsupported'
|
||||||
|
- unsupported_res.whoami == current_user
|
||||||
|
|
||||||
|
- name: run module with signed utils
|
||||||
|
ns.col.signed_module_util:
|
||||||
|
register: signed_util_res
|
||||||
|
|
||||||
|
- name: assert run module with signed utils
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- signed_util_res.language_mode == 'FullLanguage'
|
||||||
|
- signed_util_res.builtin_powershell_util == 'value'
|
||||||
|
- signed_util_res.csharp_util == 'value'
|
||||||
|
- signed_util_res.powershell_util.language_mode == 'FullLanguage'
|
||||||
|
|
||||||
|
- name: run module with unsigned C# util
|
||||||
|
ns.col.unsigned_csharp_util:
|
||||||
|
register: unsigned_csharp_util_res
|
||||||
|
|
||||||
|
- name: assert run module with unsigned C# util
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- unsigned_csharp_util_res.language_mode == 'FullLanguage'
|
||||||
|
- unsigned_csharp_util_res.res == 'value'
|
||||||
|
|
||||||
|
- name: run module with unsigned Pwsh util
|
||||||
|
ns.col.unsigned_pwsh_util:
|
||||||
|
register: unsigned_pwsh_util_res
|
||||||
|
|
||||||
|
- name: assert run module with unsigned Pwsh util
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- unsigned_pwsh_util_res.language_mode == 'FullLanguage'
|
||||||
|
- unsigned_pwsh_util_res.res.language_mode == 'FullLanguage'
|
||||||
|
|
||||||
|
- name: run unsigned module with utils
|
||||||
|
ns.col.unsigned_module_with_util:
|
||||||
|
register: unsigned_module_with_util_res
|
||||||
|
|
||||||
|
- name: assert run unsigned module with utils
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- unsigned_module_with_util_res.language_mode == 'FullLanguage'
|
||||||
|
- unsigned_module_with_util_res.builtin_csharp == True
|
||||||
|
- unsigned_module_with_util_res.builtin_pwsh == True
|
||||||
|
- unsigned_module_with_util_res.collection_csharp == True
|
||||||
|
- unsigned_module_with_util_res.collection_pwsh == True
|
||||||
|
|
||||||
|
- name: run script role
|
||||||
|
import_role:
|
||||||
|
name: ns.col.app_control_script
|
||||||
|
|
||||||
|
- name: assert script role result
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- signed_res.rc == 0
|
||||||
|
- (signed_res.stdout | from_json).language_mode == 'FullLanguage'
|
||||||
|
- (signed_res.stdout | from_json).whoami == current_user
|
||||||
|
- (signed_res.stdout | from_json).ünicode == 'café'
|
||||||
|
- unsigned_res.rc == 0
|
||||||
|
- (unsigned_res.stdout | from_json).language_mode == 'FullLanguage'
|
||||||
|
- (unsigned_res.stdout | from_json).whoami == current_user
|
||||||
|
- (unsigned_res.stdout | from_json).ünicode == 'café'
|
||||||
|
|
||||||
|
- name: run signed module with signed module util in another collection
|
||||||
|
ns.module_util_ref.module:
|
||||||
|
register: cross_util_res
|
||||||
|
|
||||||
|
- name: assert run signed module with signed module utils in another collection
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- cross_util_res.language_mode == 'FullLanguage'
|
||||||
|
- cross_util_res.csharp_util == 'value'
|
||||||
|
- "cross_util_res.powershell_util == {'language_mode': 'FullLanguage'}"
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
#!powershell
|
||||||
|
|
||||||
|
#AnsibleRequires -Wrapper
|
||||||
|
|
||||||
|
@{
|
||||||
|
changed = $false
|
||||||
|
complex_args = $complex_args
|
||||||
|
} | ConvertTo-Json -Depth 99
|
||||||
@ -0,0 +1 @@
|
|||||||
|
'ünicode'
|
||||||
Loading…
Reference in New Issue