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.
Files changed (174) hide show
  1. ansible/_internal/__init__.py +2 -2
  2. ansible/_internal/_collection_proxy.py +1 -1
  3. ansible/_internal/_errors/_alarm_timeout.py +66 -0
  4. ansible/_internal/_errors/_captured.py +25 -30
  5. ansible/_internal/_errors/_error_factory.py +89 -0
  6. ansible/_internal/_errors/_error_utils.py +240 -0
  7. ansible/_internal/_errors/_task_timeout.py +28 -0
  8. ansible/_internal/_event_formatting.py +127 -0
  9. ansible/_internal/_json/__init__.py +6 -6
  10. ansible/_internal/_json/_profiles/_cache_persistence.py +2 -0
  11. ansible/_internal/_json/_profiles/_inventory_legacy.py +1 -1
  12. ansible/_internal/_json/_profiles/_legacy.py +3 -11
  13. ansible/_internal/_ssh/__init__.py +0 -0
  14. ansible/_internal/_ssh/_agent_launch.py +91 -0
  15. ansible/{utils → _internal/_ssh}/_ssh_agent.py +55 -93
  16. ansible/_internal/_templating/__init__.py +5 -3
  17. ansible/_internal/_templating/_datatag.py +2 -1
  18. ansible/_internal/_templating/_engine.py +3 -4
  19. ansible/_internal/_templating/_jinja_bits.py +21 -16
  20. ansible/_internal/_templating/_jinja_common.py +18 -27
  21. ansible/_internal/_templating/_jinja_plugins.py +31 -3
  22. ansible/_internal/_templating/_lazy_containers.py +5 -5
  23. ansible/_internal/_templating/_transform.py +20 -19
  24. ansible/_internal/_templating/_utils.py +1 -1
  25. ansible/_internal/_testing.py +26 -0
  26. ansible/_internal/_yaml/_dumper.py +1 -1
  27. ansible/_internal/_yaml/_errors.py +7 -7
  28. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/true_type.py +1 -1
  29. ansible/_internal/ansible_collections/ansible/_protomatter/plugins/filter/unmask.py +1 -1
  30. ansible/cli/__init__.py +5 -82
  31. ansible/cli/arguments/option_helpers.py +8 -5
  32. ansible/cli/doc.py +84 -28
  33. ansible/cli/inventory.py +1 -1
  34. ansible/compat/importlib_resources.py +9 -12
  35. ansible/config/base.yml +27 -23
  36. ansible/config/manager.py +142 -101
  37. ansible/constants.py +1 -1
  38. ansible/errors/__init__.py +96 -49
  39. ansible/executor/module_common.py +8 -10
  40. ansible/executor/powershell/async_watchdog.ps1 +2 -2
  41. ansible/executor/powershell/async_wrapper.ps1 +3 -3
  42. ansible/executor/powershell/become_wrapper.ps1 +20 -2
  43. ansible/executor/powershell/bootstrap_wrapper.ps1 +28 -6
  44. ansible/executor/powershell/coverage_wrapper.ps1 +15 -6
  45. ansible/executor/powershell/exec_wrapper.ps1 +219 -6
  46. ansible/executor/powershell/module_manifest.py +52 -0
  47. ansible/executor/powershell/module_wrapper.ps1 +47 -21
  48. ansible/executor/powershell/powershell_expand_user.ps1 +20 -0
  49. ansible/executor/powershell/powershell_mkdtemp.ps1 +17 -0
  50. ansible/executor/process/worker.py +38 -113
  51. ansible/executor/task_executor.py +26 -61
  52. ansible/executor/task_result.py +2 -4
  53. ansible/galaxy/collection/__init__.py +1 -4
  54. ansible/inventory/manager.py +1 -0
  55. ansible/module_utils/_internal/__init__.py +0 -3
  56. ansible/module_utils/_internal/_ambient_context.py +3 -3
  57. ansible/module_utils/_internal/_ansiballz.py +4 -2
  58. ansible/module_utils/_internal/_datatag/__init__.py +20 -14
  59. ansible/module_utils/_internal/_datatag/_tags.py +2 -2
  60. ansible/module_utils/_internal/_deprecator.py +66 -48
  61. ansible/module_utils/_internal/_errors.py +88 -17
  62. ansible/module_utils/_internal/_event_utils.py +61 -0
  63. ansible/module_utils/_internal/_json/_profiles/__init__.py +21 -4
  64. ansible/module_utils/_internal/_json/_profiles/_module_legacy_c2m.py +2 -0
  65. ansible/module_utils/_internal/_json/_profiles/_module_legacy_m2c.py +2 -0
  66. ansible/module_utils/_internal/_json/_profiles/_tagless.py +3 -1
  67. ansible/module_utils/{common/messages.py → _internal/_messages.py} +28 -47
  68. ansible/module_utils/_internal/_patches/_dataclass_annotation_patch.py +1 -3
  69. ansible/module_utils/_internal/_plugin_info.py +1 -1
  70. ansible/module_utils/_internal/_stack.py +22 -0
  71. ansible/module_utils/_internal/_text_utils.py +6 -0
  72. ansible/module_utils/_internal/_traceback.py +11 -8
  73. ansible/module_utils/ansible_release.py +1 -1
  74. ansible/module_utils/basic.py +49 -15
  75. ansible/module_utils/common/arg_spec.py +2 -2
  76. ansible/module_utils/common/collections.py +6 -0
  77. ansible/module_utils/common/json.py +2 -2
  78. ansible/module_utils/common/text/converters.py +3 -3
  79. ansible/module_utils/common/validation.py +1 -1
  80. ansible/module_utils/common/warnings.py +80 -23
  81. ansible/module_utils/common/yaml.py +1 -1
  82. ansible/module_utils/datatag.py +5 -2
  83. ansible/module_utils/facts/system/distribution.py +16 -3
  84. ansible/module_utils/facts/virtual/linux.py +2 -2
  85. ansible/module_utils/parsing/convert_bool.py +6 -0
  86. ansible/module_utils/service.py +2 -9
  87. ansible/modules/apt_repository.py +7 -29
  88. ansible/modules/assemble.py +4 -4
  89. ansible/modules/async_status.py +13 -11
  90. ansible/modules/async_wrapper.py +5 -5
  91. ansible/modules/cron.py +3 -5
  92. ansible/modules/dnf5.py +15 -22
  93. ansible/modules/git.py +1 -6
  94. ansible/modules/hostname.py +0 -1
  95. ansible/modules/pip.py +2 -4
  96. ansible/modules/service.py +3 -9
  97. ansible/modules/sysvinit.py +3 -3
  98. ansible/parsing/ajson.py +3 -5
  99. ansible/parsing/dataloader.py +4 -4
  100. ansible/parsing/mod_args.py +1 -1
  101. ansible/parsing/plugin_docs.py +2 -2
  102. ansible/parsing/utils/yaml.py +3 -3
  103. ansible/parsing/vault/__init__.py +4 -4
  104. ansible/playbook/playbook_include.py +1 -1
  105. ansible/playbook/taggable.py +0 -3
  106. ansible/plugins/__init__.py +0 -25
  107. ansible/plugins/action/__init__.py +9 -32
  108. ansible/plugins/action/add_host.py +1 -1
  109. ansible/plugins/action/assemble.py +8 -16
  110. ansible/plugins/action/async_status.py +7 -2
  111. ansible/plugins/action/copy.py +8 -7
  112. ansible/plugins/action/gather_facts.py +8 -8
  113. ansible/plugins/action/package.py +5 -8
  114. ansible/plugins/action/script.py +8 -15
  115. ansible/plugins/action/service.py +3 -7
  116. ansible/plugins/action/template.py +6 -8
  117. ansible/plugins/action/unarchive.py +5 -15
  118. ansible/plugins/action/uri.py +9 -20
  119. ansible/plugins/callback/__init__.py +4 -6
  120. ansible/plugins/callback/junit.py +4 -2
  121. ansible/plugins/connection/local.py +2 -2
  122. ansible/plugins/connection/ssh.py +17 -9
  123. ansible/plugins/connection/winrm.py +5 -2
  124. ansible/plugins/doc_fragments/constructed.py +2 -2
  125. ansible/plugins/filter/core.py +13 -6
  126. ansible/plugins/filter/encryption.py +4 -4
  127. ansible/plugins/inventory/__init__.py +11 -10
  128. ansible/plugins/inventory/script.py +1 -1
  129. ansible/plugins/list.py +69 -16
  130. ansible/plugins/loader.py +10 -9
  131. ansible/plugins/lookup/csvfile.py +16 -71
  132. ansible/plugins/lookup/first_found.py +2 -1
  133. ansible/plugins/shell/__init__.py +56 -2
  134. ansible/plugins/shell/powershell.py +66 -9
  135. ansible/plugins/shell/sh.py +9 -5
  136. ansible/plugins/test/core.py +21 -15
  137. ansible/plugins/test/finished.yml +1 -1
  138. ansible/plugins/test/uri.py +2 -5
  139. ansible/release.py +1 -1
  140. ansible/template/__init__.py +30 -2
  141. ansible/utils/collection_loader/__init__.py +2 -0
  142. ansible/utils/display.py +107 -128
  143. ansible/utils/hashing.py +0 -1
  144. ansible/utils/listify.py +6 -4
  145. ansible/utils/plugin_docs.py +2 -1
  146. ansible/utils/unsafe_proxy.py +1 -1
  147. ansible/vars/hostvars.py +1 -1
  148. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/METADATA +3 -2
  149. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/RECORD +173 -161
  150. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/WHEEL +1 -1
  151. ansible_test/_data/completion/docker.txt +3 -3
  152. ansible_test/_data/completion/remote.txt +1 -0
  153. ansible_test/_data/requirements/sanity.ansible-doc.txt +1 -1
  154. ansible_test/_data/requirements/sanity.changelog.txt +2 -2
  155. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  156. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  157. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  158. ansible_test/_internal/util.py +20 -0
  159. ansible_test/_util/controller/sanity/pylint/config/ansible-test-target.cfg +1 -0
  160. ansible_test/_util/controller/sanity/pylint/config/ansible-test.cfg +1 -0
  161. ansible_test/_util/controller/sanity/pylint/config/code-smell.cfg +1 -0
  162. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  163. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  164. ansible_test/_util/controller/sanity/pylint/plugins/deprecated_calls.py +73 -8
  165. ansible_test/_util/target/setup/bootstrap.sh +31 -0
  166. ansible/_internal/_errors/_utils.py +0 -310
  167. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info}/entry_points.txt +0 -0
  168. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses}/COPYING +0 -0
  169. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/Apache-License.txt +0 -0
  170. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/BSD-3-Clause.txt +0 -0
  171. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/MIT-license.txt +0 -0
  172. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/PSF-license.txt +0 -0
  173. {ansible_core-2.19.0b3.dist-info → ansible_core-2.19.0b5.dist-info/licenses/licenses}/simplified_bsd.txt +0 -0
  174. {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-RELEASE: add support for ignoring imports with a "controller only" comment, this will allow replacing import_controller_module with standard imports
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=_messages.PluginInfo._from_collection_name(self._collection_name),
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-RELEASE: not sure if this case is even reachable
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
- # DTFIX-RELEASE: while module metadata works, this feature isn't fully baked and should be turned off before release
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
- # DTFIX-RELEASE: secure this (locked down pickle, don't use pickle, etc.)
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 = 1
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 = 1
139
- finished = 0
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 | & $c @p
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
- $cmd = [System.Management.Automation.Language.Parser]::ParseInput(
22
- $code.script,
23
- "$($code.name).ps1", # Name is used in stack traces.
24
- [ref]$null,
25
- [ref]$null).GetScriptBlock()
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
- [ScriptBlock]
32
- $ScriptBlock,
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 = $ScriptBlock.Ast.FindAll($predicate, $true)
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(@($ScriptBlock.File, $cmd.Extent.StartLineNumber, $cmd.Extent.StartColumnNumber, $null))
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
- $scriptInfo = Get-AnsibleScript -Name $scriptName -IncludeScriptBlock
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
- ScriptBlock = $scriptInfo.ScriptBlock
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
- $sbk = Get-AnsibleScript -Name exec_wrapper.ps1 -IncludeScriptBlock:$IncludeScriptBlock
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
- Script = $sbk.Script
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
- (Get-AnsibleScript -Name $moduleName).Script
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