1 #!powershell
2 
3 # Copyright: (c) 2017, Andrew Saraceni <andrew.saraceni@gmail.com>
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #Requires -Module Ansible.ModuleUtils.Legacy
7 
8 $ErrorActionPreference = "Stop"
9 
Get-EventLogDetail()10 function Get-EventLogDetail {
11     <#
12     .SYNOPSIS
13     Get details of an event log, sources, and associated attributes.
14     Used for comparison against passed-in option values to ensure idempotency.
15     #>
16     param(
17         [String]$LogName
18     )
19 
20     $log_details = @{}
21     $log_details.name = $LogName
22     $log_details.exists = $false
23     $log = Get-EventLog -List | Where-Object {$_.Log -eq $LogName}
24 
25     if ($log) {
26         $log_details.exists = $true
27         $log_details.maximum_size_kb = $log.MaximumKilobytes
28         $log_details.overflow_action = $log.OverflowAction.ToString()
29         $log_details.retention_days = $log.MinimumRetentionDays
30         $log_details.entries = $log.Entries.Count
31         $log_details.sources = [Ordered]@{}
32 
33         # Retrieve existing sources and category/message/parameter file locations
34         # Associating file locations and sources with logs can only be done from the registry
35 
36         $root_key = "HKLM:\SYSTEM\CurrentControlSet\Services\EventLog\{0}" -f $LogName
37         $log_root = Get-ChildItem -Path $root_key
38 
39         foreach ($child in $log_root) {
40             $source_name = $child.PSChildName
41             $log_details.sources.$source_name = @{}
42             $hash_cursor = $log_details.sources.$source_name
43 
44             $source_root = "{0}\{1}" -f $root_key, $source_name
45             $resource_files = Get-ItemProperty -Path $source_root
46 
47             $hash_cursor.category_file = $resource_files.CategoryMessageFile
48             $hash_cursor.message_file = $resource_files.EventMessageFile
49             $hash_cursor.parameter_file = $resource_files.ParameterMessageFile
50         }
51     }
52 
53     return $log_details
54 }
55 
Test-SourceExistence()56 function Test-SourceExistence {
57     <#
58     .SYNOPSIS
59     Get information on a source's existence.
60     Examine existence regarding the parent log it belongs to and its expected state.
61     #>
62     param(
63         [String]$LogName,
64         [String]$SourceName,
65         [Switch]$NoLogShouldExist
66     )
67 
68     $source_exists = [System.Diagnostics.EventLog]::SourceExists($SourceName)
69 
70     if ($source_exists -and $NoLogShouldExist) {
71         Fail-Json -obj $result -message "Source $SourceName already exists and cannot be created"
72     }
73     elseif ($source_exists) {
74         $source_log = [System.Diagnostics.EventLog]::LogNameFromSourceName($SourceName, ".")
75         if ($source_log -ne $LogName) {
76             Fail-Json -obj $result -message "Source $SourceName does not belong to log $LogName and cannot be modified"
77         }
78     }
79 
80     return $source_exists
81 }
82 
ConvertTo-MaximumSizenull83 function ConvertTo-MaximumSize {
84     <#
85     .SYNOPSIS
86     Convert a string KB/MB/GB value to common bytes and KB representations.
87     .NOTES
88     Size must be between 64KB and 4GB and divisible by 64KB, as per the MaximumSize parameter of Limit-EventLog.
89     #>
90     param(
91         [String]$Size
92     )
93 
94     $parsed_size = @{
95         bytes = $null
96         KB = $null
97     }
98 
99     $size_regex = "^\d+(\.\d+)?(KB|MB|GB)$"
100     if ($Size -notmatch $size_regex) {
101         Fail-Json -obj $result -message "Maximum size $Size is not properly specified"
102     }
103 
104     $size_upper = $Size.ToUpper()
105     $size_numeric = [Double]$Size.Substring(0, $Size.Length -2)
106 
107     if ($size_upper.EndsWith("GB")) {
108         $size_bytes = $size_numeric * 1GB
109     }
110     elseif ($size_upper.EndsWith("MB")) {
111         $size_bytes = $size_numeric * 1MB
112     }
113     elseif ($size_upper.EndsWith("KB")) {
114         $size_bytes = $size_numeric * 1KB
115     }
116 
117     if (($size_bytes -lt 64KB) -or ($size_bytes -ge 4GB)) {
118         Fail-Json -obj $result -message "Maximum size must be between 64KB and 4GB"
119     }
120     elseif (($size_bytes % 64KB) -ne 0) {
121         Fail-Json -obj $result -message "Maximum size must be divisible by 64KB"
122     }
123 
124     $parsed_size.bytes = $size_bytes
125     $parsed_size.KB = $size_bytes / 1KB
126     return $parsed_size
127 }
128 
129 $params = Parse-Args $args -supports_check_mode $true
130 $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
131 
132 $name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true
133 $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "present","clear","absent"
134 $sources = Get-AnsibleParam -obj $params -name "sources" -type "list"
135 $category_file = Get-AnsibleParam -obj $params -name "category_file" -type "path"
136 $message_file = Get-AnsibleParam -obj $params -name "message_file" -type "path"
137 $parameter_file = Get-AnsibleParam -obj $params -name "parameter_file" -type "path"
138 $maximum_size = Get-AnsibleParam -obj $params -name "maximum_size" -type "str"
139 $overflow_action = Get-AnsibleParam -obj $params -name "overflow_action" -type "str" -validateset "OverwriteOlder","OverwriteAsNeeded","DoNotOverwrite"
140 $retention_days = Get-AnsibleParam -obj $params -name "retention_days" -type "int"
141 
142 $result = @{
143     changed = $false
144     name = $name
145     sources_changed = @()
146 }
147 
148 $log_details = Get-EventLogDetail -LogName $name
149 
150 # Handle common error cases up front
151 if ($state -eq "present" -and !$log_details.exists -and !$sources) {
152     # When creating a log, one or more sources must be passed
153     Fail-Json -obj $result -message "You must specify one or more sources when creating a log for the first time"
154 }
155 elseif ($state -eq "present" -and $log_details.exists -and $name -in $sources -and ($category_file -or $message_file -or $parameter_file)) {
156     # After a default source of the same name is created, it cannot be modified without removing the log
157     Fail-Json -obj $result -message "Cannot modify default source $name of log $name - you must remove the log"
158 }
159 elseif ($state -eq "clear" -and !$log_details.exists) {
160     Fail-Json -obj $result -message "Cannot clear log $name as it does not exist"
161 }
162 elseif ($state -eq "absent" -and $name -in $sources) {
163     # You also cannot remove a default source for the log - you must remove the log itself
164     Fail-Json -obj $result -message "Cannot remove default source $name from log $name - you must remove the log"
165 }
166 
167 try {
168     switch ($state) {
169         "present" {
170             foreach ($source in $sources) {
171                 if ($log_details.exists) {
172                     $source_exists = Test-SourceExistence -LogName $name -SourceName $source
173                 }
174                 else {
175                     $source_exists = Test-SourceExistence -LogName $name -SourceName $source -NoLogShouldExist
176                 }
177 
178                 if ($source_exists) {
179                     $category_change = $category_file -and $log_details.sources.$source.category_file -ne $category_file
180                     $message_change = $message_file -and $log_details.sources.$source.message_file -ne $message_file
181                     $parameter_change = $parameter_file -and $log_details.sources.$source.parameter_file -ne $parameter_file
182                     # Remove source and recreate later if any of the above are true
183                     if ($category_change -or $message_change -or $parameter_change) {
184                         Remove-EventLog -Source $source -WhatIf:$check_mode
185                     }
186                     else {
187                         continue
188                     }
189                 }
190 
191                 $new_params = @{
192                     LogName = $name
193                     Source = $source
194                 }
195                 if ($category_file) {
196                     $new_params.CategoryResourceFile = $category_file
197                 }
198                 if ($message_file) {
199                     $new_params.MessageResourceFile = $message_file
200                 }
201                 if ($parameter_file) {
202                     $new_params.ParameterResourceFile = $parameter_file
203                 }
204 
205                 if (!$check_mode) {
206                     New-EventLog @new_params
207                     $result.sources_changed += $source
208                 }
209                 $result.changed = $true
210             }
211 
212             if ($maximum_size) {
213                 $converted_size = ConvertTo-MaximumSize -Size $maximum_size
214             }
215 
216             $size_change = $maximum_size -and $log_details.maximum_size_kb -ne $converted_size.KB
217             $overflow_change = $overflow_action -and $log_details.overflow_action -ne $overflow_action
218             $retention_change = $retention_days -and $log_details.retention_days -ne $retention_days
219 
220             if ($size_change -or $overflow_change -or $retention_change) {
221                 $limit_params = @{
222                     LogName = $name
223                     WhatIf = $check_mode
224                 }
225                 if ($maximum_size) {
226                     $limit_params.MaximumSize = $converted_size.bytes
227                 }
228                 if ($overflow_action) {
229                     $limit_params.OverflowAction = $overflow_action
230                 }
231                 if ($retention_days) {
232                     $limit_params.RetentionDays = $retention_days
233                 }
234 
235                 Limit-EventLog @limit_params
236                 $result.changed = $true
237             }
238 
239         }
240         "clear" {
241             if ($log_details.entries -gt 0) {
242                 Clear-EventLog -LogName $name -WhatIf:$check_mode
243                 $result.changed = $true
244             }
245         }
246         "absent" {
247             if ($sources -and $log_details.exists) {
248                 # Since sources were passed, remove sources tied to event log
249                 foreach ($source in $sources) {
250                     $source_exists = Test-SourceExistence -LogName $name -SourceName $source
251                     if ($source_exists) {
252                         Remove-EventLog -Source $source -WhatIf:$check_mode
253                         if (!$check_mode) {
254                             $result.sources_changed += $source
255                         }
256                         $result.changed = $true
257                     }
258                 }
259             }
260             elseif ($log_details.exists) {
261                 # Only name passed, so remove event log itself (which also removes contained sources)
262                 Remove-EventLog -LogName $name -WhatIf:$check_mode
263                 if (!$check_mode) {
264                     $log_details.sources.GetEnumerator() | ForEach-Object { $result.sources_changed += $_.Name }
265                 }
266                 $result.changed = $true
267             }
268         }
269     }
270 }
271 catch {
272     Fail-Json -obj $result -message $_.Exception.Message
273 }
274 
275 $final_log_details = Get-EventLogDetail -LogName $name
276 foreach ($final_log_detail in $final_log_details.GetEnumerator()) {
277     if ($final_log_detail.Name -eq "sources") {
278         $sources = @()
279         $final_log_detail.Value.GetEnumerator() | ForEach-Object { $sources += $_.Name }
280         $result.$($final_log_detail.Name) = [Array]$sources
281     }
282     else {
283         $result.$($final_log_detail.Name) = $final_log_detail.Value
284     }
285 }
286 
287 Exit-Json -obj $result
288