ansible-core 2.19.0b4__py3-none-any.whl → 2.19.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansible/_internal/__init__.py +1 -1
- ansible/_internal/_collection_proxy.py +1 -1
- ansible/_internal/_errors/_alarm_timeout.py +66 -0
- ansible/_internal/_errors/_captured.py +25 -30
- ansible/_internal/_errors/_error_factory.py +89 -0
- ansible/_internal/_errors/_error_utils.py +240 -0
- ansible/_internal/_errors/_task_timeout.py +28 -0
- ansible/_internal/_event_formatting.py +127 -0
- ansible/_internal/_json/__init__.py +5 -5
- ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
- ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
- ansible/_internal/_json/_profiles/_legacy.py +3 -11
- ansible/_internal/_ssh/__init__.py +0 -0
- ansible/_internal/_ssh/_agent_launch.py +91 -0
- ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
- ansible/_internal/_templating/__init__.py +5 -3
- ansible/_internal/_templating/_datatag.py +2 -1
- ansible/_internal/_templating/_engine.py +3 -4
- ansible/_internal/_templating/_jinja_bits.py +21 -16
- ansible/_internal/_templating/_jinja_common.py +18 -27
- ansible/_internal/_templating/_jinja_plugins.py +31 -3
- ansible/_internal/_templating/_lazy_containers.py +5 -5
- ansible/_internal/_templating/_transform.py +20 -19
- ansible/_internal/_templating/_utils.py +1 -1
- ansible/_internal/_yaml/_dumper.py +1 -1
- ansible/_internal/_yaml/_errors.py +7 -7
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
- ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
- ansible/cli/__init__.py +5 -82
- ansible/cli/arguments/option_helpers.py +2 -3
- ansible/cli/doc.py +84 -28
- ansible/cli/inventory.py +1 -1
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +22 -0
- ansible/errors/__init__.py +96 -49
- ansible/executor/module_common.py +8 -10
- ansible/executor/powershell/async_watchdog.ps1 +2 -2
- ansible/executor/powershell/async_wrapper.ps1 +3 -3
- ansible/executor/powershell/become_wrapper.ps1 +20 -2
- ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
- ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
- ansible/executor/powershell/exec_wrapper.ps1 +219 -6
- ansible/executor/powershell/module_manifest.py +52 -0
- ansible/executor/powershell/module_wrapper.ps1 +47 -21
- ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
- ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
- ansible/executor/process/worker.py +38 -113
- ansible/executor/task_executor.py +26 -61
- ansible/executor/task_result.py +2 -4
- ansible/galaxy/collection/__init__.py +1 -4
- ansible/inventory/manager.py +1 -1
- ansible/module_utils/_internal/__init__.py +0 -3
- ansible/module_utils/_internal/_ambient_context.py +3 -3
- ansible/module_utils/_internal/_ansiballz.py +4 -2
- ansible/module_utils/_internal/_datatag/__init__.py +20 -14
- ansible/module_utils/_internal/_datatag/_tags.py +2 -2
- ansible/module_utils/_internal/_deprecator.py +66 -48
- ansible/module_utils/_internal/_errors.py +88 -17
- ansible/module_utils/_internal/_event_utils.py +61 -0
- ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
- ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
- ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
- ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
- ansible/module_utils/_internal/_plugin_info.py +1 -1
- ansible/module_utils/_internal/_stack.py +22 -0
- ansible/module_utils/_internal/_text_utils.py +6 -0
- ansible/module_utils/_internal/_traceback.py +11 -8
- ansible/module_utils/ansible_release.py +1 -1
- ansible/module_utils/basic.py +49 -15
- ansible/module_utils/common/arg_spec.py +2 -2
- ansible/module_utils/common/collections.py +6 -0
- ansible/module_utils/common/json.py +2 -2
- ansible/module_utils/common/text/converters.py +3 -3
- ansible/module_utils/common/validation.py +1 -1
- ansible/module_utils/common/warnings.py +80 -23
- ansible/module_utils/common/yaml.py +1 -1
- ansible/module_utils/datatag.py +5 -2
- ansible/module_utils/facts/system/distribution.py +16 -3
- ansible/module_utils/facts/virtual/linux.py +1 -1
- ansible/module_utils/service.py +2 -9
- ansible/modules/apt_repository.py +7 -29
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +5 -5
- ansible/modules/dnf5.py +14 -22
- ansible/modules/hostname.py +0 -1
- ansible/modules/service.py +3 -9
- ansible/parsing/ajson.py +3 -5
- ansible/parsing/dataloader.py +4 -4
- ansible/parsing/mod_args.py +1 -1
- ansible/parsing/plugin_docs.py +2 -2
- ansible/parsing/utils/yaml.py +3 -3
- ansible/parsing/vault/__init__.py +4 -4
- ansible/playbook/playbook_include.py +1 -1
- ansible/playbook/taggable.py +0 -3
- ansible/plugins/__init__.py +0 -25
- ansible/plugins/action/__init__.py +8 -31
- ansible/plugins/action/add_host.py +1 -1
- ansible/plugins/action/assemble.py +8 -16
- ansible/plugins/action/async_status.py +7 -2
- ansible/plugins/action/copy.py +8 -7
- ansible/plugins/action/gather_facts.py +8 -8
- ansible/plugins/action/package.py +5 -8
- ansible/plugins/action/script.py +8 -15
- ansible/plugins/action/service.py +3 -7
- ansible/plugins/action/template.py +3 -8
- ansible/plugins/action/unarchive.py +5 -15
- ansible/plugins/action/uri.py +9 -20
- ansible/plugins/callback/__init__.py +4 -6
- ansible/plugins/callback/junit.py +4 -2
- ansible/plugins/connection/local.py +2 -2
- ansible/plugins/connection/ssh.py +17 -9
- ansible/plugins/connection/winrm.py +5 -2
- ansible/plugins/doc_fragments/constructed.py +2 -2
- ansible/plugins/filter/core.py +13 -6
- ansible/plugins/filter/encryption.py +4 -4
- ansible/plugins/inventory/__init__.py +11 -10
- ansible/plugins/inventory/script.py +1 -1
- ansible/plugins/list.py +69 -16
- ansible/plugins/loader.py +7 -7
- ansible/plugins/lookup/csvfile.py +16 -71
- ansible/plugins/lookup/first_found.py +2 -1
- ansible/plugins/shell/__init__.py +56 -2
- ansible/plugins/shell/powershell.py +66 -9
- ansible/plugins/shell/sh.py +9 -5
- ansible/plugins/test/core.py +21 -15
- ansible/plugins/test/finished.yml +1 -1
- ansible/plugins/test/uri.py +2 -5
- ansible/release.py +1 -1
- ansible/template/__init__.py +30 -2
- ansible/utils/display.py +103 -128
- ansible/utils/hashing.py +0 -1
- ansible/utils/listify.py +6 -4
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +1 -1
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +162 -151
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
- ansible_test/_data/completion/docker.txt +3 -3
- ansible_test/_data/completion/remote.txt +1 -0
- ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
- ansible_test/_data/requirements/sanity.changelog.txt +2 -2
- ansible_test/_data/requirements/sanity.pep8.txt +1 -1
- ansible_test/_data/requirements/sanity.pylint.txt +4 -4
- ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
- ansible_test/_internal/util.py +20 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
- ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +61 -7
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/COPYING +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/Apache-License.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/MIT-license.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/PSF-license.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b4.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -18,10 +18,32 @@ foreach ($obj in $code.params.PSObject.Properties) {
|
|
18
18
|
$splat[$obj.Name] = $obj.Value
|
19
19
|
}
|
20
20
|
|
21
|
-
$
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
$filePath = $null
|
22
|
+
try {
|
23
|
+
$cmd = if ($ExecutionContext.SessionState.LanguageMode -eq 'FullLanguage') {
|
24
|
+
# In FLM we can just invoke the code as a scriptblock without touching the
|
25
|
+
# disk.
|
26
|
+
[System.Management.Automation.Language.Parser]::ParseInput(
|
27
|
+
$code.script,
|
28
|
+
"$($code.name).ps1", # Name is used in stack traces.
|
29
|
+
[ref]$null,
|
30
|
+
[ref]$null).GetScriptBlock()
|
31
|
+
}
|
32
|
+
else {
|
33
|
+
# CLM needs to execute code from a file for it to run in FLM when trusted.
|
34
|
+
# Set-Item on 5.1 doesn't have a way to use UTF-8 without a BOM but luckily
|
35
|
+
# New-Item does that by default for both 5.1 and 7. We need to ensure we
|
36
|
+
# use UTF-8 without BOM so the signature is correct.
|
37
|
+
$filePath = Join-Path -Path $env:TEMP -ChildPath "$($code.name)-$(New-Guid).ps1"
|
38
|
+
$null = New-Item -Path $filePath -Value $code.script -ItemType File -Force
|
39
|
+
|
40
|
+
$filePath
|
41
|
+
}
|
26
42
|
|
27
|
-
$input | & $cmd @splat
|
43
|
+
$input | & $cmd @splat
|
44
|
+
}
|
45
|
+
finally {
|
46
|
+
if ($filePath -and (Test-Path -LiteralPath $filePath)) {
|
47
|
+
Remove-Item -LiteralPath $filePath -Force
|
48
|
+
}
|
49
|
+
}
|
@@ -28,8 +28,12 @@ $Host.Runspace.Debugger.SetDebugMode([DebugModes]::RemoteScript)
|
|
28
28
|
Function New-CoverageBreakpointsForScriptBlock {
|
29
29
|
Param (
|
30
30
|
[Parameter(Mandatory)]
|
31
|
-
[
|
32
|
-
$
|
31
|
+
[string]
|
32
|
+
$ScriptName,
|
33
|
+
|
34
|
+
[Parameter(Mandatory)]
|
35
|
+
[ScriptBlockAst]
|
36
|
+
$ScriptBlockAst,
|
33
37
|
|
34
38
|
[Parameter(Mandatory)]
|
35
39
|
[String]
|
@@ -39,7 +43,7 @@ Function New-CoverageBreakpointsForScriptBlock {
|
|
39
43
|
$predicate = {
|
40
44
|
$args[0] -is [CommandBaseAst]
|
41
45
|
}
|
42
|
-
$scriptCmds = $
|
46
|
+
$scriptCmds = $ScriptBlockAst.FindAll($predicate, $true)
|
43
47
|
|
44
48
|
# Create an object that tracks the Ansible path of the file and the breakpoints that have been set in it
|
45
49
|
$info = [PSCustomObject]@{
|
@@ -68,7 +72,7 @@ Function New-CoverageBreakpointsForScriptBlock {
|
|
68
72
|
}
|
69
73
|
|
70
74
|
# Action is explicitly $null as it will slow down the runtime quite dramatically.
|
71
|
-
$b = $lineCtor.Invoke(@($
|
75
|
+
$b = $lineCtor.Invoke(@($ScriptName, $cmd.Extent.StartLineNumber, $cmd.Extent.StartColumnNumber, $null))
|
72
76
|
$info.Breakpoints.Add($b)
|
73
77
|
}
|
74
78
|
|
@@ -102,11 +106,16 @@ try {
|
|
102
106
|
$coveragePathFilter = $PathFilter.Split(":", [StringSplitOptions]::RemoveEmptyEntries)
|
103
107
|
$breakpointInfo = @(
|
104
108
|
foreach ($scriptName in @($ModuleName; $actionParams.PowerShellModules)) {
|
105
|
-
|
109
|
+
# We don't use -IncludeScriptBlock as the script might be untrusted
|
110
|
+
# and will run under CLM. While we recreate the ScriptBlock here it
|
111
|
+
# is only to get the AST that contains the statements and their
|
112
|
+
# line numbers to create the breakpoint info for.
|
113
|
+
$scriptInfo = Get-AnsibleScript -Name $scriptName
|
106
114
|
|
107
115
|
if (Compare-PathFilterPattern -Patterns $coveragePathFilter -Path $scriptInfo.Path) {
|
108
116
|
$covParams = @{
|
109
|
-
|
117
|
+
ScriptName = $scriptInfo.Name
|
118
|
+
ScriptBlockAst = [ScriptBlock]::Create($scriptInfo.Script).Ast
|
110
119
|
AnsiblePath = $scriptInfo.Path
|
111
120
|
}
|
112
121
|
New-CoverageBreakpointsForScriptBlock @covParams
|
@@ -9,6 +9,7 @@ using namespace System.Linq
|
|
9
9
|
using namespace System.Management.Automation
|
10
10
|
using namespace System.Management.Automation.Language
|
11
11
|
using namespace System.Management.Automation.Security
|
12
|
+
using namespace System.Reflection
|
12
13
|
using namespace System.Security.Cryptography
|
13
14
|
using namespace System.Text
|
14
15
|
|
@@ -53,6 +54,10 @@ begin {
|
|
53
54
|
$ErrorActionPreference = "Stop"
|
54
55
|
$ProgressPreference = "SilentlyContinue"
|
55
56
|
|
57
|
+
if ($PSCommandPath -and (Test-Path -LiteralPath $PSCommandPath)) {
|
58
|
+
Remove-Item -LiteralPath $PSCommandPath -Force
|
59
|
+
}
|
60
|
+
|
56
61
|
# Try and set the console encoding to UTF-8 allowing Ansible to read the
|
57
62
|
# output of the wrapper as UTF-8 bytes.
|
58
63
|
try {
|
@@ -89,6 +94,9 @@ begin {
|
|
89
94
|
}
|
90
95
|
|
91
96
|
# $Script:AnsibleManifest = @{} # Defined in process/end.
|
97
|
+
$Script:AnsibleShouldConstrain = [SystemPolicy]::GetSystemLockdownPolicy() -eq 'Enforce'
|
98
|
+
$Script:AnsibleTrustedHashList = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
99
|
+
$Script:AnsibleUnsupportedHashList = [HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
92
100
|
$Script:AnsibleWrapperWarnings = [List[string]]::new()
|
93
101
|
$Script:AnsibleTempPath = @(
|
94
102
|
# Wrapper defined tmpdir
|
@@ -110,6 +118,8 @@ begin {
|
|
110
118
|
$false
|
111
119
|
}
|
112
120
|
} | Select-Object -First 1
|
121
|
+
$Script:AnsibleTempScripts = [List[string]]::new()
|
122
|
+
$Script:AnsibleClrFacadeSet = $false
|
113
123
|
|
114
124
|
Function Convert-JsonObject {
|
115
125
|
param(
|
@@ -147,7 +157,11 @@ begin {
|
|
147
157
|
|
148
158
|
[Parameter()]
|
149
159
|
[switch]
|
150
|
-
$IncludeScriptBlock
|
160
|
+
$IncludeScriptBlock,
|
161
|
+
|
162
|
+
[Parameter()]
|
163
|
+
[switch]
|
164
|
+
$SkipHashCheck
|
151
165
|
)
|
152
166
|
|
153
167
|
if (-not $Script:AnsibleManifest.scripts.Contains($Name)) {
|
@@ -172,11 +186,93 @@ begin {
|
|
172
186
|
[ref]$null).GetScriptBlock()
|
173
187
|
}
|
174
188
|
|
175
|
-
[PSCustomObject]@{
|
189
|
+
$outputValue = [PSCustomObject]@{
|
176
190
|
Name = $Name
|
177
191
|
Script = $scriptContents
|
178
192
|
Path = $scriptInfo.path
|
179
193
|
ScriptBlock = $sbk
|
194
|
+
ShouldConstrain = $false
|
195
|
+
}
|
196
|
+
|
197
|
+
if (-not $Script:AnsibleShouldConstrain) {
|
198
|
+
$outputValue
|
199
|
+
return
|
200
|
+
}
|
201
|
+
|
202
|
+
if (-not $SkipHashCheck) {
|
203
|
+
$sha256 = [SHA256]::Create()
|
204
|
+
$scriptHash = [BitConverter]::ToString($sha256.ComputeHash($scriptBytes)).Replace("-", "")
|
205
|
+
$sha256.Dispose()
|
206
|
+
|
207
|
+
if ($Script:AnsibleUnsupportedHashList.Contains($scriptHash)) {
|
208
|
+
$err = [ErrorRecord]::new(
|
209
|
+
[Exception]::new("Provided script for '$Name' is marked as unsupported in CLM mode."),
|
210
|
+
"ScriptUnsupported",
|
211
|
+
[ErrorCategory]::SecurityError,
|
212
|
+
$Name)
|
213
|
+
$PSCmdlet.ThrowTerminatingError($err)
|
214
|
+
}
|
215
|
+
elseif ($Script:AnsibleTrustedHashList.Contains($scriptHash)) {
|
216
|
+
$outputValue
|
217
|
+
return
|
218
|
+
}
|
219
|
+
}
|
220
|
+
|
221
|
+
# If we have reached here we are running in a locked down environment
|
222
|
+
# and the script is not trusted in the signed hashlists. Check if it
|
223
|
+
# contains the authenticode signature and verify that using PowerShell.
|
224
|
+
# [SystemPolicy]::GetFilePolicyEnforcement(...) is a new API but only
|
225
|
+
# present in Server 2025+ so we need to rely on the known behaviour of
|
226
|
+
# Get-Command to fail with CommandNotFoundException if the script is
|
227
|
+
# not allowed to run.
|
228
|
+
$outputValue.ShouldConstrain = $true
|
229
|
+
if ($scriptContents -like "*`r`n# SIG # Begin signature block`r`n*") {
|
230
|
+
Set-WinPSDefaultFileEncoding
|
231
|
+
|
232
|
+
# If the script is manually signed we need to ensure the signature
|
233
|
+
# is valid and trusted by the OS policy.
|
234
|
+
# We must use '.ps1' so the ExternalScript WDAC check will apply.
|
235
|
+
$tmpFile = [Path]::Combine($Script:AnsibleTempPath, "ansible-tmp-$([Guid]::NewGuid()).ps1")
|
236
|
+
try {
|
237
|
+
[File]::WriteAllBytes($tmpFile, $scriptBytes)
|
238
|
+
$cmd = Get-Command -Name $tmpFile -CommandType ExternalScript -ErrorAction Stop
|
239
|
+
|
240
|
+
# Get-Command caches the file contents after loading which we
|
241
|
+
# use to verify it was not modified before the signature check.
|
242
|
+
$expectedScript = $cmd.OriginalEncoding.GetString($scriptBytes)
|
243
|
+
if ($expectedScript -ne $cmd.ScriptContents) {
|
244
|
+
$err = [ErrorRecord]::new(
|
245
|
+
[Exception]::new("Script has been modified during signature check."),
|
246
|
+
"ScriptModifiedTrusted",
|
247
|
+
[ErrorCategory]::SecurityError,
|
248
|
+
$Name)
|
249
|
+
$PSCmdlet.ThrowTerminatingError($err)
|
250
|
+
}
|
251
|
+
|
252
|
+
$outputValue.ShouldConstrain = $false
|
253
|
+
}
|
254
|
+
catch [CommandNotFoundException] {
|
255
|
+
$null = $null # No-op but satisfies the linter.
|
256
|
+
}
|
257
|
+
finally {
|
258
|
+
if (Test-Path -LiteralPath $tmpFile) {
|
259
|
+
Remove-Item -LiteralPath $tmpFile -Force
|
260
|
+
}
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
if ($outputValue.ShouldConstrain -and $IncludeScriptBlock) {
|
265
|
+
# If the script is untrusted and a scriptblock was requested we
|
266
|
+
# error out as the sbk would have run in FLM.
|
267
|
+
$err = [ErrorRecord]::new(
|
268
|
+
[Exception]::new("Provided script for '$Name' is not trusted to run."),
|
269
|
+
"ScriptNotTrusted",
|
270
|
+
[ErrorCategory]::SecurityError,
|
271
|
+
$Name)
|
272
|
+
$PSCmdlet.ThrowTerminatingError($err)
|
273
|
+
}
|
274
|
+
else {
|
275
|
+
$outputValue
|
180
276
|
}
|
181
277
|
}
|
182
278
|
|
@@ -223,7 +319,7 @@ begin {
|
|
223
319
|
$IncludeScriptBlock
|
224
320
|
)
|
225
321
|
|
226
|
-
$
|
322
|
+
$scriptInfo = Get-AnsibleScript -Name exec_wrapper.ps1 -IncludeScriptBlock:$IncludeScriptBlock
|
227
323
|
$params = @{
|
228
324
|
# TempPath may contain env vars that change based on the runtime
|
229
325
|
# environment. Ensure we use that and not the $script:AnsibleTempPath
|
@@ -244,8 +340,7 @@ begin {
|
|
244
340
|
}
|
245
341
|
|
246
342
|
[PSCustomObject]@{
|
247
|
-
|
248
|
-
ScriptBlock = $sbk.ScriptBlock
|
343
|
+
ScriptInfo = $scriptInfo
|
249
344
|
Parameters = $params
|
250
345
|
InputData = $inputData
|
251
346
|
}
|
@@ -279,11 +374,16 @@ begin {
|
|
279
374
|
|
280
375
|
$isBasicUtil = $false
|
281
376
|
$csharpModules = foreach ($moduleName in $Name) {
|
282
|
-
|
377
|
+
$scriptInfo = Get-AnsibleScript -Name $moduleName
|
283
378
|
|
379
|
+
if ($scriptInfo.ShouldConstrain) {
|
380
|
+
throw "C# module util '$Name' is not trusted and cannot be loaded."
|
381
|
+
}
|
284
382
|
if ($moduleName -eq 'Ansible.Basic.cs') {
|
285
383
|
$isBasicUtil = $true
|
286
384
|
}
|
385
|
+
|
386
|
+
$scriptInfo.Script
|
287
387
|
}
|
288
388
|
|
289
389
|
$fakeModule = [PSCustomObject]@{
|
@@ -303,6 +403,112 @@ begin {
|
|
303
403
|
}
|
304
404
|
}
|
305
405
|
|
406
|
+
Function Import-SignedHashList {
|
407
|
+
[CmdletBinding()]
|
408
|
+
param (
|
409
|
+
[Parameter(Mandatory, ValueFromPipeline)]
|
410
|
+
[string]
|
411
|
+
$Name
|
412
|
+
)
|
413
|
+
|
414
|
+
process {
|
415
|
+
try {
|
416
|
+
# We skip the hash check to ensure we verify based on the
|
417
|
+
# authenticode signature and not whether it's trusted by an
|
418
|
+
# existing signed hash list.
|
419
|
+
$scriptInfo = Get-AnsibleScript -Name $Name -SkipHashCheck
|
420
|
+
if ($scriptInfo.ShouldConstrain) {
|
421
|
+
throw "script is not signed or not trusted to run."
|
422
|
+
}
|
423
|
+
|
424
|
+
$hashListAst = [Parser]::ParseInput(
|
425
|
+
$scriptInfo.Script,
|
426
|
+
$Name,
|
427
|
+
[ref]$null,
|
428
|
+
[ref]$null)
|
429
|
+
$manifestAst = $hashListAst.Find({ $args[0] -is [HashtableAst] }, $false)
|
430
|
+
if ($null -eq $manifestAst) {
|
431
|
+
throw "expecting a single hashtable in the signed manifest."
|
432
|
+
}
|
433
|
+
|
434
|
+
$out = $manifestAst.SafeGetValue()
|
435
|
+
if (-not $out.Contains('Version')) {
|
436
|
+
throw "expecting hash list to contain 'Version' key."
|
437
|
+
}
|
438
|
+
if ($out.Version -ne 1) {
|
439
|
+
throw "unsupported hash list Version $($out.Version), expecting 1."
|
440
|
+
}
|
441
|
+
|
442
|
+
if (-not $out.Contains('HashList')) {
|
443
|
+
throw "expecting hash list to contain 'HashList' key."
|
444
|
+
}
|
445
|
+
|
446
|
+
$out.HashList | ForEach-Object {
|
447
|
+
if ($_ -isnot [hashtable] -or -not $_.ContainsKey('Hash') -or $_.Hash -isnot [string] -or $_.Hash.Length -ne 64) {
|
448
|
+
throw "expecting hash list to contain hashtable with Hash key with a value of a SHA256 strings."
|
449
|
+
}
|
450
|
+
|
451
|
+
if ($_.Mode -eq 'Trusted') {
|
452
|
+
$null = $Script:AnsibleTrustedHashList.Add($_.Hash)
|
453
|
+
}
|
454
|
+
elseif ($_.Mode -eq 'Unsupported') {
|
455
|
+
# Allows us to provide a better error when trying to run
|
456
|
+
# something in CLM that is marked as unsupported.
|
457
|
+
$null = $Script:AnsibleUnsupportedHashList.Add($_.Hash)
|
458
|
+
}
|
459
|
+
else {
|
460
|
+
throw "expecting hash list entry for $($_.Hash) to contain a mode of 'Trusted' or 'Unsupported' but got '$($_.Mode)'."
|
461
|
+
}
|
462
|
+
}
|
463
|
+
}
|
464
|
+
catch {
|
465
|
+
$_.ErrorDetails = [ErrorDetails]::new("Failed to process signed manifest '$Name': $_")
|
466
|
+
$PSCmdlet.WriteError($_)
|
467
|
+
}
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
Function New-TempAnsibleFile {
|
472
|
+
[OutputType([string])]
|
473
|
+
[CmdletBinding()]
|
474
|
+
param (
|
475
|
+
[Parameter(Mandatory)]
|
476
|
+
[string]
|
477
|
+
$FileName,
|
478
|
+
|
479
|
+
[Parameter(Mandatory)]
|
480
|
+
[string]
|
481
|
+
$Content
|
482
|
+
)
|
483
|
+
|
484
|
+
$name = [Path]::GetFileNameWithoutExtension($FileName)
|
485
|
+
$ext = [Path]::GetExtension($FileName)
|
486
|
+
$newName = "$($name)-$([Guid]::NewGuid())$ext"
|
487
|
+
|
488
|
+
$path = Join-Path -Path $Script:AnsibleTempPath $newName
|
489
|
+
Set-WinPSDefaultFileEncoding
|
490
|
+
[File]::WriteAllText($path, $Content, [UTF8Encoding]::new($false))
|
491
|
+
|
492
|
+
$path
|
493
|
+
}
|
494
|
+
|
495
|
+
Function Set-WinPSDefaultFileEncoding {
|
496
|
+
[CmdletBinding()]
|
497
|
+
param ()
|
498
|
+
|
499
|
+
# WinPS defaults to the locale encoding when loading a script from the
|
500
|
+
# file path but in Ansible we expect it to always be UTF-8 without a
|
501
|
+
# BOM. This lazily sets an internal field so pwsh reads it as UTF-8.
|
502
|
+
# If we don't do this then scripts saved as UTF-8 on the Ansible
|
503
|
+
# controller will not run as expected.
|
504
|
+
if ($PSVersionTable.PSVersion -lt '6.0' -and -not $Script:AnsibleClrFacadeSet) {
|
505
|
+
$clrFacade = [PSObject].Assembly.GetType('System.Management.Automation.ClrFacade')
|
506
|
+
$defaultEncodingField = $clrFacade.GetField('_defaultEncoding', [BindingFlags]'NonPublic, Static')
|
507
|
+
$defaultEncodingField.SetValue($null, [UTF8Encoding]::new($false))
|
508
|
+
$Script:AnsibleClrFacadeSet = $true
|
509
|
+
}
|
510
|
+
}
|
511
|
+
|
306
512
|
Function Write-AnsibleErrorJson {
|
307
513
|
[CmdletBinding()]
|
308
514
|
param (
|
@@ -414,6 +620,10 @@ begin {
|
|
414
620
|
$Script:AnsibleManifest = $Manifest
|
415
621
|
}
|
416
622
|
|
623
|
+
if ($Script:AnsibleShouldConstrain) {
|
624
|
+
$Script:AnsibleManifest.signed_hashlist | Import-SignedHashList
|
625
|
+
}
|
626
|
+
|
417
627
|
$actionInfo = Get-NextAnsibleAction
|
418
628
|
$actionParams = $actionInfo.Parameters
|
419
629
|
|
@@ -500,5 +710,8 @@ end {
|
|
500
710
|
}
|
501
711
|
finally {
|
502
712
|
$actionPipeline.Dispose()
|
713
|
+
if ($Script:AnsibleTempScripts) {
|
714
|
+
Remove-Item -LiteralPath $Script:AnsibleTempScripts -Force -ErrorAction Ignore
|
715
|
+
}
|
503
716
|
}
|
504
717
|
}
|
@@ -30,6 +30,7 @@ from ansible.plugins.loader import ps_module_utils_loader
|
|
30
30
|
class _ExecManifest:
|
31
31
|
scripts: dict[str, _ScriptInfo] = dataclasses.field(default_factory=dict)
|
32
32
|
actions: list[_ManifestAction] = dataclasses.field(default_factory=list)
|
33
|
+
signed_hashlist: list[str] = dataclasses.field(default_factory=list)
|
33
34
|
|
34
35
|
|
35
36
|
@dataclasses.dataclass(frozen=True, kw_only=True)
|
@@ -54,6 +55,11 @@ class PSModuleDepFinder(object):
|
|
54
55
|
def __init__(self) -> None:
|
55
56
|
# This is also used by validate-modules to get a module's required utils in base and a collection.
|
56
57
|
self.scripts: dict[str, _ScriptInfo] = {}
|
58
|
+
self.signed_hashlist: set[str] = set()
|
59
|
+
|
60
|
+
if builtin_hashlist := _get_powershell_signed_hashlist():
|
61
|
+
self.signed_hashlist.add(builtin_hashlist.path)
|
62
|
+
self.scripts[builtin_hashlist.path] = builtin_hashlist
|
57
63
|
|
58
64
|
self._util_deps: dict[str, set[str]] = {}
|
59
65
|
|
@@ -119,6 +125,15 @@ class PSModuleDepFinder(object):
|
|
119
125
|
lines = module_data.split(b'\n')
|
120
126
|
module_utils: set[tuple[str, str, bool]] = set()
|
121
127
|
|
128
|
+
if fqn and fqn.startswith("ansible_collections."):
|
129
|
+
submodules = fqn.split('.')
|
130
|
+
collection_name = '.'.join(submodules[:3])
|
131
|
+
|
132
|
+
collection_hashlist = _get_powershell_signed_hashlist(collection_name)
|
133
|
+
if collection_hashlist and collection_hashlist.path not in self.signed_hashlist:
|
134
|
+
self.signed_hashlist.add(collection_hashlist.path)
|
135
|
+
self.scripts[collection_hashlist.path] = collection_hashlist
|
136
|
+
|
122
137
|
if powershell:
|
123
138
|
checks = [
|
124
139
|
# PS module contains '#Requires -Module Ansible.ModuleUtils.*'
|
@@ -315,6 +330,10 @@ def _bootstrap_powershell_script(
|
|
315
330
|
)
|
316
331
|
)
|
317
332
|
|
333
|
+
if hashlist := _get_powershell_signed_hashlist():
|
334
|
+
exec_manifest.signed_hashlist.append(hashlist.path)
|
335
|
+
exec_manifest.scripts[hashlist.path] = hashlist
|
336
|
+
|
318
337
|
bootstrap_wrapper = _get_powershell_script("bootstrap_wrapper.ps1")
|
319
338
|
bootstrap_input = _get_bootstrap_input(exec_manifest)
|
320
339
|
if has_input:
|
@@ -339,6 +358,14 @@ def _get_powershell_script(
|
|
339
358
|
if code is None:
|
340
359
|
raise AnsibleFileNotFound(f"Could not find powershell script '{package_name}.{name}'")
|
341
360
|
|
361
|
+
try:
|
362
|
+
sig_data = pkgutil.get_data(package_name, f"{name}.authenticode")
|
363
|
+
except FileNotFoundError:
|
364
|
+
sig_data = None
|
365
|
+
|
366
|
+
if sig_data:
|
367
|
+
code = code + b"\r\n" + b"\r\n".join(sig_data.splitlines()) + b"\r\n"
|
368
|
+
|
342
369
|
return code
|
343
370
|
|
344
371
|
|
@@ -501,6 +528,7 @@ def _create_powershell_wrapper(
|
|
501
528
|
exec_manifest = _ExecManifest(
|
502
529
|
scripts=finder.scripts,
|
503
530
|
actions=actions,
|
531
|
+
signed_hashlist=list(finder.signed_hashlist),
|
504
532
|
)
|
505
533
|
|
506
534
|
return _get_bootstrap_input(
|
@@ -551,3 +579,27 @@ def _prepare_module_args(module_args: dict[str, t.Any], profile: str) -> dict[st
|
|
551
579
|
encoder = get_module_encoder(profile, Direction.CONTROLLER_TO_MODULE)
|
552
580
|
|
553
581
|
return json.loads(json.dumps(module_args, cls=encoder))
|
582
|
+
|
583
|
+
|
584
|
+
def _get_powershell_signed_hashlist(
|
585
|
+
collection: str | None = None,
|
586
|
+
) -> _ScriptInfo | None:
|
587
|
+
"""Gets the signed hashlist script stored in either the Ansible package or for
|
588
|
+
the collection specified.
|
589
|
+
|
590
|
+
:param collection: The collection namespace to get the signed hashlist for or None for the builtin.
|
591
|
+
:return: The _ScriptInfo payload of the signed hashlist script if found, None if not.
|
592
|
+
"""
|
593
|
+
resource = 'ansible.config' if collection is None else f"{collection}.meta"
|
594
|
+
signature_file = 'powershell_signatures.psd1'
|
595
|
+
|
596
|
+
try:
|
597
|
+
sig_data = pkgutil.get_data(resource, signature_file)
|
598
|
+
except FileNotFoundError:
|
599
|
+
sig_data = None
|
600
|
+
|
601
|
+
if sig_data:
|
602
|
+
resource_path = f"{resource}.{signature_file}"
|
603
|
+
return _ScriptInfo(content=sig_data, path=resource_path)
|
604
|
+
|
605
|
+
return None
|
@@ -97,6 +97,12 @@ $ps = [PowerShell]::Create()
|
|
97
97
|
if ($ForModule) {
|
98
98
|
$ps.Runspace.SessionStateProxy.SetVariable("ErrorActionPreference", "Stop")
|
99
99
|
}
|
100
|
+
else {
|
101
|
+
# For script files we want to ensure we load it as UTF-8. We don't set this
|
102
|
+
# for modules as they are loaded from memory whereas a script is loaded
|
103
|
+
# from disk as part of the script being run than by us.
|
104
|
+
Set-WinPSDefaultFileEncoding
|
105
|
+
}
|
100
106
|
|
101
107
|
foreach ($variable in $Variables) {
|
102
108
|
$null = $ps.AddCommand("Set-Variable").AddParameters($variable).AddStatement()
|
@@ -112,12 +118,31 @@ foreach ($env in $Environment.GetEnumerator()) {
|
|
112
118
|
$null = $ps.AddScript('Function Write-Host($msg) { Write-Output -InputObject $msg }').AddStatement()
|
113
119
|
|
114
120
|
$scriptInfo = Get-AnsibleScript -Name $Script
|
121
|
+
if ($scriptInfo.ShouldConstrain) {
|
122
|
+
# Fail if there are any module utils, in the future we may allow unsigned
|
123
|
+
# PowerShell utils in CLM but for now we don't.
|
124
|
+
if ($PowerShellModules -or $CSharpModules) {
|
125
|
+
throw "Cannot run untrusted PowerShell script '$Script' in ConstrainedLanguage mode with module util imports."
|
126
|
+
}
|
115
127
|
|
116
|
-
|
117
|
-
|
118
|
-
|
128
|
+
# If the module is marked as needing to be constrained then we set the
|
129
|
+
# language mode to ConstrainedLanguage so that when parsed inside the
|
130
|
+
# Runspace it will run in CLM. We need to run it from a filepath as in
|
131
|
+
# CLM we cannot call the methods needed to create the ScriptBlock and we
|
132
|
+
# need to be in CLM to downgrade the language mode.
|
133
|
+
$null = $ps.AddScript('$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"').AddStatement()
|
134
|
+
$scriptPath = New-TempAnsibleFile -FileName $Script -Content $scriptInfo.Script
|
135
|
+
$null = $ps.AddCommand($scriptPath, $false).AddStatement()
|
136
|
+
}
|
137
|
+
else {
|
138
|
+
if ($PowerShellModules) {
|
139
|
+
foreach ($utilName in $PowerShellModules) {
|
140
|
+
$utilInfo = Get-AnsibleScript -Name $utilName
|
141
|
+
if ($utilInfo.ShouldConstrain) {
|
142
|
+
throw "PowerShell module util '$utilName' is not trusted and cannot be loaded."
|
143
|
+
}
|
119
144
|
|
120
|
-
|
145
|
+
$null = $ps.AddScript(@'
|
121
146
|
param ($Name, $Script)
|
122
147
|
|
123
148
|
$moduleName = [System.IO.Path]::GetFileNameWithoutExtension($Name)
|
@@ -130,32 +155,33 @@ $sbk = [System.Management.Automation.Language.Parser]::ParseInput(
|
|
130
155
|
New-Module -Name $moduleName -ScriptBlock $sbk |
|
131
156
|
Import-Module -WarningAction SilentlyContinue -Scope Global
|
132
157
|
'@, $true)
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
158
|
+
$null = $ps.AddParameters(
|
159
|
+
@{
|
160
|
+
Name = $utilName
|
161
|
+
Script = $utilInfo.Script
|
162
|
+
}
|
163
|
+
).AddStatement()
|
164
|
+
}
|
139
165
|
}
|
140
|
-
}
|
141
166
|
|
142
|
-
if ($CSharpModules) {
|
143
|
-
|
144
|
-
|
145
|
-
}
|
167
|
+
if ($CSharpModules) {
|
168
|
+
# C# utils are process wide so just load them here.
|
169
|
+
Import-CSharpUtil -Name $CSharpModules
|
170
|
+
}
|
146
171
|
|
147
|
-
# We invoke it through a command with useLocalScope $false to
|
148
|
-
# ensure the code runs with it's own $script: scope. It also
|
149
|
-
# cleans up the StackTrace on errors by not showing the stub
|
150
|
-
# execution line and starts immediately at the module "cmd".
|
151
|
-
$null = $ps.AddScript(@'
|
172
|
+
# We invoke it through a command with useLocalScope $false to
|
173
|
+
# ensure the code runs with it's own $script: scope. It also
|
174
|
+
# cleans up the StackTrace on errors by not showing the stub
|
175
|
+
# execution line and starts immediately at the module "cmd".
|
176
|
+
$null = $ps.AddScript(@'
|
152
177
|
${function:<AnsibleModule>} = [System.Management.Automation.Language.Parser]::ParseInput(
|
153
178
|
$args[0],
|
154
179
|
$args[1],
|
155
180
|
[ref]$null,
|
156
181
|
[ref]$null).GetScriptBlock()
|
157
182
|
'@).AddArgument($scriptInfo.Script).AddArgument($Script).AddStatement()
|
158
|
-
$null = $ps.AddCommand('<AnsibleModule>', $false).AddStatement()
|
183
|
+
$null = $ps.AddCommand('<AnsibleModule>', $false).AddStatement()
|
184
|
+
}
|
159
185
|
|
160
186
|
if ($Breakpoints) {
|
161
187
|
$ps.Runspace.Debugger.SetBreakpoints($Breakpoints)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# (c) 2025 Ansible Project
|
2
|
+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
|
4
|
+
[CmdletBinding()]
|
5
|
+
param (
|
6
|
+
[Parameter(Mandatory)]
|
7
|
+
[string]
|
8
|
+
$Path
|
9
|
+
)
|
10
|
+
|
11
|
+
$userProfile = [Environment]::GetFolderPath([Environment+SpecialFolder]::UserProfile)
|
12
|
+
if ($Path -eq '~') {
|
13
|
+
$userProfile
|
14
|
+
}
|
15
|
+
elseif ($Path.StartsWith(('~\'))) {
|
16
|
+
Join-Path -Path $userProfile -ChildPath $Path.Substring(2)
|
17
|
+
}
|
18
|
+
else {
|
19
|
+
$Path
|
20
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# (c) 2025 Ansible Project
|
2
|
+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
3
|
+
|
4
|
+
[CmdletBinding()]
|
5
|
+
param (
|
6
|
+
[Parameter(Mandatory)]
|
7
|
+
[string]
|
8
|
+
$Directory,
|
9
|
+
|
10
|
+
[Parameter(Mandatory)]
|
11
|
+
[string]
|
12
|
+
$Name
|
13
|
+
)
|
14
|
+
|
15
|
+
$path = [Environment]::ExpandEnvironmentVariables($Directory)
|
16
|
+
$tmp = New-Item -Path $path -Name $Name -ItemType Directory
|
17
|
+
$tmp.FullName
|