1 #!powershell
2 
3 # Copyright: (c) 2020, Brian Scholer <@briantist>
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #AnsibleRequires -CSharpUtil Ansible.Basic
7 
8 $spec = @{
9     supports_check_mode = $true
10     options = @{
11         source = @{
12             type = 'path'
13             default = '%LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet\PSRepositories.xml'
14         }
15         name = @{
16             type = 'list'
17             elements = 'str'
18             default = @(
19                 '*'
20             )
21         }
22         exclude = @{
23             type = 'list'
24             elements = 'str'
25         }
26         profiles = @{
27             type = 'list'
28             elements = 'str'
29             default = @(
30                 '*'
31             )
32         }
33         exclude_profiles = @{
34             type = 'list'
35             elements = 'str'
36             default = @(
37                 'systemprofile'
38                 'LocalService'
39                 'NetworkService'
40             )
41         }
42     }
43 }
44 
45 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
46 
47 $module.Diff.before = @{}
48 $module.Diff.after = @{}
49 
Select-Wildcard()50 function Select-Wildcard {
51     <#
52         .SYNOPSIS
53         Compares a value to an Include and Exclude list of wildcards,
54         returning the input object if a match is found
55 
56         .DESCRIPTION
57         If $Property is specified, that property of the input object is
58         compared rather than the object itself, but the original object
59         is returned, not the property.
60     #>
61     [CmdletBinding()]
62     [OutputType([object])]
63     param(
64         [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
65         [object]
66         $InputObject ,
67 
68         [Parameter()]
69         [String]
70         $Property ,
71 
72         [Parameter()]
73         [String[]]
74         $Include ,
75 
76         [Parameter()]
77         [String[]]
78         $Exclude
79     )
80 
81     Process {
82         $o = if ($Property) {
83             $InputObject.($Property)
84         }
85         else {
86             $InputObject
87         }
88 
89         foreach ($inc in $Include) {
90             $imatch = $o -like $inc
91             if ($imatch) {
92                 break
93             }
94         }
95 
96         if (-not $imatch) {
97             return
98         }
99 
100         foreach ($exc in $Exclude) {
101             if ($o -like $exc) {
102                 return
103             }
104         }
105 
106         $InputObject
107     }
108 }
109 
Get-ProfileDirectory()110 function Get-ProfileDirectory {
111     <#
112         .SYNOPSIS
113         Returns DirectoryInfo objects for each profile on the system, as reported by the registry
114 
115         .DESCRIPTION
116         The special "Default" profile, used as a template for newly created users, is explicitly
117         added to the list of possible profiles returned. Public is explicitly excluded.
118         Paths reported by the registry that don't exist on the filesystem are silently skipped.
119     #>
120     [CmdletBinding()]
121     [OutputType([System.IO.DirectoryInfo])]
122     param(
123         [Parameter()]
124         [String[]]
125         $Include ,
126 
127         [Parameter()]
128         [String[]]
129         $Exclude
130     )
131 
132     $regPL = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'
133 
134     # note: this is a key named "Default", not the (Default) key
135     $default = Get-ItemProperty -LiteralPath $regPL | Select-Object -ExpandProperty Default
136 
137     # "ProfileImagePath" is always the local side of the profile, even if roaming profiles are used
138     # This is what we want, because PSRepositories are stored in AppData/Local and don't roam
139     $profiles = (
140         @($default) +
141         (Get-ChildItem -LiteralPath $regPL | Get-ItemProperty | Select-Object -ExpandProperty ProfileImagePath)
142     ) -as [System.IO.DirectoryInfo[]]
143 
144     $profiles |
145         Where-Object -Property Exists -EQ $true |
146         Select-Wildcard -Property Name -Include $Include -Exclude $Exclude
147 }
148 
Compare-Hashtable()149 function Compare-Hashtable {
150     <#
151         .SYNOPSIS
152         Attempts to naively compare two hashtables by serializing them and string comparing the serialized versions
153     #>
154     [CmdletBinding()]
155     [OutputType([bool])]
156     param(
157         [Parameter(Mandatory)]
158         [hashtable]
159         $ReferenceObject ,
160 
161         [Parameter(Mandatory)]
162         [hashtable]
163         $DifferenceObject ,
164 
165         [Parameter()]
166         [int]
167         $Depth
168     )
169 
170     if ($PSBoundParameters.ContainsKey('Depth')) {
171         $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject, $Depth)
172         $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject, $Depth)
173     }
174     else {
175         $sRef = [System.Management.Automation.PSSerializer]::Serialize($ReferenceObject)
176         $sDif = [System.Management.Automation.PSSerializer]::Serialize($DifferenceObject)
177     }
178 
179     $sRef -ceq $sDif
180 }
181 
182 # load the repositories from the source file
183 try {
184     $src = $module.Params.source
185     $src_repos = Import-Clixml -LiteralPath $src -ErrorAction Stop
186 }
187 catch [System.IO.FileNotFoundException] {
188     $module.FailJson("The source file '$src' was not found.", $_)
189 }
190 catch {
191     $module.FailJson("There was an error loading the source file '$src': $($_.Exception.Message).", $_)
192 }
193 
194 $profiles = Get-ProfileDirectory -Include $module.Params.profiles -Exclude $module.Params.exclude_profiles
195 
196 foreach ($user in $profiles) {
197     $username = $user.Name
198 
199     $repo_dir = $user.FullName | Join-Path -ChildPath 'AppData\Local\Microsoft\Windows\PowerShell\PowerShellGet'
200     $repo_path = $repo_dir | Join-Path -ChildPath 'PSRepositories.xml'
201 
202     if (Test-Path -LiteralPath $repo_path) {
203         $cur_repos = Import-Clixml -LiteralPath $repo_path
204     }
205     else {
206         $cur_repos = @{}
207     }
208 
209     $new_repos = $cur_repos.Clone()
210     $updated = $false
211 
212     $src_repos.Keys |
213         Select-Wildcard -Include $module.Params.name -Exclude $module.Params.exclude |
214         ForEach-Object -Process {
215             # explicit scope used inside ForEach-Object to satisfy lint (PSUseDeclaredVarsMoreThanAssignment)
216             # see https://github.com/PowerShell/PSScriptAnalyzer/issues/827
217             $Script:updated = $true
218             $Script:new_repos[$_] = $Script:src_repos[$_]
219         }
220 
221     $module.Diff.before[$username] = $cur_repos
222     $module.Diff.after[$username] = $new_repos
223 
224     if ($updated -and -not (Compare-Hashtable -ReferenceObject $cur_repos -DifferenceObject $new_repos)) {
225         if (-not $module.CheckMode) {
226             $null = New-Item -Path $repo_dir -ItemType Directory -Force -ErrorAction SilentlyContinue
227             $new_repos | Export-Clixml -LiteralPath $repo_path -Force
228         }
229         $module.Result.changed = $true
230     }
231 }
232 
233 $module.ExitJson()
234