diff --git a/lib/ansible/modules/windows/win_security_policy.ps1 b/lib/ansible/modules/windows/win_security_policy.ps1 new file mode 100644 index 00000000000..2381d0e4b91 --- /dev/null +++ b/lib/ansible/modules/windows/win_security_policy.ps1 @@ -0,0 +1,203 @@ +#!powershell +# This file is part of Ansible +# +# Copyright 2017, Jordan Borean +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +$ErrorActionPreference = 'Stop' + +$params = Parse-Args $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 + +$section = Get-AnsibleParam -obj $params -name "section" -type "str" -failifempty $true +$key = Get-AnsibleParam -obj $params -name "key" -type "str" -failifempty $true +$value = Get-AnsibleParam -obj $params -name "value" -failifempty $true + +$result = @{ + changed = $false + section = $section + key = $key + value = $value +} + +if ($diff_mode) { + $result.diff = @{} +} + +Function Run-SecEdit($arguments) { + $rc = $null + $stdout = $null + $stderr = $null + $log_path = [IO.Path]::GetTempFileName() + $arguments = $arguments + @("/log", $log_path) + + try { + $stdout = &SecEdit.exe $arguments | Out-String + } catch { + $stderr = $_.Exception.Message + } + $log = Get-Content -Path $log_path + Remove-Item -Path $log_path -Force + + $return = @{ + log = ($log -join "`n").Trim() + stdout = $stdout + stderr = $stderr + rc = $LASTEXITCODE + } + + return $return +} + +Function Export-SecEdit() { + $secedit_ini_path = [IO.Path]::GetTempFileName() + # while this will technically make a change to the system in check mode by + # creating a new file, we need these values to be able to do anything + # substantial in check mode + $export_result = Run-SecEdit -arguments @("/export", "/cfg", $secedit_ini_path, "/quiet") + + # check the return code and if the file has been populated, otherwise error out + if (($export_result.rc -ne 0) -or ((Get-Item -Path $secedit_ini_path).Length -eq 0)) { + Remove-Item -Path $secedit_ini_path -Force + $result.rc = $export_result.rc + $result.stdout = $export_result.stdout + $result.stderr = $export_result.stderr + Fail-Json $result "Failed to export secedit.ini file to $($secedit_ini_path)" + } + $secedit_ini = ConvertFrom-Ini -file_path $secedit_ini_path + + return $secedit_ini +} + +Function Import-SecEdit($ini) { + $secedit_ini_path = [IO.Path]::GetTempFileName() + $secedit_db_path = [IO.Path]::GetTempFileName() + Remove-Item -Path $secedit_db_path -Force # needs to be deleted for SecEdit.exe /import to work + + $ini_contents = ConvertTo-Ini -ini $ini + Set-Content -Path $secedit_ini_path -Value $ini_contents + $result.changed = $true + + $import_result = Run-SecEdit -arguments @("/configure", "/db", $secedit_db_path, "/cfg", $secedit_ini_path, "/quiet") + $result.import_log = $import_result.log + Remove-Item -Path $secedit_ini_path -Force + if ($import_result.rc -ne 0) { + $result.rc = $import_result.rc + $result.stdout = $import_result.stdout + $result.stderr = $import_result.stderr + Fail-Json $result "Failed to import secedit.ini file from $($secedit_ini_path)" + } +} + +Function ConvertTo-Ini($ini) { + $content = @() + foreach ($key in $ini.GetEnumerator()) { + $section = $key.Name + $values = $key.Value + + $content += "[$section]" + foreach ($value in $values.GetEnumerator()) { + $value_key = $value.Name + $value_value = $value.Value + + if ($value_value -ne $null) { + $content += "$value_key = $value_value" + } + } + } + + return $content -join "`r`n" +} + +Function ConvertFrom-Ini($file_path) { + $ini = @{} + switch -Regex -File $file_path { + "^\[(.+)\]" { + $section = $matches[1] + $ini.$section = @{} + } + "(.+?)\s*=(.*)" { + $name = $matches[1].Trim() + $value = $matches[2].Trim() + if ($value -match "^\d+$") { + $value = [int]$value + } elseif ($value.StartsWith('"') -and $value.EndsWith('"')) { + $value = $value.Substring(1, $value.Length - 2) + } + + $ini.$section.$name = $value + } + } + + return $ini +} + +$will_change = $false +$secedit_ini = Export-SecEdit +if (-not ($secedit_ini.ContainsKey($section))) { + Fail-Json $result "The section '$section' does not exist in SecEdit.exe output ini" +} + +if ($secedit_ini.$section.ContainsKey($key)) { + $current_value = $secedit_ini.$section.$key + + if ($current_value -cne $value) { + if ($diff_mode) { + $result.diff.prepared = @" +[$section] +-$key = $current_value ++$key = $value +"@ + } + + $secedit_ini.$section.$key = $value + $will_change = $true + } +} else { + if ($diff_mode) { + $result.diff.prepared = @" +[$section] ++$key = $value +"@ + } + $secedit_ini.$section.$key = $value + $will_change = $true +} + +if ($will_change -eq $true) { + $result.changed = $true + if (-not $check_mode) { + Import-SecEdit -ini $secedit_ini + + # secedit doesn't error out on improper entries, re-export and verify + # the changes occurred + $verification_ini = Export-SecEdit + $new_section_values = $verification_ini.$section + if ($new_section_values.ContainsKey($key)) { + $new_value = $new_section_values.$key + if ($new_value -cne $value) { + Fail-Json $result "Failed to change the value for key '$key' in section '$section', the value is still $new_value" + } + } else { + Fail-Json $result "The key '$key' in section '$section' is not a valid key, cannot set this value" + } + } +} + +Exit-Json $result diff --git a/lib/ansible/modules/windows/win_security_policy.py b/lib/ansible/modules/windows/win_security_policy.py new file mode 100644 index 00000000000..bbedd49728c --- /dev/null +++ b/lib/ansible/modules/windows/win_security_policy.py @@ -0,0 +1,130 @@ +#!/usr/bin/python +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub, actual code lives in the .ps1 +# file of the same name + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + + +DOCUMENTATION = r''' +--- +module: win_security_policy +version_added: '2.4' +short_description: changes local security policy settings +description: +- Allows you to set the local security policies that are configured by + SecEdit.exe. +notes: +- This module uses the SecEdit.exe tool to configure the values, more details + of the areas and keys that can be configured can be found here + U(https://msdn.microsoft.com/en-us/library/bb742512.aspx). +- If you are in a domain environment these policies may be set by a GPO policy, + this module can temporarily change these values but the GPO will override + it if the value differs. +- You can also run C(SecEdit.exe /export /cfg C:\temp\output.ini) to view the + current policies set on your system. +options: + section: + description: + - The ini section the key exists in. + - If the section does not exist then the module will return an error. + - Example sections to use are 'Account Policies', 'Local Policies', + 'Event Log', 'Restricted Groups', 'System Services', 'Registry' and + 'File System' + required: yes + key: + description: + - The ini key of the section or policy name to modify. + - The module will return an error if this key is invalid. + required: yes + value: + description: + - The value for the ini key or policy name. + - If the key takes in a boolean value then 0 = False and 1 = True. + required: yes +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: change the guest account name + win_security_policy: + section: System Access + key: NewGuestName + value: Guest Account + +- name: set the maximum password age + win_security_policy: + section: System Access + key: MaximumPasswordAge + value: 15 + +- name: do not store passwords using reversible encryption + win_security_policy: + section: System Access + key: ClearTextPassword + value: 0 + +- name: enable system events + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: 1 +''' + +RETURN = r''' +rc: + description: The return code after a failure when running SecEdit.exe. + returned: failure with secedit calls + type: int + sample: -1 +stdout: + description: The output of the STDOUT buffer after a failure when running + SecEdit.exe. + returned: failure with secedit calls + type: string + sample: check log for error details +stderr: + description: The output of the STDERR buffer after a failure when running + SecEdit.exe. + returned: failure with secedit calls + type: string + sample: failed to import security policy +import_log: + description: The log of the SecEdit.exe /configure job that configured the + local policies. This is used for debugging purposes on failures. + returned: secedit.exe /import run and change occurred + type: string + sample: Completed 6 percent (0/15) \tProcess Privilege Rights area. +key: + description: The key in the section passed to the module to modify. + returned: success + type: string + sample: NewGuestName +section: + description: The section passed to the module to modify. + returned: success + type: string + sample: System Access +value: + description: The value passed to the module to modify to. + returned: success + type: string + sample: Guest Account +''' diff --git a/test/integration/targets/win_security_policy/aliases b/test/integration/targets/win_security_policy/aliases new file mode 100644 index 00000000000..10e03fc2bf7 --- /dev/null +++ b/test/integration/targets/win_security_policy/aliases @@ -0,0 +1 @@ +windows/ci/group1 diff --git a/test/integration/targets/win_security_policy/library/test_win_security_policy.ps1 b/test/integration/targets/win_security_policy/library/test_win_security_policy.ps1 new file mode 100644 index 00000000000..5c83c1b5d0d --- /dev/null +++ b/test/integration/targets/win_security_policy/library/test_win_security_policy.ps1 @@ -0,0 +1,53 @@ +#!powershell + +# WANT_JSON +# POWERSHELL_COMMON + +# basic script to get the lsit of users in a particular right +# this is quite complex to put as a simple script so this is +# just a simple module + +$ErrorActionPreference = 'Stop' + +$params = Parse-Args $args -supports_check_mode $false +$section = Get-AnsibleParam -obj $params -name "section" -type "str" -failifempty $true +$key = Get-AnsibleParam -obj $params -name "key" -type "str" -failifempty $true + +$result = @{ + changed = $false +} + +Function ConvertFrom-Ini($file_path) { + $ini = @{} + switch -Regex -File $file_path { + "^\[(.+)\]" { + $section = $matches[1] + $ini.$section = @{} + } + "(.+?)\s*=(.*)" { + $name = $matches[1].Trim() + $value = $matches[2].Trim() + if ($value -match "^\d+$") { + $value = [int]$value + } elseif ($value.StartsWith('"') -and $value.EndsWith('"')) { + $value = $value.Substring(1, $value.Length - 2) + } + + $ini.$section.$name = $value + } + } + + $ini +} + +$secedit_ini_path = [IO.Path]::GetTempFileName() +&SecEdit.exe /export /cfg $secedit_ini_path /quiet +$secedit_ini = ConvertFrom-Ini -file_path $secedit_ini_path + +if ($secedit_ini.ContainsKey($section)) { + $result.value = $secedit_ini.$section.$key +} else { + $result.value = $null +} + +Exit-Json $result diff --git a/test/integration/targets/win_security_policy/tasks/main.yml b/test/integration/targets/win_security_policy/tasks/main.yml new file mode 100644 index 00000000000..28fdb5ea094 --- /dev/null +++ b/test/integration/targets/win_security_policy/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: get current entry for audit + test_win_security_policy: + section: Event Audit + key: AuditSystemEvents + register: before_value_audit + +- name: get current entry for guest + test_win_security_policy: + section: System Access + key: NewGuestName + register: before_value_guest + +- block: + - name: set AuditSystemEvents entry before tests + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: 0 + + - name: set NewGuestName entry before tests + win_security_policy: + section: System Access + key: NewGuestName + value: Guest + + - name: run tests + include_tasks: tests.yml + + always: + - name: reset entries for AuditSystemEvents + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: "{{before_value_audit.value}}" + + - name: reset entries for NewGuestName + win_security_policy: + section: System Access + key: NewGuestName + value: "{{before_value_guest.value}}" diff --git a/test/integration/targets/win_security_policy/tasks/tests.yml b/test/integration/targets/win_security_policy/tasks/tests.yml new file mode 100644 index 00000000000..6fe79df4aef --- /dev/null +++ b/test/integration/targets/win_security_policy/tasks/tests.yml @@ -0,0 +1,133 @@ +--- +- name: fail with invalid section name + win_security_policy: + section: This is not a valid section + key: KeyName + value: 0 + register: fail_invalid_section + failed_when: fail_invalid_section.msg != "The section 'This is not a valid section' does not exist in SecEdit.exe output ini" + +- name: fail with invalid key name + win_security_policy: + section: System Access + key: InvalidKey + value: 0 + register: fail_invalid_key + failed_when: fail_invalid_key.msg != "The key 'InvalidKey' in section 'System Access' is not a valid key, cannot set this value" + +- name: change existing key check + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: 1 + register: change_existing_check + check_mode: yes + +- name: get actual change existing key check + test_win_security_policy: + section: Event Audit + key: AuditSystemEvents + register: change_existing_actual_check + +- name: assert change existing key check + assert: + that: + - change_existing_check|changed + - change_existing_actual_check.value == 0 + +- name: change existing key + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: 1 + register: change_existing + +- name: get actual change existing key + test_win_security_policy: + section: Event Audit + key: AuditSystemEvents + register: change_existing_actual + +- name: assert change existing key + assert: + that: + - change_existing|changed + - change_existing_actual.value == 1 + +- name: change existing key again + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: 1 + register: change_existing_again + +- name: assert change existing key again + assert: + that: + - not change_existing_again|changed + - change_existing_again.value == 1 + +- name: change existing key with string type + win_security_policy: + section: Event Audit + key: AuditSystemEvents + value: "1" + register: change_existing_key_with_type + +- name: assert change existing key with string type + assert: + that: + - not change_existing_key_with_type|changed + - change_existing_key_with_type.value == "1" + +- name: change existing string key check + win_security_policy: + section: System Access + key: NewGuestName + value: New Guest + register: change_existing_string_check + check_mode: yes + +- name: get actual change existing string key check + test_win_security_policy: + section: System Access + key: NewGuestName + register: change_existing_string_actual_check + +- name: assert change existing string key check + assert: + that: + - change_existing_string_check|changed + - change_existing_string_actual_check.value == "Guest" + +- name: change existing string key + win_security_policy: + section: System Access + key: NewGuestName + value: New Guest + register: change_existing_string + +- name: get actual change existing string key + test_win_security_policy: + section: System Access + key: NewGuestName + register: change_existing_string_actual + +- name: assert change existing string key + assert: + that: + - change_existing_string|changed + - change_existing_string_actual.value == "New Guest" + +- name: change existing string key again + win_security_policy: + section: System Access + key: NewGuestName + value: New Guest + register: change_existing_string_again + +- name: assert change existing string key again + assert: + that: + - not change_existing_string_again|changed + - change_existing_string_again.value == "New Guest"