File: win_regedit.ps1

package info (click to toggle)
ansible-core 2.19.0~beta6-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 32,628 kB
  • sloc: python: 180,313; cs: 4,929; sh: 4,601; xml: 34; makefile: 21
file content (495 lines) | stat: -rw-r--r-- 18,891 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
#!powershell

# Copyright: (c) 2015, Adam Keech <akeech@chathamfinancial.com>
# Copyright: (c) 2015, Josh Ludwig <jludwig@chathamfinancial.com>
# Copyright: (c) 2017, Jordan Borean <jborean93@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

#Requires -Module Ansible.ModuleUtils.Legacy
#Requires -Module Ansible.ModuleUtils.PrivilegeUtil

$params = Parse-Args -arguments $args -supports_check_mode $true
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false
$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP

$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true -aliases "key"
$name = Get-AnsibleParam -obj $params -name "name" -type "str" -aliases "entry","value"
$data = Get-AnsibleParam -obj $params -name "data"
$type = Get-AnsibleParam -obj $params -name "type" -type "str" -default "string" -validateset "none","binary","dword","expandstring","multistring","string","qword" -aliases "datatype"
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","absent"
$delete_key = Get-AnsibleParam -obj $params -name "delete_key" -type "bool" -default $true
$hive = Get-AnsibleParam -obj $params -name "hive" -type "path"

$result = @{
    changed = $false
    data_changed = $false
    data_type_changed = $false
}

if ($diff_mode) {
    $result.diff = @{
        before = ""
        after = ""
    }
}

$registry_util = @'
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;

namespace Ansible.WinRegedit
{
    internal class NativeMethods
    {
        [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
        public static extern int RegLoadKeyW(
            UInt32 hKey,
            string lpSubKey,
            string lpFile);

        [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
        public static extern int RegUnLoadKeyW(
            UInt32 hKey,
            string lpSubKey);
    }

    public class Win32Exception : System.ComponentModel.Win32Exception
    {
        private string _msg;
        public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
        public Win32Exception(int errorCode, string message) : base(errorCode)
        {
            _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
        }
        public override string Message { get { return _msg; } }
        public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
    }

    public class Hive : IDisposable
    {
        private const UInt32 SCOPE = 0x80000002;  // HKLM
        private string hiveKey;
        private bool loaded = false;

        public Hive(string hiveKey, string hivePath)
        {
            this.hiveKey = hiveKey;
            int ret = NativeMethods.RegLoadKeyW(SCOPE, hiveKey, hivePath);
            if (ret != 0)
                throw new Win32Exception(ret, String.Format("Failed to load registry hive at {0}", hivePath));
            loaded = true;
        }

        public static void UnloadHive(string hiveKey)
        {
            int ret = NativeMethods.RegUnLoadKeyW(SCOPE, hiveKey);
            if (ret != 0)
                throw new Win32Exception(ret, String.Format("Failed to unload registry hive at {0}", hiveKey));
        }

        public void Dispose()
        {
            if (loaded)
            {
                // Make sure the garbage collector disposes all unused handles and waits until it is complete
                GC.Collect();
                GC.WaitForPendingFinalizers();

                UnloadHive(hiveKey);
                loaded = false;
            }
            GC.SuppressFinalize(this);
        }
        ~Hive() { this.Dispose(); }
    }
}
'@

# fire a warning if the property name isn't specified, the (Default) key ($null) can only be a string
if ($null -eq $name -and $type -ne "string") {
    Add-Warning -obj $result -message "the data type when name is not specified can only be 'string', the type has automatically been converted"
    $type = "string"
}

# Check that the registry path is in PSDrive format: HKCC, HKCR, HKCU, HKLM, HKU
if ($path -notmatch "^HK(CC|CR|CU|LM|U):\\") {
    Fail-Json $result "path: $path is not a valid powershell path, see module documentation for examples."
}

# Add a warning if the path does not contains a \ and is not the leaf path
$registry_path = (Split-Path -Path $path -NoQualifier).Substring(1)  # removes the hive: and leading \
$registry_leaf = Split-Path -Path $path -Leaf
if ($registry_path -ne $registry_leaf -and -not $registry_path.Contains('\')) {
    $msg = "path is not using '\' as a separator, support for '/' as a separator will be removed in a future Ansible version"
    Add-DeprecationWarning -obj $result -message $msg -version 2.12
    $registry_path = $registry_path.Replace('/', '\')
}

# Simplified version of Convert-HexStringToByteArray from
# https://cyber-defense.sans.org/blog/2010/02/11/powershell-byte-array-hex-convert
# Expects a hex in the format you get when you run reg.exe export,
# and converts to a byte array so powershell can modify binary registry entries
# import format is like 'hex:be,ef,be,ef,be,ef,be,ef,be,ef'
Function Convert-RegExportHexStringToByteArray($string) {
    # Remove 'hex:' from the front of the string if present
    $string = $string.ToLower() -replace '^hex\:',''

    # Remove whitespace and any other non-hex crud.
    $string = $string -replace '[^a-f0-9\\,x\-\:]',''

    # Turn commas into colons
    $string = $string -replace ',',':'

    # Maybe there's nothing left over to convert...
    if ($string.Length -eq 0) {
        return ,@()
    }

    # Split string with or without colon delimiters.
    if ($string.Length -eq 1) {
        return ,@([System.Convert]::ToByte($string,16))
    } elseif (($string.Length % 2 -eq 0) -and ($string.IndexOf(":") -eq -1)) {
        return ,@($string -split '([a-f0-9]{2})' | foreach-object { if ($_) {[System.Convert]::ToByte($_,16)}})
    } elseif ($string.IndexOf(":") -ne -1) {
        return ,@($string -split ':+' | foreach-object {[System.Convert]::ToByte($_,16)})
    } else {
        return ,@()
    }
}

Function Compare-RegistryProperties($existing, $new) {
    # Outputs $true if the property values don't match
    if ($existing -is [Array]) {
        (Compare-Object -ReferenceObject $existing -DifferenceObject $new -SyncWindow 0).Length -ne 0
    } else {
        $existing -cne $new
    }
}

Function Get-DiffValue {
    param(
        [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryValueKind]$Type,
        [Parameter(Mandatory=$true)][Object]$Value
    )

    $diff = @{ type = $Type.ToString(); value = $Value }

    $enum = [Microsoft.Win32.RegistryValueKind]
    if ($Type -in @($enum::Binary, $enum::None)) {
        $diff.value = [System.Collections.Generic.List`1[String]]@()
        foreach ($dec_value in $Value) {
            $diff.value.Add("0x{0:x2}" -f $dec_value)
        }
    } elseif ($Type -eq $enum::DWord) {
        $diff.value = "0x{0:x8}" -f $Value
    } elseif ($Type -eq $enum::QWord) {
        $diff.value = "0x{0:x16}" -f $Value
    }

    return $diff
}

Function Set-StateAbsent {
    param(
        # Used for diffs and exception messages to match up against Ansible input
        [Parameter(Mandatory=$true)][String]$PrintPath,
        [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
        [Parameter(Mandatory=$true)][String]$Path,
        [String]$Name,
        [Switch]$DeleteKey
    )

    $key = $Hive.OpenSubKey($Path, $true)
    if ($null -eq $key) {
        # Key does not exist, no need to delete anything
        return
    }

    try {
        if ($DeleteKey -and -not $Name) {
            # delete_key=yes is set and name is null/empty, so delete the entire key
            $key.Dispose()
            $key = $null
            if (-not $check_mode) {
                try {
                    $Hive.DeleteSubKeyTree($Path, $false)
                } catch {
                    Fail-Json -obj $result -message "failed to delete registry key at $($PrintPath): $($_.Exception.Message)"
                }
            }
            $result.changed = $true

            if ($diff_mode) {
                $result.diff.before = @{$PrintPath = @{}}
                $result.diff.after = @{}
            }
        } else {
            # delete_key=no or name is not null/empty, delete the property not the full key
            $property = $key.GetValue($Name)
            if ($null -eq $property) {
                # property does not exist
                return
            }
            $property_type = $key.GetValueKind($Name)  # used for the diff

            if (-not $check_mode) {
                try {
                    $key.DeleteValue($Name)
                } catch {
                    Fail-Json -obj $result -message "failed to delete registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
                }
            }

            $result.changed = $true
            if ($diff_mode) {
                $diff_value = Get-DiffValue -Type $property_type -Value $property
                $result.diff.before = @{ $PrintPath = @{ $Name = $diff_value } }
                $result.diff.after = @{ $PrintPath = @{} }
            }
        }
    } finally {
        if ($key) {
            $key.Dispose()
        }
    }
}

Function Set-StatePresent {
    param(
        [Parameter(Mandatory=$true)][String]$PrintPath,
        [Parameter(Mandatory=$true)][Microsoft.Win32.RegistryKey]$Hive,
        [Parameter(Mandatory=$true)][String]$Path,
        [String]$Name,
        [Object]$Data,
        [Microsoft.Win32.RegistryValueKind]$Type
    )

    $key = $Hive.OpenSubKey($Path, $true)
    try {
        if ($null -eq $key) {
            # the key does not exist, create it so the next steps work
            if (-not $check_mode) {
                try {
                    $key = $Hive.CreateSubKey($Path)
                } catch {
                    Fail-Json -obj $result -message "failed to create registry key at $($PrintPath): $($_.Exception.Message)"
                }
            }
            $result.changed = $true

            if ($diff_mode) {
                $result.diff.before = @{}
                $result.diff.after = @{$PrintPath = @{}}
            }
        } elseif ($diff_mode) {
            # Make sure the diff is in an expected state for the key
            $result.diff.before = @{$PrintPath = @{}}
            $result.diff.after = @{$PrintPath = @{}}
        }

        if ($null -eq $key -or $null -eq $Data) {
            # Check mode and key was created above, we cannot do any more work, or $Data is $null which happens when
            # we create a new key but haven't explicitly set the data
            return
        }

        $property = $key.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
        if ($null -ne $property) {
            # property exists, need to compare the values and type
            $existing_type = $key.GetValueKind($name)
            $change_value = $false

            if ($Type -ne $existing_type) {
                $change_value = $true
                $result.data_type_changed = $true
                $data_mismatch = Compare-RegistryProperties -existing $property -new $Data
                if ($data_mismatch) {
                    $result.data_changed = $true
                }
            } else {
                $data_mismatch = Compare-RegistryProperties -existing $property -new $Data
                if ($data_mismatch) {
                    $change_value = $true
                    $result.data_changed = $true
                }
            }

            if ($change_value) {
                if (-not $check_mode) {
                    try {
                        $key.SetValue($Name, $Data, $Type)
                    } catch {
                        Fail-Json -obj $result -message "failed to change registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
                    }
                }
                $result.changed = $true

                if ($diff_mode) {
                    $result.diff.before.$PrintPath.$Name = Get-DiffValue -Type $existing_type -Value $property
                    $result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
                }
            } elseif ($diff_mode) {
                $diff_value = Get-DiffValue -Type $existing_type -Value $property
                $result.diff.before.$PrintPath.$Name = $diff_value
                $result.diff.after.$PrintPath.$Name = $diff_value
            }
        } else {
            # property doesn't exist just create a new one
            if (-not $check_mode) {
                try {
                    $key.SetValue($Name, $Data, $Type)
                } catch {
                    Fail-Json -obj $result -message "failed to create registry property '$Name' at $($PrintPath): $($_.Exception.Message)"
                }
            }
            $result.changed = $true

            if ($diff_mode) {
                $result.diff.after.$PrintPath.$Name = Get-DiffValue -Type $Type -Value $Data
            }
        }
    } finally {
        if ($key) {
            $key.Dispose()
        }
    }
}

# convert property names "" to $null as "" refers to (Default)
if ($name -eq "") {
    $name = $null
}

# convert the data to the required format
if ($type -in @("binary", "none")) {
    if ($null -eq $data) {
        $data = ""
    }

    # convert the data from string to byte array if in hex: format
    if ($data -is [String]) {
        $data = [byte[]](Convert-RegExportHexStringToByteArray -string $data)
    } elseif ($data -is [Int]) {
        if ($data -gt 255) {
            Fail-Json $result "cannot convert binary data '$data' to byte array, please specify this value as a yaml byte array or a comma separated hex value string"
        }
        $data = [byte[]]@([byte]$data)
    } elseif ($data -is [Array]) {
        $data = [byte[]]$data
    }
} elseif ($type -in @("dword", "qword")) {
    # dword's and dword's don't allow null values, set to 0
    if ($null -eq $data) {
        $data = 0
    }

    if ($data -is [String]) {
        # if the data is a string we need to convert it to an unsigned int64
        # it needs to be unsigned as Ansible passes in an unsigned value while
        # powershell uses a signed data type. The value will then be converted
        # below
        $data = [UInt64]$data
    }

    if ($type -eq "dword") {
        if ($data -gt [UInt32]::MaxValue) {
            Fail-Json $result "data cannot be larger than 0xffffffff when type is dword"
        } elseif ($data -gt [Int32]::MaxValue) {
            # when dealing with larger int32 (> 2147483647 or 0x7FFFFFFF) powershell
            # automatically converts it to a signed int64. We need to convert this to
            # signed int32 by parsing the hex string value.
            $data = "0x$("{0:x}" -f $data)"
        }
        $data = [Int32]$data
    } else {
        if ($data -gt [UInt64]::MaxValue) {
            Fail-Json $result "data cannot be larger than 0xffffffffffffffff when type is qword"
        } elseif ($data -gt [Int64]::MaxValue) {
            $data = "0x$("{0:x}" -f $data)"
        }
        $data = [Int64]$data
    }
} elseif ($type -in @("string", "expandstring") -and $name) {
    # a null string or expandstring must be empty quotes
    # Only do this if $name has been defined (not the default key)
    if ($null -eq $data) {
        $data = ""
    }
} elseif ($type -eq "multistring") {
    # convert the data for a multistring to a String[] array
    if ($null -eq $data) {
        $data = [String[]]@()
    } elseif ($data -isnot [Array]) {
        $new_data = New-Object -TypeName String[] -ArgumentList 1
        $new_data[0] = $data.ToString([CultureInfo]::InvariantCulture)
        $data = $new_data
    } else {
        $new_data = New-Object -TypeName String[] -ArgumentList $data.Count
        foreach ($entry in $data) {
            $new_data[$data.IndexOf($entry)] = $entry.ToString([CultureInfo]::InvariantCulture)
        }
        $data = $new_data
    }
}

# convert the type string to the .NET class
$type = [System.Enum]::Parse([Microsoft.Win32.RegistryValueKind], $type, $true)

$registry_hive = switch(Split-Path -Path $path -Qualifier) {
    "HKCR:" { [Microsoft.Win32.Registry]::ClassesRoot }
    "HKCC:" { [Microsoft.Win32.Registry]::CurrentConfig }
    "HKCU:" { [Microsoft.Win32.Registry]::CurrentUser }
    "HKLM:" { [Microsoft.Win32.Registry]::LocalMachine }
    "HKU:" { [Microsoft.Win32.Registry]::Users }
}
$loaded_hive = $null
try {
    if ($hive) {
        if (-not (Test-Path -LiteralPath $hive)) {
            Fail-Json -obj $result -message "hive at path '$hive' is not valid or accessible, cannot load hive"
        }

        $original_tmp = $env:TMP
        $env:TMP = $_remote_tmp
        Add-Type -TypeDefinition $registry_util
        $env:TMP = $original_tmp

        try {
            Set-AnsiblePrivilege -Name SeBackupPrivilege -Value $true
            Set-AnsiblePrivilege -Name SeRestorePrivilege -Value $true
        } catch [System.ComponentModel.Win32Exception] {
            Fail-Json -obj $result -message "failed to enable SeBackupPrivilege and SeRestorePrivilege for the current process: $($_.Exception.Message)"
        }

        if (Test-Path -Path HKLM:\ANSIBLE) {
            Add-Warning -obj $result -message "hive already loaded at HKLM:\ANSIBLE, had to unload hive for win_regedit to continue"
            try {
                [Ansible.WinRegedit.Hive]::UnloadHive("ANSIBLE")
            } catch [System.ComponentModel.Win32Exception] {
                Fail-Json -obj $result -message "failed to unload registry hive HKLM:\ANSIBLE from $($hive): $($_.Exception.Message)"
            }
        }

        try {
            $loaded_hive = New-Object -TypeName Ansible.WinRegedit.Hive -ArgumentList "ANSIBLE", $hive
        } catch [System.ComponentModel.Win32Exception] {
            Fail-Json -obj $result -message "failed to load registry hive from '$hive' to HKLM:\ANSIBLE: $($_.Exception.Message)"
        }
    }

    if ($state -eq "present") {
        Set-StatePresent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -Data $data -Type $type
    } else {
        Set-StateAbsent -PrintPath $path -Hive $registry_hive -Path $registry_path -Name $name -DeleteKey:$delete_key
    }
} finally {
    $registry_hive.Dispose()
    if ($loaded_hive) {
        $loaded_hive.Dispose()
    }
}

Exit-Json $result