ansible-core 2.19.0b3__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 +2 -2
- 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 +6 -6
- 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/_testing.py +26 -0
- 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 +8 -5
- ansible/cli/doc.py +84 -28
- ansible/cli/inventory.py +1 -1
- ansible/compat/importlib_resources.py +9 -12
- ansible/config/base.yml +27 -23
- ansible/config/manager.py +142 -101
- ansible/constants.py +1 -1
- 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 -0
- 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 +2 -2
- ansible/module_utils/parsing/convert_bool.py +6 -0
- ansible/module_utils/service.py +2 -9
- ansible/modules/apt_repository.py +7 -29
- ansible/modules/assemble.py +4 -4
- ansible/modules/async_status.py +13 -11
- ansible/modules/async_wrapper.py +5 -5
- ansible/modules/cron.py +3 -5
- ansible/modules/dnf5.py +15 -22
- ansible/modules/git.py +1 -6
- ansible/modules/hostname.py +0 -1
- ansible/modules/pip.py +2 -4
- ansible/modules/service.py +3 -9
- ansible/modules/sysvinit.py +3 -3
- 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 +9 -32
- 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 +6 -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 +10 -9
- 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/collection_loader/__init__.py +2 -0
- ansible/utils/display.py +107 -128
- ansible/utils/hashing.py +0 -1
- ansible/utils/listify.py +6 -4
- ansible/utils/plugin_docs.py +2 -1
- ansible/utils/unsafe_proxy.py +1 -1
- ansible/vars/hostvars.py +1 -1
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
- {ansible_core-2.19.0b3.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 +73 -8
- ansible_test/_util/target/setup/bootstrap.sh +31 -0
- ansible/_internal/_errors/_utils.py +0 -310
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
- {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/top_level.txt +0 -0
@@ -40,6 +40,7 @@ from ansible._internal import _locking
|
|
40
40
|
from ansible._internal._datatag import _utils
|
41
41
|
from ansible.module_utils._internal import _dataclass_validation
|
42
42
|
from ansible.module_utils.common.yaml import yaml_load
|
43
|
+
from ansible.module_utils.datatag import deprecator_from_collection_name
|
43
44
|
from ansible._internal._datatag._tags import Origin
|
44
45
|
from ansible.module_utils.common.json import Direction, get_module_encoder
|
45
46
|
from ansible.release import __version__, __author__
|
@@ -55,7 +56,6 @@ from ansible.template import Templar
|
|
55
56
|
from ansible.utils.collection_loader._collection_finder import _get_collection_metadata, _nested_dict_get
|
56
57
|
from ansible.module_utils._internal import _json, _ansiballz
|
57
58
|
from ansible.module_utils import basic as _basic
|
58
|
-
from ansible.module_utils.common import messages as _messages
|
59
59
|
|
60
60
|
if t.TYPE_CHECKING:
|
61
61
|
from ansible import template as _template
|
@@ -166,7 +166,7 @@ NEW_STYLE_PYTHON_MODULE_RE = re.compile(
|
|
166
166
|
|
167
167
|
|
168
168
|
class ModuleDepFinder(ast.NodeVisitor):
|
169
|
-
# DTFIX-
|
169
|
+
# DTFIX-FUTURE: add support for ignoring imports with a "controller only" comment, this will allow replacing import_controller_module with standard imports
|
170
170
|
def __init__(self, module_fqn, tree, is_pkg_init=False, *args, **kwargs):
|
171
171
|
"""
|
172
172
|
Walk the ast tree for the python module.
|
@@ -439,7 +439,7 @@ class ModuleUtilLocatorBase:
|
|
439
439
|
version=removal_version,
|
440
440
|
removed=removed,
|
441
441
|
date=removal_date,
|
442
|
-
deprecator=
|
442
|
+
deprecator=deprecator_from_collection_name(self._collection_name),
|
443
443
|
)
|
444
444
|
if 'redirect' in routing_entry:
|
445
445
|
self.redirected = True
|
@@ -618,7 +618,7 @@ class CollectionModuleUtilLocator(ModuleUtilLocatorBase):
|
|
618
618
|
if pkg_path:
|
619
619
|
origin = Origin(path=os.path.join(pkg_path, src_path))
|
620
620
|
else:
|
621
|
-
# DTFIX-
|
621
|
+
# DTFIX-FUTURE: not sure if this case is even reachable
|
622
622
|
origin = Origin(description=f'<synthetic collection package for {collection_pkg_name}!r>')
|
623
623
|
|
624
624
|
self.source_code = origin.tag(src)
|
@@ -658,7 +658,7 @@ metadata_versions: dict[t.Any, type[ModuleMetadata]] = {
|
|
658
658
|
|
659
659
|
|
660
660
|
def _get_module_metadata(module: ast.Module) -> ModuleMetadata:
|
661
|
-
#
|
661
|
+
# DTFIX2: while module metadata works, this feature isn't fully baked and should be turned off before release
|
662
662
|
metadata_nodes: list[ast.Assign] = []
|
663
663
|
|
664
664
|
for node in module.body:
|
@@ -928,7 +928,7 @@ class _BuiltModule:
|
|
928
928
|
class _CachedModule:
|
929
929
|
"""Cached Python module created by AnsiballZ."""
|
930
930
|
|
931
|
-
#
|
931
|
+
# DTFIX5: secure this (locked down pickle, don't use pickle, etc.)
|
932
932
|
|
933
933
|
zip_data: bytes
|
934
934
|
metadata: ModuleMetadata
|
@@ -991,10 +991,8 @@ def _find_module_utils(
|
|
991
991
|
module_substyle = 'powershell'
|
992
992
|
b_module_data = b_module_data.replace(REPLACER_WINDOWS, b'#AnsibleRequires -PowerShell Ansible.ModuleUtils.Legacy')
|
993
993
|
elif re.search(b'#Requires -Module', b_module_data, re.IGNORECASE) \
|
994
|
-
or re.search(b'#Requires -Version', b_module_data, re.IGNORECASE)\
|
995
|
-
or re.search(b'#AnsibleRequires -OSVersion', b_module_data, re.IGNORECASE)
|
996
|
-
or re.search(b'#AnsibleRequires -Powershell', b_module_data, re.IGNORECASE) \
|
997
|
-
or re.search(b'#AnsibleRequires -CSharpUtil', b_module_data, re.IGNORECASE):
|
994
|
+
or re.search(b'#Requires -Version', b_module_data, re.IGNORECASE) \
|
995
|
+
or re.search(b'#AnsibleRequires -(OSVersion|PowerShell|CSharpUtil|Wrapper)', b_module_data, re.IGNORECASE):
|
998
996
|
module_style = 'new'
|
999
997
|
module_substyle = 'powershell'
|
1000
998
|
elif REPLACER_JSONARGS in b_module_data:
|
@@ -40,7 +40,7 @@ param([ScriptBlock]$ScriptBlock, $Param)
|
|
40
40
|
& $ScriptBlock.Ast.GetScriptBlock() @Param
|
41
41
|
'@).AddParameters(
|
42
42
|
@{
|
43
|
-
ScriptBlock = $execInfo.ScriptBlock
|
43
|
+
ScriptBlock = $execInfo.ScriptInfo.ScriptBlock
|
44
44
|
Param = $execInfo.Parameters
|
45
45
|
})
|
46
46
|
|
@@ -64,7 +64,7 @@ $jobError = $null
|
|
64
64
|
try {
|
65
65
|
$jobAsyncResult = $ps.BeginInvoke($pipelineInput, $invocationSettings, $null, $null)
|
66
66
|
$jobAsyncResult.AsyncWaitHandle.WaitOne($Timeout * 1000) > $null
|
67
|
-
$result.finished =
|
67
|
+
$result.finished = $true
|
68
68
|
|
69
69
|
if ($jobAsyncResult.IsCompleted) {
|
70
70
|
$jobOutput = $ps.EndInvoke($jobAsyncResult)
|
@@ -113,7 +113,7 @@ try {
|
|
113
113
|
}
|
114
114
|
$execWrapper = @{
|
115
115
|
name = 'exec_wrapper-async.ps1'
|
116
|
-
script = $execAction.Script
|
116
|
+
script = $execAction.ScriptInfo.Script
|
117
117
|
params = $execAction.Parameters
|
118
118
|
} | ConvertTo-Json -Compress -Depth 99
|
119
119
|
$asyncInput = "$execWrapper`n`0`0`0`0`n$($execAction.InputData)"
|
@@ -135,8 +135,8 @@ try {
|
|
135
135
|
# We need to write the result file before the process is started to ensure
|
136
136
|
# it can read the file.
|
137
137
|
$result = @{
|
138
|
-
started =
|
139
|
-
finished =
|
138
|
+
started = $true
|
139
|
+
finished = $false
|
140
140
|
results_file = $resultsPath
|
141
141
|
ansible_job_id = $localJid
|
142
142
|
_ansible_suppress_tmpdir_delete = $true
|
@@ -7,6 +7,7 @@ using namespace System.Collections
|
|
7
7
|
using namespace System.Diagnostics
|
8
8
|
using namespace System.IO
|
9
9
|
using namespace System.Management.Automation
|
10
|
+
using namespace System.Management.Automation.Security
|
10
11
|
using namespace System.Net
|
11
12
|
using namespace System.Text
|
12
13
|
|
@@ -53,7 +54,7 @@ $executablePath = Join-Path -Path $PSHome -ChildPath $executable
|
|
53
54
|
$actionInfo = Get-AnsibleExecWrapper -EncodeInputOutput
|
54
55
|
$bootstrapManifest = ConvertTo-Json -InputObject @{
|
55
56
|
n = "exec_wrapper-become-$([Guid]::NewGuid()).ps1"
|
56
|
-
s = $actionInfo.Script
|
57
|
+
s = $actionInfo.ScriptInfo.Script
|
57
58
|
p = $actionInfo.Parameters
|
58
59
|
} -Depth 99 -Compress
|
59
60
|
|
@@ -68,9 +69,26 @@ $m=foreach($i in $input){
|
|
68
69
|
$m=$m|ConvertFrom-Json
|
69
70
|
$p=@{}
|
70
71
|
foreach($o in $m.p.PSObject.Properties){$p[$o.Name]=$o.Value}
|
72
|
+
'@
|
73
|
+
|
74
|
+
if ([SystemPolicy]::GetSystemLockdownPolicy() -eq 'Enforce') {
|
75
|
+
# If we started in CLM we need to execute the script from a file so that
|
76
|
+
# PowerShell validates our exec_wrapper is trusted and will run in FLM.
|
77
|
+
$command += @'
|
78
|
+
$n=Join-Path $env:TEMP $m.n
|
79
|
+
$null=New-Item $n -Value $m.s -Type File -Force
|
80
|
+
try{$input|&$n @p}
|
81
|
+
finally{if(Test-Path -LiteralPath $n){Remove-Item -LiteralPath $n -Force}}
|
82
|
+
'@
|
83
|
+
}
|
84
|
+
else {
|
85
|
+
# If we started in FLM we pass the script through stdin and execute in
|
86
|
+
# memory.
|
87
|
+
$command += @'
|
71
88
|
$c=[System.Management.Automation.Language.Parser]::ParseInput($m.s,$m.n,[ref]$null,[ref]$null).GetScriptBlock()
|
72
|
-
$input
|
89
|
+
$input|&$c @p
|
73
90
|
'@
|
91
|
+
}
|
74
92
|
|
75
93
|
# Strip out any leading or trailing whitespace and remove empty lines.
|
76
94
|
$command = @(
|
@@ -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
|