From 8e05d7d962d6ffc41d07df7f143fd39f02885716 Mon Sep 17 00:00:00 2001
From: Jordan Borean <jborean93@gmail.com>
Date: Sat, 15 Jul 2017 04:00:29 +1000
Subject: [PATCH] win_secedit: Added module with tests/diff mode (#26332)

* win_secedit: Added module with tests/diff mode

* fixed up test issues

* Added missing return value

* change for win_secedit based on review

* updated win_security_policy examples for rename
---
 .../modules/windows/win_security_policy.ps1   | 203 ++++++++++++++++++
 .../modules/windows/win_security_policy.py    | 130 +++++++++++
 .../targets/win_security_policy/aliases       |   1 +
 .../library/test_win_security_policy.ps1      |  53 +++++
 .../win_security_policy/tasks/main.yml        |  41 ++++
 .../win_security_policy/tasks/tests.yml       | 133 ++++++++++++
 6 files changed, 561 insertions(+)
 create mode 100644 lib/ansible/modules/windows/win_security_policy.ps1
 create mode 100644 lib/ansible/modules/windows/win_security_policy.py
 create mode 100644 test/integration/targets/win_security_policy/aliases
 create mode 100644 test/integration/targets/win_security_policy/library/test_win_security_policy.ps1
 create mode 100644 test/integration/targets/win_security_policy/tasks/main.yml
 create mode 100644 test/integration/targets/win_security_policy/tasks/tests.yml

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 <jborean93@gmail.com>
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+# 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 <http://www.gnu.org/licenses/>.
+
+# 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"