From b7293f32d02c0781d5a939fa0b5e0b5e5699eedb Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Mon, 20 Mar 2017 23:00:01 +0000 Subject: [PATCH 01/24] Windows modules (web browsers, languages and text editor) and chocolatey repository manager. --- .../languages/python/manifests/install.pp | 10 + .../windows/languages/python/python.pp | 1 + .../languages/python/secgen_metadata.xml | 20 + .../languages/ruby/manifests/install.pp | 10 + .../utilities/windows/languages/ruby/ruby.pp | 1 + .../languages/ruby/secgen_metadata.xml | 20 + .../chocolatey/CHANGELOG.md | 172 ++++ .../chocolatey/CONTRIBUTING.md | 219 +++++ .../repository_managers/chocolatey/Gemfile | 152 ++++ .../repository_managers/chocolatey/LICENSE | 202 +++++ .../chocolatey/MAINTAINERS.md | 6 + .../repository_managers/chocolatey/NOTICE | 0 .../repository_managers/chocolatey/README.md | 783 ++++++++++++++++++ .../repository_managers/chocolatey/Rakefile | 104 +++ .../chocolatey/appveyor.yml | 44 + .../chocolatey/checksums.json | 91 ++ .../chocolatey/chocolatey.pp | 1 + .../chocolatey/examples/init.pp | 23 + .../lib/facter/choco_install_path.rb | 9 + .../lib/facter/chocolateyversion.rb | 9 + .../provider/chocolateyconfig/windows.rb | 144 ++++ .../provider/chocolateyfeature/windows.rb | 126 +++ .../provider/chocolateysource/windows.rb | 197 +++++ .../lib/puppet/provider/package/chocolatey.rb | 280 +++++++ .../lib/puppet/type/chocolateyconfig.rb | 85 ++ .../lib/puppet/type/chocolateyfeature.rb | 57 ++ .../lib/puppet/type/chocolateysource.rb | 141 ++++ .../puppet_x/chocolatey/chocolatey_common.rb | 90 ++ .../puppet_x/chocolatey/chocolatey_install.rb | 34 + .../puppet_x/chocolatey/chocolatey_version.rb | 32 + .../chocolatey/manifests/config.pp | 34 + .../chocolatey/manifests/init.pp | 104 +++ .../chocolatey/manifests/install.pp | 27 + .../chocolatey/manifests/params.pp | 9 + .../chocolatey/metadata.json | 53 ++ .../chocolatey/secgen_metadata.xml | 17 + .../chocolatey/spec/classes/config_spec.rb | 77 ++ .../chocolatey/spec/classes/coverage_spec.rb | 1 + .../chocolatey/spec/classes/init_spec.rb | 200 +++++ .../chocolatey/spec/classes/install_spec.rb | 30 + .../chocolatey/spec/spec_helper.rb | 64 ++ .../unit/facter/choco_install_path_spec.rb | 32 + .../unit/facter/chocolateyversion_spec.rb | 32 + .../provider/chocolateyconfig/windows_spec.rb | 268 ++++++ .../chocolateyfeature/windows_spec.rb | 170 ++++ .../provider/chocolateysource/windows_spec.rb | 607 ++++++++++++++ .../provider/package/chocolatey_spec.rb | 514 ++++++++++++ .../unit/puppet/type/chocolateyconfig_spec.rb | 103 +++ .../puppet/type/chocolateyfeature_spec.rb | 58 ++ .../unit/puppet/type/chocolateysource_spec.rb | 129 +++ .../chocolatey/chocolatey_common_spec.rb | 71 ++ .../chocolatey/chocolatey_install_spec.rb | 52 ++ .../chocolatey/chocolatey_version_spec.rb | 81 ++ .../templates/InstallChocolatey.ps1.erb | 151 ++++ .../acceptance/pre-suite/00_pe_install.rb | 20 + .../pre-suite/01_chocolatey_module.install.rb | 25 + .../02_chocolatey_application_install.rb | 47 ++ .../tests/acceptance/tests/hello.rb | 7 + .../chocolatey/tests/configs/.gitignore | 3 + .../chocolatey/tests/lib/chocolatey_helper.rb | 50 ++ .../reference/pre-suite/00_install_certs.rb | 93 +++ .../pre-suite/01_puppet_agent_install.rb | 13 + .../pre-suite/02_chocolatey_module_install.rb | 27 + .../03_chocolatey_application_install.rb | 39 + .../chocolateyconfig/add_new_config_item.rb | 29 + .../add_value_to_existing_config.rb | 29 + .../chocolateyconfig/change_config_value.rb | 46 + ...sure_config_value_with_password_in_name.rb | 50 ++ .../fail_to_appy_bad_manifest.rb | 25 + .../fail_to_set_present_without_value.rb | 24 + ...move_config_value_with_password_in_name.rb | 50 ++ .../remove_value_from_config.rb | 28 + .../disable_disabled_feature.rb | 35 + .../disable_enabled_feature.rb | 35 + .../enable_disabled_feature.rb | 35 + .../enable_enabled_feature.rb | 35 + .../fail_to_enable_nonexistent_feature.rb | 24 + .../fail_to_remove_feature.rb | 25 + .../install_and_remove_good_package.rb | 62 ++ .../install_and_remove_good_package_utf-8.rb | 65 ++ .../add_priority_to_existing_source.rb | 30 + .../add_source_all_options.rb | 36 + .../chocolateysource/add_source_minimal.rb | 29 + .../chocolateysource/add_source_normal.rb | 30 + .../add_user_pass_to_existing_source.rb | 33 + .../change_existing_priority.rb | 51 ++ .../change_existing_source_location.rb | 30 + .../chocolateysource/change_user_pass.rb | 54 ++ .../disable_existing_source.rb | 28 + .../disable_existing_source_two_runs.rb | 43 + .../fail_to_apply_source_without_location.rb | 24 + .../fail_to_appy_bad_manifest.rb | 25 + .../fail_to_set_password_without_user.rb | 26 + .../fail_to_set_user_without_password.rb | 26 + .../remove_existing_source.rb | 28 + .../remove_priority_from_existing_source.rb | 50 ++ .../remove_user_pass_from_existing_source.rb | 53 ++ .../test_run_scripts/acceptance_tests.sh | 68 ++ .../tests/test_run_scripts/reference_tests.sh | 65 ++ .../notepadplusplus/manifests/install.pp | 12 + .../notepadplusplus/notepadplusplus.pp | 1 + .../notepadplusplus/secgen_metadata.xml | 20 + .../windows/web_browsers/firefox/firefox.pp | 1 + .../web_browsers/firefox/manifests/install.pp | 10 + .../web_browsers/firefox/secgen_metadata.xml | 20 + .../google_chrome/google_chrome.pp | 1 + .../google_chrome/manifests/configure.pp | 13 + .../google_chrome/manifests/install.pp | 10 + .../google_chrome/secgen_metadata.xml | 20 + .../internet_explorer_11.pp | 1 + .../internet_explorer_11/manifests/install.pp | 10 + .../internet_explorer_11/secgen_metadata.xml | 20 + 112 files changed, 7736 insertions(+) create mode 100644 modules/utilities/windows/languages/python/manifests/install.pp create mode 100644 modules/utilities/windows/languages/python/python.pp create mode 100644 modules/utilities/windows/languages/python/secgen_metadata.xml create mode 100644 modules/utilities/windows/languages/ruby/manifests/install.pp create mode 100644 modules/utilities/windows/languages/ruby/ruby.pp create mode 100644 modules/utilities/windows/languages/ruby/secgen_metadata.xml create mode 100644 modules/utilities/windows/repository_managers/chocolatey/CHANGELOG.md create mode 100644 modules/utilities/windows/repository_managers/chocolatey/CONTRIBUTING.md create mode 100644 modules/utilities/windows/repository_managers/chocolatey/Gemfile create mode 100644 modules/utilities/windows/repository_managers/chocolatey/LICENSE create mode 100644 modules/utilities/windows/repository_managers/chocolatey/MAINTAINERS.md create mode 100644 modules/utilities/windows/repository_managers/chocolatey/NOTICE create mode 100644 modules/utilities/windows/repository_managers/chocolatey/README.md create mode 100644 modules/utilities/windows/repository_managers/chocolatey/Rakefile create mode 100644 modules/utilities/windows/repository_managers/chocolatey/appveyor.yml create mode 100644 modules/utilities/windows/repository_managers/chocolatey/checksums.json create mode 100644 modules/utilities/windows/repository_managers/chocolatey/chocolatey.pp create mode 100644 modules/utilities/windows/repository_managers/chocolatey/examples/init.pp create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/facter/choco_install_path.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/facter/chocolateyversion.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyconfig/windows.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyfeature/windows.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateysource/windows.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/package/chocolatey.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyconfig.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyfeature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateysource.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_common.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_version.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/manifests/config.pp create mode 100644 modules/utilities/windows/repository_managers/chocolatey/manifests/init.pp create mode 100644 modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp create mode 100644 modules/utilities/windows/repository_managers/chocolatey/manifests/params.pp create mode 100644 modules/utilities/windows/repository_managers/chocolatey/metadata.json create mode 100644 modules/utilities/windows/repository_managers/chocolatey/secgen_metadata.xml create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/classes/config_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/classes/coverage_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/classes/init_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/classes/install_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/spec_helper.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/choco_install_path_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/chocolateyversion_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyconfig/windows_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyfeature/windows_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateysource/windows_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/package/chocolatey_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyconfig_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyfeature_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateysource_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_common_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_install_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_version_spec.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/templates/InstallChocolatey.ps1.erb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/00_pe_install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/01_chocolatey_module.install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/02_chocolatey_application_install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/tests/hello.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/configs/.gitignore create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/lib/chocolatey_helper.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/00_install_certs.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/01_puppet_agent_install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/02_chocolatey_module_install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/03_chocolatey_application_install.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_new_config_item.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_value_to_existing_config.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/change_config_value.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/ensure_config_value_with_password_in_name.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_appy_bad_manifest.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_set_present_without_value.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_config_value_with_password_in_name.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_value_from_config.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_disabled_feature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_enabled_feature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_disabled_feature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_enabled_feature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_enable_nonexistent_feature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_remove_feature.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package_utf-8.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_priority_to_existing_source.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_all_options.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_minimal.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_normal.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_user_pass_to_existing_source.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_priority.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_source_location.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_user_pass.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source_two_runs.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_apply_source_without_location.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_appy_bad_manifest.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_password_without_user.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_user_without_password.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_existing_source.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_priority_from_existing_source.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_user_pass_from_existing_source.rb create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/acceptance_tests.sh create mode 100644 modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/reference_tests.sh create mode 100644 modules/utilities/windows/text_editor/notepadplusplus/manifests/install.pp create mode 100644 modules/utilities/windows/text_editor/notepadplusplus/notepadplusplus.pp create mode 100644 modules/utilities/windows/text_editor/notepadplusplus/secgen_metadata.xml create mode 100644 modules/utilities/windows/web_browsers/firefox/firefox.pp create mode 100644 modules/utilities/windows/web_browsers/firefox/manifests/install.pp create mode 100644 modules/utilities/windows/web_browsers/firefox/secgen_metadata.xml create mode 100644 modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp create mode 100644 modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp create mode 100644 modules/utilities/windows/web_browsers/google_chrome/manifests/install.pp create mode 100644 modules/utilities/windows/web_browsers/google_chrome/secgen_metadata.xml create mode 100644 modules/utilities/windows/web_browsers/internet_explorer_11/internet_explorer_11.pp create mode 100644 modules/utilities/windows/web_browsers/internet_explorer_11/manifests/install.pp create mode 100644 modules/utilities/windows/web_browsers/internet_explorer_11/secgen_metadata.xml diff --git a/modules/utilities/windows/languages/python/manifests/install.pp b/modules/utilities/windows/languages/python/manifests/install.pp new file mode 100644 index 000000000..74e75b016 --- /dev/null +++ b/modules/utilities/windows/languages/python/manifests/install.pp @@ -0,0 +1,10 @@ +class python::install { + include chocolatey + + notice('Installing python') + + package { 'python': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/languages/python/python.pp b/modules/utilities/windows/languages/python/python.pp new file mode 100644 index 000000000..0a11b98f1 --- /dev/null +++ b/modules/utilities/windows/languages/python/python.pp @@ -0,0 +1 @@ +include python::install \ No newline at end of file diff --git a/modules/utilities/windows/languages/python/secgen_metadata.xml b/modules/utilities/windows/languages/python/secgen_metadata.xml new file mode 100644 index 000000000..79fb6d3c3 --- /dev/null +++ b/modules/utilities/windows/languages/python/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Python programming language + Jason Keighley + Apache v2 + A Python installation + + languages + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/languages/ruby/manifests/install.pp b/modules/utilities/windows/languages/ruby/manifests/install.pp new file mode 100644 index 000000000..6a21fd72d --- /dev/null +++ b/modules/utilities/windows/languages/ruby/manifests/install.pp @@ -0,0 +1,10 @@ +class ruby::install { + include chocolatey + + notice('Installing ruby') + + package { 'ruby': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/languages/ruby/ruby.pp b/modules/utilities/windows/languages/ruby/ruby.pp new file mode 100644 index 000000000..782d52619 --- /dev/null +++ b/modules/utilities/windows/languages/ruby/ruby.pp @@ -0,0 +1 @@ +include ruby::install \ No newline at end of file diff --git a/modules/utilities/windows/languages/ruby/secgen_metadata.xml b/modules/utilities/windows/languages/ruby/secgen_metadata.xml new file mode 100644 index 000000000..39c1d4718 --- /dev/null +++ b/modules/utilities/windows/languages/ruby/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Ruby programming language + Jason Keighley + Apache v2 + A Ruby installation + + languages + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/CHANGELOG.md b/modules/utilities/windows/repository_managers/chocolatey/CHANGELOG.md new file mode 100644 index 000000000..ca4f59c21 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/CHANGELOG.md @@ -0,0 +1,172 @@ +## 2016-12-30 Supported Release 2.0.1 + +### Summary + +This is a bug fix release, correcting some issues in the original supported release and one that was introduced by the switchover to the puppetlabs-powershell v2 module. + +### Bug Fixes + +- Fix: ChocolateyInstall environment variable not set for alternate installation directory ([MODULES-4091](https://tickets.puppetlabs.com/browse/MODULES-4091)) +- Fix: Unsuitable providers should not cause errors ([MODULES-4149](https://tickets.puppetlabs.com/browse/MODULES-4149)) +- Fix: version is malformed with any extraneous messages ([MODULES-4135](https://tickets.puppetlabs.com/browse/MODULES-4135)) +- Fix: module does not propagate null source error correctly ([MODULES-4056](https://tickets.puppetlabs.com/browse/MODULES-4056)) +- Fix: install fails on Windows 10 when using built-in compression ([MODULES-4210](https://tickets.puppetlabs.com/browse/MODULES-4210)) + +### Improvements + +- Set TLS 1.1+ when available +- Document considerations for install to "C:\Chocolatey" ([MODULES-4090](https://tickets.puppetlabs.com/browse/MODULES-4090)) + + +## 2016-09-29 First Supported Release 2.0.0 + +### Summary + +Puppetlabs-Chocolatey is now a supported module! This includes everything from the approved chocolatey-chocolatey module, plus the improvements in the unsupported releases 0.7.0 and 0.8.0. It also adds the following additional changes and fixes. + +### Features + +- `chocolateysource` - explicitly require location in ensure ([MODULES-3430](https://tickets.puppet.com/browse/MODULES-3430)) +- set ignore package exit codes when Chocolatey is on version 0.9.10+ ([MODULES-3880](https://tickets.puppet.com/browse/MODULES-3880)) + +### Bug Fixes + +- Fix: Ensure config file exists before `chocolateyfeature`, `chocolateyconfig`, or `chocolateysource` ([MODULES-3677](https://tickets.puppet.com/browse/MODULES-3677)) +- Fix: `chocolateysource` - ensure flush when disabling source ([MODULES-3430](https://tickets.puppet.com/browse/MODULES-3430)) +- Fix: `chocolateysource` - erroneous user sync messages ([MODULES-3758](https://tickets.puppet.com/browse/MODULES-3758)) + + +## 2016-07-13 Unsupported Release 0.8.0 + +This brings the unsupported puppetlabs-chocolatey provider on par with the approved chocolatey-chocolatey at 1.2.6 and adds additional features. + + * Includes community module releases up to 1.2.6 (changelog below). + * Manage features - `chocolateyfeature` - see [MODULES-3034](https://tickets.puppet.com/browse/MODULES-3034) + * Manage config settings - `chocolateyconfig` - see [MODULES-3035](https://tickets.puppet.com/browse/MODULES-3035) + + +## 2016-06-01 Unsupported Release 0.7.0 + + * Manage sources - `chocolateysource` - see [MODULES-3037](https://tickets.puppetlabs.com/browse/MODULES-3037) + * Includes community module releases up to 1.2.1 (changelog below up to 1.2.1), plus these additional fixes: + * $::chocolateyversion fact is optional - see [#110](https://github.com/chocolatey/puppet-chocolatey/issues/110) + * Fix: puppet apply works again - see [#105](https://github.com/chocolatey/puppet-chocolatey/issues/105) + + +# Approved Community Module Changelog - Chocolatey Team + +The Chocolatey team has graciously agreed to allow Puppet to take this module +to the next level. Puppet will rerelease a supported module under the original +versioning scheme. For now we are using a number less than 1.0 to show that this +could have some technical issues and should be treated as a prerelease version. + +## 2016-07-11 Release 1.2.6 + +- Fix - AutoUninstaller runs every time in 0.9.9.x [#134](https://github.com/chocolatey/puppet-chocolatey/issues/134) + + +## 2016-06-20 Release 1.2.5 + +- Support feature list changes in v0.9.10+ [#133](https://github.com/chocolatey/puppet-chocolatey/issues/133) +- Fix - Chocolatey fails to install in PowerShell v2 with PowerShell Module 1.x [#128](https://github.com/chocolatey/puppet-chocolatey/issues/128) + + +## 2016-06-04 Release 1.2.4 + +- Compatibility with puppetlabs-powershell 2.x [#125](https://github.com/chocolatey/puppet-chocolatey/issues/125). + + +## 2016-05-06 Release 1.2.3 + +- Do not call choco with --debug --verbose by default [#100](https://github.com/chocolatey/puppet-chocolatey/issues/100). +- Announce [Chocolatey for Business](https://chocolatey.org/compare) in ReadMe. + + +## 2016-05-06 Release 1.2.3 + +- Do not call choco with --debug --verbose by default [#100](https://github.com/chocolatey/puppet-chocolatey/issues/100). +- Announce Chocolatey for Business in ReadMe. + + +## 2016-04-06 Release 1.2.2 + +- Fix: puppet apply works again [#105](https://github.com/chocolatey/puppet-chocolatey/issues/105). +- `$::chocolateyversion` fact is optional - see [#110](https://github.com/chocolatey/puppet-chocolatey/issues/110) +- Fix: Implement PowerShell Redirection Fix for Windows 2008 / PowerShell v2 - see [#119](https://github.com/chocolatey/puppet-chocolatey/issues/119) + + +## 2015-12-08 Release 1.2.1 + +- Small release for support of newer PE versions. + + +##2015-11-03 Release 1.2.0 + +- Implement holdable ([#95](https://github.com/chocolatey/puppet-chocolatey/issues/95)) +- Fix - Use install unless version specified in install ([#71](https://github.com/chocolatey/puppet-chocolatey/issues/71)) + + +## 2015-10-02 Release 1.1.2 + +- Ensure 0.9.9.9 compatibility ([#94](https://github.com/chocolatey/puppet-chocolatey/issues/94)) +- Fix - Mixed stale environment variables of existing choco install causing issues ([#86](https://github.com/chocolatey/puppet-chocolatey/issues/86)) +- Upgrade From POSH Version of Chocolatey Fails from Puppet ([#60](https://github.com/chocolatey/puppet-chocolatey/issues/60)) + + +## 2015-09-25 Release 1.1.1 + +- Add log_output for chocolatey bootstrap installer script +- Ensure bootstrap enforces chocolatey.nupkg in libs folder +- Allow file location for installing nupkg file. + + +## 2015-09-09 Release 1.1.0 + +- Install Chocolatey itself / ensure Chocolatey is installed (PUP-1691) +- Adds custom facts for chocolateyversion and choco_install_path + + +## 2015-07-23 Release 1.0.2 + +- Fixes [#71](https://github.com/chocolatey/puppet-chocolatey/issues/71) - Allow `ensure => $version` to work with already installed packages + + +## 2015-07-01 Release 1.0.1 + +- Fixes [#66](https://github.com/chocolatey/puppet-chocolatey/issues/66) - Check for choco existence more comprehensively + + +## 2015-06-08 Release 1.0.0 + +- No change, bumping to 1.0.0 + + +## 2015-05-22 Release 0.5.3 + +- Fix manifest issue +- Fix choco path issue +- Update ReadMe - fix/clarify how options with quotes need to be passed. + + +## 2015-04-23 Release 0.5.2 + +- Update ReadMe +- Add support for Windows 10. +- Fixes [#56](https://github.com/chocolatey/puppet-chocolatey/pull/56) - Avoiding puppet returning 2 instead of 0 when there are no changes to be done. + + +## 2015-03-31 Release 0.5.1 + +- Fixes [#54](https://github.com/chocolatey/puppet-chocolatey/issues/54) - Blocking: Linux masters throw error if module is present + + +## 2015-03-30 Release 0.5.0 + +- Provider enhancements +- Better docs +- Works with both compiled and powershell Chocolatey clients +- Fixes #50 - work with newer compiled Chocolatey client (0.9.9+) +- Fixes #43 - check for installed packages is case sensitive +- Fixes #18 - The OS handle's position is not what FileStream expected. +- Fixes #52 - Document best way to pass options with spaces (#15 also related) +- Fixes #26 - Document Chocolatey needs to be installed by other means diff --git a/modules/utilities/windows/repository_managers/chocolatey/CONTRIBUTING.md b/modules/utilities/windows/repository_managers/chocolatey/CONTRIBUTING.md new file mode 100644 index 000000000..dd2b5c4ff --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/CONTRIBUTING.md @@ -0,0 +1,219 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](https://tickets.puppet.com/secure/CreateIssue!default.jspa), or [watch the ticket](https://tickets.puppet.com/browse/) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 3. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% rake spec_clean +% rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://puppet.com/community/get-help) + +* [Writing tests](https://docs.puppet.com/guides/module_guides/bgtm.html#step-three-module-testing) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) + + diff --git a/modules/utilities/windows/repository_managers/chocolatey/Gemfile b/modules/utilities/windows/repository_managers/chocolatey/Gemfile new file mode 100644 index 000000000..0c629e701 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/Gemfile @@ -0,0 +1,152 @@ +#This file is generated by ModuleSync, do not edit. + +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +# Determines what type of gem is requested based on place_or_version. +def gem_type(place_or_version) + if place_or_version =~ /^git:/ + :git + elsif place_or_version =~ /^file:/ + :file + else + :gem + end +end + +# Find a location or specific version for a gem. place_or_version can be a +# version, which is most often used. It can also be git, which is specified as +# `git://somewhere.git#branch`. You can also use a file source location, which +# is specified as `file://some/location/on/disk`. +def location_for(place_or_version, fake_version = nil) + if place_or_version =~ /^(git[:@][^#]*)#(.*)/ + [fake_version, { :git => $1, :branch => $2, :require => false }].compact + elsif place_or_version =~ /^file:\/\/(.*)/ + ['>= 0', { :path => File.expand_path($1), :require => false }] + else + [place_or_version, { :require => false }] + end +end + +# Used for gem conditionals +supports_windows = true + +# The following gems are not included by default as they require DevKit on Windows. +# You should probably include them in a Gemfile.local or a ~/.gemfile +#gem 'pry' #this may already be included in the gemfile +#gem 'pry-stack_explorer', :require => false +#if RUBY_VERSION =~ /^2/ +# gem 'pry-byebug' +#else +# gem 'pry-debugger' +#end + +group :development do + gem 'rake', :require => false + gem 'rspec', '~>3.0', :require => false + gem 'puppet-lint', :require => false + gem 'puppetlabs_spec_helper', '~>0.10.3', :require => false + gem 'puppet_facts', :require => false + gem 'mocha', '~>0.10.5', :require => false + gem 'pry', :require => false + gem 'json_pure', '<= 2.0.1', :require => false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') +end + +group :system_tests do + gem 'beaker', *location_for(ENV['BEAKER_VERSION'] || '~> 2.20') + gem 'master_manipulator', '~> 1.2', :require => false + gem 'beaker-windows', '~> 0.6', :require => false + gem 'beaker-hostgenerator', '~> 0.6', :require => false +end + +gem 'puppet', *location_for(ENV['PUPPET_GEM_VERSION']) + +# Only explicitly specify Facter/Hiera if a version has been specified. +# Otherwise it can lead to strange bundler behavior. If you are seeing weird +# gem resolution behavior, try setting `DEBUG_RESOLVER` environment variable +# to `1` and then run bundle install. +gem 'facter', *location_for(ENV['FACTER_GEM_VERSION']) if ENV['FACTER_GEM_VERSION'] +gem 'hiera', *location_for(ENV['HIERA_GEM_VERSION']) if ENV['HIERA_GEM_VERSION'] + +# For Windows dependencies, these could be required based on the version of +# Puppet you are requiring. Anything greater than v3.5.0 is going to have +# Windows-specific dependencies dictated by the gem itself. The other scenario +# is when you are faking out Puppet to use a local file path / git path. +explicitly_require_windows_gems = false +puppet_gem_location = gem_type(ENV['PUPPET_GEM_VERSION']) +# This is not a perfect answer to the version check +if puppet_gem_location != :gem || (ENV['PUPPET_GEM_VERSION'] && Gem::Version.correct?(ENV['PUPPET_GEM_VERSION']) && Gem::Requirement.new('< 3.5.0').satisfied_by?(Gem::Version.new(ENV['PUPPET_GEM_VERSION'].dup))) + if Gem::Platform.local.os == 'mingw32' + explicitly_require_windows_gems = true + end + if puppet_gem_location == :gem + # If facterversion hasn't been specified and we are + # looking for a Puppet Gem version less than 3.5.0, we + # need to ensure we get a good Facter for specs. + gem "facter",">= 1.6.11","<= 1.7.5",:require => false unless ENV['FACTER_GEM_VERSION'] + # If hieraversion hasn't been specified and we are + # looking for a Puppet Gem version less than 3.5.0, we + # need to ensure we get a good Hiera for specs. + gem "hiera",">= 1.0.0","<= 1.3.0",:require => false unless ENV['HIERA_GEM_VERSION'] + end +end + +if explicitly_require_windows_gems + # This also means Puppet Gem less than 3.5.0 - this has been tested back + # to 3.0.0. Any further back is likely not supported. + if puppet_gem_location == :gem + gem "ffi", "1.9.0", :require => false + gem "win32-eventlog", "0.5.3","<= 0.6.5", :require => false + gem "win32-process", "0.6.5","<= 0.7.5", :require => false + gem "win32-security", "~> 0.1.2","<= 0.2.5", :require => false + gem "win32-service", "0.7.2","<= 0.8.8", :require => false + gem "minitar", "0.5.4", :require => false + else + gem "ffi", "~> 1.9.0", :require => false + gem "win32-eventlog", "~> 0.5","<= 0.6.5", :require => false + gem "win32-process", "~> 0.6","<= 0.7.5", :require => false + gem "win32-security", "~> 0.1","<= 0.2.5", :require => false + gem "win32-service", "~> 0.7","<= 0.8.8", :require => false + gem "minitar", "~> 0.5.4", :require => false + end + + gem "win32-dir", "~> 0.3","<= 0.4.9", :require => false + gem "win32console", "1.3.2", :require => false if RUBY_VERSION =~ /^1\./ + + # sys-admin was removed in Puppet 3.7.0+, and doesn't compile + # under Ruby 2.3 - so restrict it to Ruby 1.x + gem "sys-admin", "1.5.6", :require => false if RUBY_VERSION =~ /^1\./ + + # Puppet less than 3.7.0 requires these. + # Puppet 3.5.0+ will control the actual requirements. + # These are listed in formats that work with all versions of + # Puppet from 3.0.0 to 3.6.x. After that, these were no longer used. + # We do not want to allow newer versions than what came out after + # 3.6.x to be used as they constitute some risk in breaking older + # functionality. So we set these to exact versions. + gem "win32-api", "1.4.8", :require => false + gem "win32-taskscheduler", "0.2.2", :require => false + gem "windows-api", "0.4.3", :require => false + gem "windows-pr", "1.2.3", :require => false +else + if Gem::Platform.local.os == 'mingw32' + # If we're using a Puppet gem on windows, which handles its own win32-xxx gem dependencies (Pup 3.5.0 and above), set maximum versions + # Required due to PUP-6445 + gem "win32-dir", "<= 0.4.9", :require => false + gem "win32-eventlog", "<= 0.6.5", :require => false + gem "win32-process", "<= 0.7.5", :require => false + gem "win32-security", "<= 0.2.5", :require => false + gem "win32-service", "<= 0.8.8", :require => false + end +end + +# Evaluate Gemfile.local if it exists +if File.exists? "#{__FILE__}.local" + eval(File.read("#{__FILE__}.local"), binding) +end + +# Evaluate ~/.gemfile if it exists +if File.exists?(File.join(Dir.home, '.gemfile')) + eval(File.read(File.join(Dir.home, '.gemfile')), binding) +end + +# vim:ft=ruby diff --git a/modules/utilities/windows/repository_managers/chocolatey/LICENSE b/modules/utilities/windows/repository_managers/chocolatey/LICENSE new file mode 100644 index 000000000..7a4a3ea24 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/MAINTAINERS.md b/modules/utilities/windows/repository_managers/chocolatey/MAINTAINERS.md new file mode 100644 index 000000000..980e355ee --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/MAINTAINERS.md @@ -0,0 +1,6 @@ +## Maintenance + +Maintainers: + - Puppet Windows Team `windows |at| puppet |dot| com` + +Tickets: https://tickets.puppet.com/browse/MODULES. Make sure to set component to `chocolatey`. \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/NOTICE b/modules/utilities/windows/repository_managers/chocolatey/NOTICE new file mode 100644 index 000000000..e69de29bb diff --git a/modules/utilities/windows/repository_managers/chocolatey/README.md b/modules/utilities/windows/repository_managers/chocolatey/README.md new file mode 100644 index 000000000..73bdbbf6d --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/README.md @@ -0,0 +1,783 @@ +# Chocolatey Package Provider for Puppet + +### Chocolatey for Business Now Available! + +We're excited for you to learn more about what's available in the [Business editions](https://chocolatey.org/compare)! + +## Build Status + +Travis | AppVeyor +------------- | ------------- +[![Build Status](https://api.travis-ci.org/puppetlabs/puppetlabs-chocolatey.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-chocolatey) | [![Build status](https://ci.appveyor.com/api/projects/status/uosorvcyhnayv70m/branch/master?svg=true)](https://ci.appveyor.com/project/puppetlabs/puppetlabs-chocolatey/branch/master) + +#### Table of Contents + +1. [Overview](#overview) +2. [Module Description - What the chocolatey module does and why it is useful](#module-description) + * [Why Chocolatey](#why-chocolatey) +3. [Setup - The basics of getting started with chocolatey](#setup) + * [What chocolatey affects](#what-chocolatey-affects) + * [Setup requirements](#setup-requirements) + * [Beginning with Chocolatey provider](#beginning-with-chocolatey-provider) +4. [Usage - Configuration options and additional functionality](#usage) +5. [Reference](#reference) + * [Classes](#public-classes) + * [Facts](#facts) + * [Types/Providers](#typesproviders) + * [Package provider: Chocolatey](#package-provider-chocolatey) + * [Chocolatey source configuration](#chocolateysource) + * [Chocolatey feature configuration](#chocolateyfeature) + * [Chocolatey config configuration](#chocolateyconfig) + * [Class: chocolatey](#class-chocolatey) +6. [Limitations - OS compatibility, etc.](#limitations) + * [Known Issues](#known-issues) +7. [Development - Guide for contributing to the module](#development) +8. [Attributions](#attributions) + +## Overview + +This is a [Puppet](http://docs.puppet.com/) package provider for +[Chocolatey](https://github.com/chocolatey/chocolatey), which is +like apt-get, but for Windows. Check the module's metadata.json for +compatible Puppet and Puppet Enterprise versions. + +## Module Description + +This is the official module for working with the [Chocolatey](https://chocolatey.org/about) +package manager. + +This module supports all editions of Chocolatey, including FOSS, [Professional](https://chocolatey.org/compare) and [Chocolatey for Business](https://chocolatey.org/compare). + +This module is able to: + +* Install Chocolatey +* Work with custom location installations +* Configure Chocolatey +* Use Chocolatey as a package provider + +### Why Chocolatey + +Chocolatey closely mimics how package managers on other operating systems work. If you can imagine the built-in provider for +Windows versus Chocolatey, take a look at the use case of installing git: + +~~~puppet +# Using built-in provider +package { "Git version 1.8.4-preview20130916": + ensure => installed, + source => 'C:\temp\Git-1.8.4-preview20130916.exe', + install_options => ['/VERYSILENT'] +} +~~~ + +~~~puppet +# Using Chocolatey (set as default for Windows) +package { 'git': + ensure => latest, +} +~~~ + +With the built-in provider: + * The [package name must match ***exactly***](https://docs.puppet.com/puppet/latest/reference/resources_package_windows.html#package-name-must-be-the-displayname) the name from installed programs. + * The package name has issues with unicode characters. + * The [source must point to the location](https://docs.puppet.com/puppet/latest/reference/resources_package_windows.html#the-source-attribute-is-required) of the executable installer. + * It cannot `ensure => latest`. Read about [handling versions and upgrades](https://docs.puppet.com/puppet/latest/reference/resources_package_windows.html#handling-versions-and-upgrades) in the Puppet documentation. + +With Chocolatey's provider: + * The package name only has to match the name of the package, which can be whatever you choose. + * The package knows how to install the software silently. + * The package knows where to get the executable installer. + * The source is able to specify different Chocolatey feeds. + * Chocolatey makes `package` more platform agnostic, because it looks exactly like other platforms. + +For reference, read about the [provider features available](https://docs.puppet.com/references/latest/type.html#package-provider-features) from the built-in provider, compared to other package managers: + +| Provider | holdable | install options | installable | package settings | purgeable | reinstallable | uninstall options | uninstallable | upgradeable | versionable | virtual packages | +|------------|----------|-----------------|-------------|------------------|-----------|---------------|-------------------|---------------|-------------|-------------|------------------| +| Windows | | x | x | | | | x | x | | x | | +| Chocolatey | x | x | x | | | | x | x | x | x | | +| apt | x | x | x | | x | | | x | x | x | | +| yum | | x | x | | x | | | x | x | x | x | + +## Setup + +### What Chocolatey affects + +Chocolatey affects your system and what software is installed on it, ranging +from tools and portable software, to natively installed applications. + +### Setup Requirements + +Chocolatey requires the following components: + + * Powershell v2+ (Installed on most systems by default) + * .NET Framework v4+ + +### Beginning with Chocolatey provider + +Install this module via any of these approaches: + +* [Puppet Forge](http://forge.puppet.com/chocolatey/chocolatey) +* git-submodule ([tutorial](http://goo.gl/e9aXh)) +* [librarian-puppet](https://github.com/rodjek/librarian-puppet) +* [r10k](https://github.com/puppetlabs/r10k) + +## Usage + +### Manage Chocolatey installation + +Ensure Chocolatey is installed and configured: + +~~~puppet +include chocolatey +~~~ + +#### Override default Chocolatey install location + +~~~puppet +class {'chocolatey': + choco_install_location => 'D:\secured\choco', +} +~~~ + +**NOTE:** This will affect suitability on first install. There are also +special considerations for `C:\Chocolatey` as an install location, see +[`choco_install_location`](#choco_install_location) for details. + +#### Use an internal chocolatey.nupkg for Chocolatey installation + +~~~puppet +class {'chocolatey': + chocolatey_download_url => 'https://internalurl/to/chocolatey.nupkg', + use_7zip => false, + choco_install_timeout_seconds => 2700, +} +~~~ + +#### Use a file chocolatey.0.9.9.9.nupkg for installation + +~~~puppet +class {'chocolatey': + chocolatey_download_url => 'file:///c:/location/of/chocolatey.0.9.9.9.nupkg', + use_7zip => false, + choco_install_timeout_seconds => 2700, +} +~~~ + +#### Specify the version of chocolatey by class parameters + +~~~puppet +class {'chocolatey': + chocolatey_download_url => 'file:///c:/location/of/chocolatey.0.9.9.9.nupkg', + use_7zip => false, + choco_install_timeout_seconds => 2700, + chocolatey_version => '0.9.9.9', +} +~~~ + + +#### Log chocolatey bootstrap installer script output + +~~~puppet +class {'chocolatey': + log_output => true, +} +~~~ + + +### Configuration + +If you have Chocolatey 0.9.9.x or above, you can take advantage of configuring different aspects of Chocolatey. + +#### Sources Configuration + +You can specify sources that Chocolatey uses by default, along with priority. + +Requires Chocolatey v0.9.9.0+. + +##### Disable the default community repository source + +~~~puppet +chocolateysource {'chocolatey': + ensure => disabled, +} +~~~ + +##### Set a priority on a source + +~~~puppet +chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + priority => 1, +} +~~~ + +##### Add credentials to a source + +~~~puppet +chocolateysource {'sourcename': + ensure => present, + location => 'https://internal/source', + user => 'username', + password => 'password', +} +~~~ + +**NOTE:** Chocolatey encrypts the password in a way that is not +verifiable. If you need to rotate passwords, you cannot use this +resource to do so unless you also change the location, user, or priority +(because those are ensurable properties). + +#### Features Configuration + +You can configure features that Chocolatey has available. Run +`choco feature list` to see the available configuration features. + +Requires Chocolatey v0.9.9.0+. + +##### Enable Auto Uninstaller + +Uninstall from Programs and Features without requiring an explicit +uninstall script. + +~~~puppet +chocolateyfeature {'autouninstaller': + ensure => enabled, +} +~~~ + +##### Disable Use Package Exit Codes + +Requires 0.9.10+ for this feature. + +**Use Package Exit Codes** - Allows package scripts to provide exit codes. With +this enabled, Chocolatey uses package exit codes for exit when +non-zero (this value can come from a dependency package). Chocolatey +defines valid exit codes as 0, 1605, 1614, 1641, 3010. With this feature +disabled, Chocolatey exits with a 0 or a 1 (matching previous behavior). + +~~~puppet +chocolateyfeature {'usepackageexitcodes': + ensure => disabled, +} +~~~ + +##### Enable Virus Check + +Requires 0.9.10+ and [Chocolatey Pro / Business](https://chocolatey.org/compare) +for this feature. + +**Virus Check** - Performs virus checking on downloaded files. *(Licensed versions only.)* + +~~~puppet +chocolateyfeature {'viruscheck': + ensure => enabled, +} +~~~ + +##### Enable FIPS Compliant Checksums + +Requires 0.9.10+ for this feature. + +**Use FIPS Compliant Checksums** - Ensures checksumming done by Chocolatey uses +FIPS compliant algorithms. *Not recommended unless required by FIPS Mode.* +Enabling on an existing installation could have unintended consequences +related to upgrades or uninstalls. + +~~~puppet +chocolateyfeature {'usefipscompliantchecksums': + ensure => enabled, +} +~~~ + +#### Config configuration + +You can configure config values that Chocolatey has available. Run +`choco config list` to see the config settings available (just the +config settings section). + +Requires Chocolatey v0.9.10.0+. + +##### Set cache location + +The cache location defaults to the TEMP directory. You can set an explicit directory +to cache downloads to instead of the default. + +~~~puppet +chocolateyconfig {'cachelocation': + value => "c:\\downloads", +} +~~~ + +##### Unset cache location + +Removes cache location setting, returning the setting to its default. + +~~~puppet +chocolateyconfig {'cachelocation': + ensure => absent, +} +~~~ + +##### Use an explicit proxy + +When using Chocolatey behind a proxy, set `proxy` and optionally +`proxyUser` and `proxyPassword`. + +**NOTE:** The `proxyPassword` value is not verifiable. + +~~~puppet +chocolateyconfig {'proxy': + value => 'https://someproxy.com', +} + +chocolateyconfig {'proxyUser': + value => 'bob', +} + +# not verifiable +chocolateyconfig {'proxyPassword': + value => 'securepassword', +} +~~~ + +#### Set Chocolatey as Default Windows Provider + +If you want to set this provider as the site-wide default, +add to your `site.pp`: + +~~~puppet +if $::kernel == 'windows' { + Package { provider => chocolatey, } +} + +# OR + +case $operatingsystem { + 'windows': { + Package { provider => chocolatey, } + } +} +~~~ + +### Packages + +#### With all options + +~~~puppet +package { 'notepadplusplus': + ensure => installed|latest|'1.0.0'|absent, + provider => 'chocolatey', + install_options => ['-pre','-params','"','param1','param2','"'], + uninstall_options => ['-r'], + source => 'https://myfeed.example.com/api/v2', +} +~~~ + +* Supports `installable` and `uninstallable`. +* Supports `versionable` so that `ensure => '1.0'` works. +* Supports `upgradeable`. +* Supports `latest` (checks upstream), `absent` (uninstall). +* Supports `install_options` for pre-release, and other command-line options. +* Supports `uninstall_options` for pre-release, and other command-line options. +* Supports `holdable`, requires Chocolatey v0.9.9.0+. + +#### Simple install + +~~~puppet +package { 'notepadplusplus': + ensure => installed, + provider => 'chocolatey', +} +~~~ + +#### To always ensure using the newest version available + +~~~puppet +package { 'notepadplusplus': + ensure => latest, + provider => 'chocolatey', +} +~~~ + +#### To ensure a specific version + +~~~puppet +package { 'notepadplusplus': + ensure => '6.7.5', + provider => 'chocolatey', +} +~~~ + +#### To specify custom source + +~~~puppet +package { 'notepadplusplus': + ensure => '6.7.5', + provider => 'chocolatey', + source => 'C:\local\folder\packages', +} +~~~ + +~~~puppet +package { 'notepadplusplus': + ensure => '6.7.5', + provider => 'chocolatey', + source => '\\unc\source\packages', +} +~~~ + +~~~puppet +package { 'notepadplusplus': + ensure => '6.7.5', + provider => 'chocolatey', + source => 'https://custom.nuget.odata.feed/api/v2/', +} +~~~ + +~~~puppet +package { 'notepadplusplus': + ensure => '6.7.5', + provider => 'chocolatey', + source => 'C:\local\folder\packages;https://chocolatey.org/api/v2/', +} +~~~ + +#### Install options with spaces + +Spaces in arguments **must always** be covered with a separation. Shown +below is an example of how you configure `-installArgs "/VERYSILENT /NORESTART"`. + +~~~puppet +package {'launchy': + ensure => installed, + provider => 'chocolatey', + install_options => ['-override', '-installArgs', '"', '/VERYSILENT', '/NORESTART', '"'], +} +~~~ + +#### Install options with quotes or spaces + +The underlying installer may need quotes passed to it. This is possible, but not +as intuitive. The example below covers passing `/INSTALLDIR="C:\Program Files\somewhere"`. + +For this to be passed through with Chocolatey, you need a set of double +quotes surrounding the argument and two sets of double quotes surrounding the +item that must be quoted (see [how to pass/options/switches](https://github.com/chocolatey/choco/wiki/CommandsReference#how-to-pass-options--switches)). This makes the +string look like `-installArgs "/INSTALLDIR=""C:\Program Files\somewhere"""` for +proper use with Chocolatey. + +Then, for Puppet to handle that appropriately, you must split on ***every*** space. +Yes, on **every** space you must split the string or the result comes out +incorrectly. This means it will look like the following: + +~~~puppet +install_options => ['-installArgs', + '"/INSTALLDIR=""C:\Program', 'Files\somewhere"""'] +~~~ + +Make sure you have all of the right quotes - start it off with a single double +quote, then two double quotes, then close it all by closing the two double +quotes and then the single double quote or a possible three double quotes at +the end. + +~~~puppet +package {'mysql': + ensure => latest, + provider => 'chocolatey', + install_options => ['-override', '-installArgs', + '"/INSTALLDIR=""C:\Program', 'Files\somewhere"""'], +} +~~~ + +You can split it up a bit for readability if it suits you: + +~~~puppet +package {'mysql': + ensure => latest, + provider => 'chocolatey', + install_options => ['-override', '-installArgs', '"' + '/INSTALLDIR=""C:\Program', 'Files\somewhere""', + '"'], +} +~~~ + +**Note:** The above is for Chocolatey v0.9.9+. You may need to look for an +alternative method to pass args if you have 0.9.8.x and below. + +## Reference + +### Classes + +#### Public classes + +* [`chocolatey`](#class-chocolatey) + +#### Private classes + +* `chocolatey::install.pp`: Ensures Chocolatey is installed. +* `chocolatey::config.pp`: Ensures Chocolatey is configured. + +### Facts + +* `chocolateyversion` - The version of the installed Chocolatey client (could also be provided by class parameter `chocolatey_version`). +* `choco_install_path` - The location of the installed Chocolatey client (could also be provided by class parameter `choco_install_location`). + +### Types/Providers + +* [Chocolatey provider](#package-provider-chocolatey) +* [Chocolatey source configuration](#chocolateysource) +* [Chocolatey feature configuration](#chocolateyfeature) + + +### Package provider: Chocolatey + +Chocolatey implements a [package type](http://docs.puppet.com/references/latest/type.html#package) with a resource provider, which is built into Puppet. + +This provider supports the `install_options` and `uninstall_options` attributes, +which allow command-line options to be passed to the `choco` command. These options +should be specified as documented below. + + * Required binaries: `choco.exe`, usually found in `C:\Program Data\chocolatey\bin\choco.exe`. + * The binary is searched for using the environment variable `ChocolateyInstall`, then by two known locations (`C:\Chocolatey\bin\choco.exe` and `C:\ProgramData\chocolatey\bin\choco.exe`). + * Supported features: `install_options`, `installable`, `uninstall_options`, +`uninstallable`, `upgradeable`, `versionable`. + +**NOTE**: the root of `C:\` is not a secure location by default, so you may want to update the security on the folder. + +#### Properties/Parameters + +##### `ensure` + +(**Property**: This attribute represents a concrete state on the target system.) + +Specifies what state the package should be in. You can choose which package to retrieve by +specifying a version number or `latest` as the ensure value. Valid options: `present` (also called `installed`), `absent`, `latest`, +`held` or a version number. Default: `installed`. + +##### `install_options` + +Specifies an array of additional options to pass when installing a package. These options are +package-specific, and should be documented by the software vendor. One commonly +implemented option is `INSTALLDIR`: + +~~~puppet +package {'launchy': + ensure => installed, + provider => 'chocolatey', + install_options => ['-installArgs', '"', '/VERYSILENT', '/NORESTART', '"'], +} +~~~ + +**NOTE:** The above method of single quotes in an array is the only method you should use +in passing `install_options` with the Chocolatey provider. There are other ways +to do it, but they are passed through to Chocolatey in ways that may not be +sufficient. + +This is the **only** place in Puppet where backslash separators should be used. +Note that backslashes in double-quoted strings *must* be double-escaped and +backslashes in single-quoted strings *may* be double-escaped. + +##### `name` + +Specifies the package name. This is the name that the packaging system uses internally. Valid options: String. Default: The resource's title. + +##### `provider` + +**Required.** Sets the specific backend to use for the `package` resource. Chocolatey is not the +default provider for Windows, so it must be specified (or by using a resource +default, shown in the Usage section above). Valid options: `'chocolatey'`. + +##### `source` + +Specifies where to find the package file. Use this to override the default +source(s). Valid options: String of either an absolute path to a local +directory containing packages stored on the target system, a URL (to OData feeds), or a network +drive path. Chocolatey maintains default sources in its configuration file that it uses by default. + +Puppet will not automatically retrieve source files for you, and +usually just passes the value of the source to the package installation command. +You can use a `file` resource if you need to manually copy package files to the +target system. + +##### `uninstall_options` + +Specifies an array of additional options to pass when uninstalling a package. These options +are package-specific, and should be documented by the software vendor. + +~~~puppet +package {'launchy': + ensure => absent, + provider => 'chocolatey', + uninstall_options => ['-uninstallargs', '"', '/VERYSILENT', '/NORESTART', '"'], +} +~~~ + +The above method of single quotes in an array is the only method you should use +in passing `uninstall_options` with the Chocolatey provider. There are other ways +to do it, but they are passed to Chocolatey in ways that may not be +sufficient. + +**NOTE:** This is the **only** place in Puppet where backslash separators should be used. +Backslashes in double-quoted strings *must* be double-escaped and +backslashes in single-quoted strings *may* be double-escaped. + + +### ChocolateySource + +Allows managing default sources for Chocolatey. A source can be a folder, a CIFS share, +a NuGet Http OData feed, or a full Package Gallery. Learn more about sources at +[How To Host Feed](https://chocolatey.org/docs/how-to-host-feed). Requires +Chocolatey v0.9.9.0+. + +#### Properties/Parameters + +##### `name` + +Specifies the name of the source. Used for uniqueness. Also sets the `location` to this value if `location` is unset. Valid options: String. Default: The resource's title. + +##### `ensure` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies what state the source should be in. Default: `present`. Valid options: `present`, `disabled`, or `absent`. `disabled` should only be used with existing sources. + +##### `location` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies the location of the source repository. Valid options: String of a URL pointing to an OData feed (such as chocolatey/chocolatey_server), a CIFS (UNC) share, or a local folder. Required when `ensure => present` (`present` is default value for `ensure`). + +##### `user` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies an optional user name for authenticated feeds. Requires at least Chocolatey v0.9.9.0. Default: `nil`. Specifying an empty value is the same as setting the value to `nil` or not specifying the property at all. + +##### `password` + +Specifies an optional user password for authenticated feeds. Not ensurable. Value cannot be checked with current value. If you need to update the password, update another setting as well to force the update. Requires at least Chocolatey v0.9.9.0. Default: `nil`. Specifying an empty value is the same as setting the value to `nil` or not specifying the property at all. + +##### `priority` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies an optional priority for explicit feed order when searching for packages across multiple feeds. The lower the number, the higher the priority. Sources with a 0 priority are considered no priority and are added after other sources with a priority number. Requires at least Chocolatey v0.9.9.9. Default: `0`. + +### ChocolateyFeature + +Allows managing features for Chocolatey. Features are configurations that +act as switches to turn on or off certain aspects of how +Chocolatey works. Learn more about features in the +[Chocolatey documentation](https://chocolatey.org/docs/commands-feature). Requires +Chocolatey v0.9.9.0+. + +#### Properties/Parameters + +##### `name` + +Specifies the name of the feature. Used for uniqueness. Valid options: String. Default: The resource's title. + +##### `ensure` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies what state the feature should be in. Valid options: `enabled` or `disabled`. Default: `disabled`. + + +### ChocolateyConfig + +Allows managing config settings for Chocolatey. Configuration values +provide settings for users to configure aspects of Chocolatey and the +way it functions. Similar to features, except allow for user configured +values. Learn more about config settings at +[Config](https://chocolatey.org/docs/commands-config). Requires +Chocolatey v0.9.9.9+. + +#### Properties/Parameters + +##### `name` + +(**Namevar**: If ommitted, this parameter's value will default to the resource's +title.) + +Specifies the name of the config setting. Used for uniqueness. Puppet is not able to +easily manage any values that include "password" in the key name because they +will be encrypted in the configuration file. + +##### `ensure` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies what state the config should be in. Valid options: `present` or `absent`. Default: `present`. + +##### `value` + +(**Property**: This parameter represents a concrete state on the target system.) + +Specifies the value of the config setting. If the name includes "password", then the value +is not ensurable due to being encrypted in the configuration file. + + +### Class: chocolatey + +Manages installation and configuration of Chocolatey itself. + +#### Parameters + +##### `choco_install_location` + +Specifies where Chocolatey install should be located. Valid options: Must be an absolute path starting with a drive letter, for example: `c:\`. Default: The currently detected install location based on the `ChocolateyInstall` environment variable. If not specified, falls back to `'C:\ProgramData\chocolatey'`. + +**NOTE:** Puppet can install Chocolatey and configure Chocolatey install packages during the same run *UNLESS* you specify this setting. This is due to the way the providers search for suitability of the command, falling back to the default install for the executable when none is found. Because environment variables and commands do not refresh during the same Puppet run (due somewhat to a Windows limitation with updating environment information for currently running processes), installing to a directory that is not the default won't be detected until the next time Puppet runs. So unless you really want this installed elsewhere and don't have a current existing install in that non-default location, do not set this value. + +Specifying `C:\Chocolatey` as the install directory will trigger Chocolatey to attempt to upgrade that directory. This is due to that location being the original install location for Chocolatey before it was moved to another directory and subsequently locked down. If you need this to be the installation directory, please define an environment variable `ChocolateyAllowInsecureRootDirectory` and set it to `'true'`. For more information, please see the [CHANGELOG for 0.9.9](https://github.com/chocolatey/choco/blob/master/CHANGELOG.md#099-march-3-2015). + +If you override the default installation directory you need to set appropriate permissions on that install location, because Chocolatey does not restrict access to the custom directory to only Administrators. Chocolatey only restricts access to the directory in the default install location, to avoid permissions issues with custom locations, among other reasons. See ["Can I install Chocolatey to another location?"](https://chocolatey.org/install#can-i-install-chocolatey-to-another-location) for more information. + +##### `use_7zip` + +Specifies whether to use the built-in shell or allow the installer to download 7zip to extract `chocolatey.nupkg` during installation. Valid options: `true`, `false`. Default: `false`. + +##### `choco_install_timeout_seconds` + +Specifies how long in seconds should be allowed for the install of Chocolatey (including .NET Framework 4 if necessary). Valid options: Number. Default: `1500` (25 minutes). + +##### `chocolatey_download_url` + +Specifies the URL that returns `chocolatey.nupkg`. Valid options: String of URL, not necessarily from an OData feed. Any URL location will work, but it must result in the chocolatey nupkg file being downloaded. Default: `'https://chocolatey.org/api/v2/package/chocolatey/'`. + +##### `enable_autouninstaller` + +*Only for 0.9.9.x users. Chocolatey 0.9.10.x+ ignores this setting.* Specifies whether auto uninstaller is enabled. Auto uninstaller allows Chocolatey to automatically manage the uninstall of software from Programs and Features without necessarily requiring a `chocolateyUninstall.ps1` file in the package. Valid options: `true`, `false`. Default: `true`. + +##### `log_output` + +Specifies whether to log output from the installer. Valid options: `true`, `false`. Default: `false`. + + +## Limitations + +1. Works with Windows only. +2. If you override an existing install location of Chocolatey using `choco_install_location =>` in the Chocolatey class, it does not bring any of the existing packages with it. You will need to handle that through some other means. +3. Overriding the install location will also not allow Chocolatey to be configured or install packages on the same run that it is installed on. See [`choco_install_location`](#choco_install_location) for details. + +### Known Issues + +1. This module doesn't support side by side scenarios. +2. This module may have issues upgrading Chocolatey itself using the package resource. +3. If .NET 4.0 is not installed, it may have trouble installing Chocolatey. Chocolatey version 0.9.9.9+ helps alleviate this issue. +4. If there is an error in the installer (`InstallChocolatey.ps1.erb`), it may not show as an error. This may be an issue with the PowerShell provider and is still under investigation. + +## Development + +Puppet Inc modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can’t access the huge number of platforms and myriad of hardware, software, and deployment configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. + +For more information, see our [module contribution guide.](https://docs.puppet.com/forge/contributing.html) + +## Attributions + +A special thanks goes out to [Rich Siegel](https://github.com/rismoney) and [Rob Reynolds](https://github.com/ferventcoder) who wrote the original +provider and continue to contribute to the development of this provider. diff --git a/modules/utilities/windows/repository_managers/chocolatey/Rakefile b/modules/utilities/windows/repository_managers/chocolatey/Rakefile new file mode 100644 index 000000000..8e029301c --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/Rakefile @@ -0,0 +1,104 @@ +require 'rake' +require 'rspec/core/rake_task' +require 'puppetlabs_spec_helper/rake_tasks' +require 'puppet' + +begin + require 'beaker/tasks/test' unless RUBY_PLATFORM =~ /win32/ +rescue LoadError + #Do nothing, only installed with system_tests group +end + + +# If puppet does not support symlinks (e.g., puppet <= 3.5) we cannot use +# puppetlabs_spec_helper's `rake spec` task because it requires symlink +# support. Redefine `rake spec` to avoid calling `rake spec_prep` (requires +# symlinks to place fixtures) and restrict the pattern match only files under +# the 'unit' directory (tests in other dirs require fixtures). +if Puppet::Util::Platform.windows? and !Puppet::FileSystem.respond_to?(:symlink) + ENV["SPEC"] = "./spec/{unit,integration}/**/*_spec.rb" + Rake::Task[:spec].clear if Rake::Task.task_defined?(:spec) + task :spec do + Rake::Task[:spec_standalone].invoke + Rake::Task[:spec_clean].invoke + end +end + +# These lint exclusions are in puppetlabs_spec_helper but needs a version above 0.10.3 +# Line length test is 80 chars in puppet-lint 1.1.0 +PuppetLint.configuration.send('disable_80chars') +# Line length test is 140 chars in puppet-lint 2.x +PuppetLint.configuration.send('disable_140chars') + +task :default => [:spec] + +desc 'Generate code coverage' +RSpec::Core::RakeTask.new(:coverage) do |t| + t.rcov = true + t.rcov_opts = ['--exclude', 'spec'] +end + + +platform = ENV["PLATFORM"] + +# Create the directory, if it exists already you'll get an error, but this should not stop the execution +begin + sh 'mkdir tests/configs' +rescue => e + puts e.message +end + +desc 'Executes reference tests (agent only) intended for use in CI' +task :reference_tests do + command = "bundle exec beaker-hostgenerator --global-config {masterless=true} #{platform} > tests/configs/#{platform}" # should we assume the "configs" directory is present? + sh command + + command =<<-EOS +bundle exec beaker \ + --debug \ + --preserve-hosts never \ + --config tests/configs/$PLATFORM \ + --keyfile ~/.ssh/id_rsa-acceptance \ + --load-path tests/lib \ + --type aio \ + --pre-suite tests/reference/pre-suite \ + --tests tests/reference/tests + EOS + sh command +end + +desc 'Executes acceptance tests (master and agent) intended for use in CI' +task :acceptance_tests do + command = "bundle exec beaker-hostgenerator #{platform} > tests/configs/#{platform}" + sh command + + command =<<-EOS +bundle exec beaker \ + --debug \ + --preserve-hosts never \ + --config tests/configs/$PLATFORM \ + --keyfile ~/.ssh/id_rsa-acceptance \ + --load-path tests/lib \ + --pre-suite tests/acceptance/pre-suite \ + --tests tests/acceptance/tests + EOS + sh command +end + +task :acceptance_tests => [:basic_enviroment_variable_check, :acceptance_enviroment_varible_check] +task :reference_tests => [:basic_enviroment_variable_check] + +task :basic_enviroment_variable_check do + abort('PLATFORM variable not present, aborting test.') unless ENV["PLATFORM"] + abort('MODULE_VERSION variable not present, aborting test.') unless ENV["MODULE_VERSION"] +end + +task :acceptance_enviroment_varible_check do + if ENV["BEAKER_PE_DIR"] && ENV["PE_DIST_DIR"] + abort('Either BEAKER_PE_DIR or PE_DIST_DIR variable should be set but not both, aborting test.') + end + if !ENV["BEAKER_PE_DIR"] && !ENV["PE_DIST_DIR"] + abort('Neither BEAKER_PE_DIR or PE_DIST_DIR variable is set, aborting test.') + end +end + diff --git a/modules/utilities/windows/repository_managers/chocolatey/appveyor.yml b/modules/utilities/windows/repository_managers/chocolatey/appveyor.yml new file mode 100644 index 000000000..543c81d2b --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/appveyor.yml @@ -0,0 +1,44 @@ +version: 1.1.x.{build} +skip_commits: + message: /^\(?doc\)?.*/ +clone_depth: 10 +init: +- SET +- 'mkdir C:\ProgramData\PuppetLabs\code && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\facter && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\hiera && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\puppet\var && exit 0' +environment: + matrix: + - PUPPET_GEM_VERSION: ~> 3.0 + RUBY_VER: 193 + - PUPPET_GEM_VERSION: ~> 3.0 + RUBY_VER: 200-x64 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 21 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 21-x64 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 23 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 23-x64 + - PUPPET_GEM_VERSION: 3.0.0 + RUBY_VER: 193 +matrix: + fast_finish: true +install: +- SET PATH=C:\Ruby%RUBY_VER%\bin;%PATH% +- bundle install --jobs 4 --retry 2 --without system_tests +- type Gemfile.lock +build: off +test_script: +- bundle exec puppet -V +- ruby -v +- bundle exec rspec spec/unit -fd -b +notifications: +- provider: Email + to: + - nobody@nowhere.com + on_build_success: false + on_build_failure: false + on_build_status_changed: false diff --git a/modules/utilities/windows/repository_managers/chocolatey/checksums.json b/modules/utilities/windows/repository_managers/chocolatey/checksums.json new file mode 100644 index 000000000..4cbc88179 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/checksums.json @@ -0,0 +1,91 @@ +{ + "CHANGELOG.md": "ed75f911df877999ffe0b3d85c6d4c84", + "CONTRIBUTING.md": "2398672bb3e7c2a5ec11a9cea0f809e9", + "Gemfile": "e0713c9514c65b3bcce69e0b2bace8a7", + "LICENSE": "175792518e4ac015ab6696d16c4f607e", + "MAINTAINERS.md": "00d7777a4e0e4eed90437b4a972cdd6c", + "NOTICE": "267e1dc2b672516ff482a3fe8b857e2c", + "README.md": "a76ff6fb65c846cc273cb66333c2bbb5", + "Rakefile": "cde4be099ca764cc6bd7d706d2e37c90", + "appveyor.yml": "00752619ea15b8c6d287a7ae23a6c668", + "examples/init.pp": "1af719123c9af01ce6efd8de8465ad03", + "lib/facter/choco_install_path.rb": "36ece697e936cc950f8946ca2ac11729", + "lib/facter/chocolateyversion.rb": "ebbfc8535589aad01717852c93975ac3", + "lib/puppet/provider/chocolateyconfig/windows.rb": "048191d42df7e85cfc330569f500f88e", + "lib/puppet/provider/chocolateyfeature/windows.rb": "218b285cde1d86b2a530c194501de8d4", + "lib/puppet/provider/chocolateysource/windows.rb": "0d95da93a40c687a8a631d3c0c539609", + "lib/puppet/provider/package/chocolatey.rb": "80471c41d85faf45a521b5d8467fdd8b", + "lib/puppet/type/chocolateyconfig.rb": "1d86ef49b0faf6a321ea3ca24f9f39b1", + "lib/puppet/type/chocolateyfeature.rb": "1515df1d174d814ffc9bfff073a9051e", + "lib/puppet/type/chocolateysource.rb": "e2054da90dd124161fc64af9c11faf60", + "lib/puppet_x/chocolatey/chocolatey_common.rb": "03d3bf0cf1d2ef4a6843c3cd2e616c53", + "lib/puppet_x/chocolatey/chocolatey_install.rb": "e1e172d99659eb31882d64f88aacff60", + "lib/puppet_x/chocolatey/chocolatey_version.rb": "3155abe3c0a0b20770d322654c02e78d", + "manifests/config.pp": "307f680eb54988b9df8bd16e597a5843", + "manifests/init.pp": "984c562211c9bb0a7262531e1e5b2ef0", + "manifests/install.pp": "091c863df390704a9b01c2eb10c5b133", + "manifests/params.pp": "4a73fc3b0c6379551bdad6e5d3f357b4", + "metadata.json": "140403f7468691a05d49f54a52da9bc1", + "spec/classes/config_spec.rb": "8e6bb682200b29e2d622fa1d20e2a382", + "spec/classes/coverage_spec.rb": "5b6dfa0dd426aca0ccfae23c0a629f0b", + "spec/classes/init_spec.rb": "9d1316b761393c24fe7346aca679e69a", + "spec/classes/install_spec.rb": "76c2b4d46d8ddeac0d87ecb6612d58a4", + "spec/spec_helper.rb": "7adb146e2bdbb16f41786e325f33b1c5", + "spec/unit/facter/choco_install_path_spec.rb": "f23010110c2084aadc544b5c4d42c558", + "spec/unit/facter/chocolateyversion_spec.rb": "9ae92a85ed91c5f08edc7b8c4e8e53ac", + "spec/unit/puppet/provider/chocolateyconfig/windows_spec.rb": "ad562ea5608d75115d33111aca94b524", + "spec/unit/puppet/provider/chocolateyfeature/windows_spec.rb": "1e5bbb2cafae10e6a57b4c02f223ba80", + "spec/unit/puppet/provider/chocolateysource/windows_spec.rb": "1ad0760ce83a9590de51866f9f1de3d3", + "spec/unit/puppet/provider/package/chocolatey_spec.rb": "f6fdd633b4765f8ff0e0bc74bcd09769", + "spec/unit/puppet/type/chocolateyconfig_spec.rb": "b97b3604c59f9f8d011c4cd67cff4260", + "spec/unit/puppet/type/chocolateyfeature_spec.rb": "a2bf2e2fb8a45f8eefa43a0f289b0e40", + "spec/unit/puppet/type/chocolateysource_spec.rb": "1a71f966fac89b623decd08a401f08c6", + "spec/unit/puppet_x/chocolatey/chocolatey_common_spec.rb": "cb9087f4aa76641719bb5454fb474aa3", + "spec/unit/puppet_x/chocolatey/chocolatey_install_spec.rb": "5af79f6838ea628b1f55afcce1020279", + "spec/unit/puppet_x/chocolatey/chocolatey_version_spec.rb": "3f8e5abf71112d84a5be8ad096aad6ca", + "templates/InstallChocolatey.ps1.erb": "2adb1ce976d25907164b15cf1f16fd02", + "tests/acceptance/pre-suite/00_pe_install.rb": "cfe3fa761b0560792eacb872442dc9c7", + "tests/acceptance/pre-suite/01_chocolatey_module.install.rb": "9d11cc217d7f53ddffa36de7610c09b7", + "tests/acceptance/pre-suite/02_chocolatey_application_install.rb": "15205c3858c4d0a9c098541d9e0a7191", + "tests/acceptance/tests/hello.rb": "44c8a17a9a8ace86055fca4cf66d5a90", + "tests/lib/chocolatey_helper.rb": "c12b7e4bb5f06046bffc1031a866dd6b", + "tests/reference/pre-suite/00_install_certs.rb": "0814e2c5639947378749d0001ada530a", + "tests/reference/pre-suite/01_puppet_agent_install.rb": "34107ab52e53672aebe2f11b0759d9d3", + "tests/reference/pre-suite/02_chocolatey_module_install.rb": "515a96abf6a9660a113bcee882c7dd9c", + "tests/reference/pre-suite/03_chocolatey_application_install.rb": "33e65471ef20e5a34fdad72142c4a6b0", + "tests/reference/tests/chocolateyconfig/add_new_config_item.rb": "ca7e6cb60da8f6e41d1bb039a0f08af3", + "tests/reference/tests/chocolateyconfig/add_value_to_existing_config.rb": "407e1ea27b86a7e255f60330e7d3410e", + "tests/reference/tests/chocolateyconfig/change_config_value.rb": "3e6184e3a44a5ef3e82ac88e4c2da54b", + "tests/reference/tests/chocolateyconfig/ensure_config_value_with_password_in_name.rb": "c1171b2d053318df65af8fa7641b6334", + "tests/reference/tests/chocolateyconfig/fail_to_appy_bad_manifest.rb": "cc4261382302a4b18cbceedcb6bccdb1", + "tests/reference/tests/chocolateyconfig/fail_to_set_present_without_value.rb": "eb9093717f14d2102140d9a995321e60", + "tests/reference/tests/chocolateyconfig/remove_config_value_with_password_in_name.rb": "123e9d59bd3e1e449d0e9ec2eaae1f86", + "tests/reference/tests/chocolateyconfig/remove_value_from_config.rb": "1d4f7f8dc78e728b1d582e29845bc9d7", + "tests/reference/tests/chocolateyfeature/disable_disabled_feature.rb": "b0e6527d227f49b1baf67082891ab43e", + "tests/reference/tests/chocolateyfeature/disable_enabled_feature.rb": "201db0be084202704ec41967eda28865", + "tests/reference/tests/chocolateyfeature/enable_disabled_feature.rb": "87a8704e1c5b70bca03f1d2a715198b4", + "tests/reference/tests/chocolateyfeature/enable_enabled_feature.rb": "eedaf5068e428db99e409a3f912cae97", + "tests/reference/tests/chocolateyfeature/fail_to_enable_nonexistent_feature.rb": "8000a68d7ccb62e5f6b472facb7065a1", + "tests/reference/tests/chocolateyfeature/fail_to_remove_feature.rb": "aea6bb681d46924fe98d34295cf07807", + "tests/reference/tests/chocolateypackage/install_and_remove_good_package.rb": "b1a25035d4dad0ac42caeb1becfd56f1", + "tests/reference/tests/chocolateypackage/install_and_remove_good_package_utf-8.rb": "ea75ffbe326df41a35e4731fe08a3b61", + "tests/reference/tests/chocolateysource/add_priority_to_existing_source.rb": "c6788dd1224006c819549de936076be2", + "tests/reference/tests/chocolateysource/add_source_all_options.rb": "47c6aa9fc3f062e3fdc638b001051636", + "tests/reference/tests/chocolateysource/add_source_minimal.rb": "bbc7ed0b2d3faaae707872d6d73fdd09", + "tests/reference/tests/chocolateysource/add_source_normal.rb": "29fbe2335387aa97d54e994dffe26cae", + "tests/reference/tests/chocolateysource/add_user_pass_to_existing_source.rb": "e2cbd76cc086b16d1fde5a85ac7c1fb7", + "tests/reference/tests/chocolateysource/change_existing_priority.rb": "81f5bfe7f7f2d42ce0907abbf9513185", + "tests/reference/tests/chocolateysource/change_existing_source_location.rb": "afdadbae4c3373620eef0d23ff001716", + "tests/reference/tests/chocolateysource/change_user_pass.rb": "6e6af759af2de6429cd49d9c19564934", + "tests/reference/tests/chocolateysource/disable_existing_source.rb": "bf860a4876de665781735f370d23aa6f", + "tests/reference/tests/chocolateysource/disable_existing_source_two_runs.rb": "2ecb9c4b684ab92ba2af5ea8bdc3323d", + "tests/reference/tests/chocolateysource/fail_to_apply_source_without_location.rb": "7ebcb851a3402b93b8ca99dce2afacb6", + "tests/reference/tests/chocolateysource/fail_to_appy_bad_manifest.rb": "c81e640adf6b408db72dd2216819bfe9", + "tests/reference/tests/chocolateysource/fail_to_set_password_without_user.rb": "c27cbaadb8f2df3edebf4beca8aca73c", + "tests/reference/tests/chocolateysource/fail_to_set_user_without_password.rb": "2abfeb800092c7217d7e36dcbab75d78", + "tests/reference/tests/chocolateysource/remove_existing_source.rb": "e5fa96e486e522300e03270caf3bc613", + "tests/reference/tests/chocolateysource/remove_priority_from_existing_source.rb": "d49e25470cf51d9bc69d8670c0b600ad", + "tests/reference/tests/chocolateysource/remove_user_pass_from_existing_source.rb": "87920f736ad9958ce152944629292ffc", + "tests/test_run_scripts/acceptance_tests.sh": "d7bdd1185f73fd28389a6d60da0f0d69", + "tests/test_run_scripts/reference_tests.sh": "22220232bde40afa6c90fcdfc46943e0" +} \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/chocolatey.pp b/modules/utilities/windows/repository_managers/chocolatey/chocolatey.pp new file mode 100644 index 000000000..0e42ca6db --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/chocolatey.pp @@ -0,0 +1 @@ +include chocolatey \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/examples/init.pp b/modules/utilities/windows/repository_managers/chocolatey/examples/init.pp new file mode 100644 index 000000000..8154edd19 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/examples/init.pp @@ -0,0 +1,23 @@ +# The baseline for module testing used by Puppet Labs is that each manifest +# should have a corresponding test manifest that declares that class or defined +# type. +# +# Tests are then run by using puppet apply --noop (to check for compilation errors +# and view a log of events) or by fully applying the test in a virtual environment +# (to compare the resulting system state to the desired state). +# +# Learn more about module testing here: http://docs.puppetlabs.com/guides/tests_smoke.html +# +# With symlinks on Windows, please run the following command an administrative command prompt (substituting the proper directories): + +package { $pkg: + ensure => 'latest', + provider => 'chocolatey', +} + +# mklink /D C:\ProgramData\PuppetLabs\puppet\etc\modules\chocolatey C:\code\puppetlabs\puppetlabs-chocolatey +# mklink /D C:\ProgramData\PuppetLabs\code\environments\production\modules\chocolatey C:\code\puppetlabs\puppetlabs-chocolatey + +chocolateysource { 'local': + location => 'c:\packages', +} diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/facter/choco_install_path.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/facter/choco_install_path.rb new file mode 100644 index 000000000..3301fd0ba --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/facter/choco_install_path.rb @@ -0,0 +1,9 @@ +require 'pathname' +require Pathname.new(__FILE__).dirname + '../' + 'puppet_x/chocolatey/chocolatey_install' + +Facter.add('choco_install_path') do + confine :osfamily => :windows + setcode do + PuppetX::Chocolatey::ChocolateyInstall.install_path || 'C:\ProgramData\chocolatey' + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/facter/chocolateyversion.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/facter/chocolateyversion.rb new file mode 100644 index 000000000..3d049c53b --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/facter/chocolateyversion.rb @@ -0,0 +1,9 @@ +require 'pathname' +require Pathname.new(__FILE__).dirname + '../' + 'puppet_x/chocolatey/chocolatey_version' + +Facter.add('chocolateyversion') do + confine :osfamily => :windows + setcode do + PuppetX::Chocolatey::ChocolateyVersion.version || '0' + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyconfig/windows.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyconfig/windows.rb new file mode 100644 index 000000000..69d3ffaa3 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyconfig/windows.rb @@ -0,0 +1,144 @@ +require 'puppet/type' +require 'pathname' +require 'rexml/document' + +Puppet::Type.type(:chocolateyconfig).provide(:windows) do + confine :operatingsystem => :windows + defaultfor :operatingsystem => :windows + + require Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/chocolatey/chocolatey_common' + include PuppetX::Chocolatey::ChocolateyCommon + + CONFIG_MINIMUM_SUPPORTED_CHOCO_VERSION = '0.9.10.0' + + commands :chocolatey => PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command + + def initialize(value={}) + super(value) + @property_flush = {} + end + + def properties + if @property_hash.empty? + @property_hash = query || { :ensure => ( :absent )} + @property_hash[:ensure] = :absent if @property_hash.empty? + end + @property_hash.dup + end + + def query + self.class.configs.each do |config| + return config.properties if @resource[:name][/\A\S*/].downcase == config.name.downcase + end + + return {} + end + + def self.get_configs + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + + choco_config = PuppetX::Chocolatey::ChocolateyCommon.choco_config_file + raise Puppet::ResourceError, "Config file not found for Chocolatey. Please make sure you have Chocolatey installed." if choco_config.nil? + raise Puppet::ResourceError, "An install was detected, but was unable to locate config file at #{choco_config}." unless PuppetX::Chocolatey::ChocolateyCommon.file_exists?(choco_config) + + Puppet.debug("Gathering sources from '#{choco_config}'.") + config = REXML::Document.new File.new(choco_config, 'r') + + config.elements.to_a( '//add' ) + end + + def self.get_config(element) + config = {} + return config if element.nil? + + config[:name] = element.attributes['key'] if element.attributes['key'] + config[:value] = element.attributes['value'] if element.attributes['value'] + config[:description] = element.attributes['description'] if element.attributes['description'] + + config[:ensure] = :present + + Puppet.debug("Loaded config '#{config.inspect}'.") + + config + end + + def self.configs + @configs ||= get_configs.collect do |item| + config = get_config(item) + new(config) + end + end + + def self.refresh_configs + @configs = nil + self.configs + end + + def self.instances + configs + end + + def self.prefetch(resources) + instances.each do |provider| + if (resource = resources[provider.name]) + resource.provider = provider + end + end + end + + def create + @property_flush[:ensure] = :present + end + + def exists? + @property_hash[:ensure] == :present + end + + def destroy + @property_flush[:ensure] = :absent + end + + def validate + choco_version = Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) + if PuppetX::Chocolatey::ChocolateyCommon.file_exists?(PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command) && choco_version < Gem::Version.new(CONFIG_MINIMUM_SUPPORTED_CHOCO_VERSION) + raise Puppet::ResourceError, "Chocolatey version must be '#{CONFIG_MINIMUM_SUPPORTED_CHOCO_VERSION}' to manage configuration values. Detected '#{choco_version}' as your version. Please upgrade Chocolatey." + end + end + + mk_resource_methods + + def flush + choco_version = Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) + + args = [] + args << 'config' + + # look at the hash, then flush if present. + # If all else fails, looks at resource[:ensure] + property_ensure = @property_hash[:ensure] + property_ensure = @property_flush[:ensure] if @property_flush[:ensure] + property_ensure = resource[:ensure] if property_ensure.nil? + + command = 'set' + command = 'unset' if property_ensure == :absent + + args << command + args << '--name' << resource[:name] + + if property_ensure != :absent + args << '--value' << resource[:value] + end + + begin + Puppet::Util::Execution.execute([command(:chocolatey), *args]) + rescue Puppet::ExecutionFailure => e + raise Puppet::Error, "An error occurred running choco. Unable to set Chocolateyconfig[#{self.name}]: #{e}" + end + + @property_hash.clear + @property_flush.clear + + self.class.refresh_configs + @property_hash = query + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyfeature/windows.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyfeature/windows.rb new file mode 100644 index 000000000..de0ccfd22 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateyfeature/windows.rb @@ -0,0 +1,126 @@ +require 'puppet/type' +require 'pathname' +require 'rexml/document' + +Puppet::Type.type(:chocolateyfeature).provide(:windows) do + confine :operatingsystem => :windows + defaultfor :operatingsystem => :windows + + require Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/chocolatey/chocolatey_common' + include PuppetX::Chocolatey::ChocolateyCommon + + FEATURE_MINIMUM_SUPPORTED_CHOCO_VERSION = '0.9.9.0' + + commands :chocolatey => PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command + + def initialize(value={}) + super(value) + @property_flush = {} + end + + def properties + if @property_hash.empty? + @property_hash = query + end + @property_hash.dup + end + + def query + self.class.features.each do |feature| + return feature.properties if @resource[:name][/\A\S*/].downcase == feature.name.downcase + end + + return {} + end + + def self.get_features + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + + choco_config = PuppetX::Chocolatey::ChocolateyCommon.choco_config_file + raise Puppet::ResourceError, "Config file not found for Chocolatey. Please make sure you have Chocolatey installed." if choco_config.nil? + raise Puppet::ResourceError, "An install was detected, but was unable to locate config file at #{choco_config}." unless PuppetX::Chocolatey::ChocolateyCommon.file_exists?(choco_config) + + Puppet.debug("Gathering features from '#{choco_config}'.") + config = REXML::Document.new File.new(choco_config, 'r') + + config.elements.to_a( '//feature' ) + end + + def self.get_feature(element) + feature = {} + return feature if element.nil? + + feature[:name] = element.attributes['name'].downcase if element.attributes['name'] + feature[:description] = element.attributes['description'].downcase if element.attributes['description'] + + enabled = false + enabled = element.attributes['enabled'].downcase == 'true' if element.attributes['enabled'] + + feature[:ensure] = :disabled + feature[:ensure] = :enabled if enabled + + Puppet.debug("Loaded feature '#{feature.inspect}'.") + + feature + end + + def self.features + get_features.collect do |item| + feature = get_feature(item) + new(feature) + end + end + + def self.instances + features + end + + def self.prefetch(resources) + instances.each do |provider| + if (resource = resources[provider.name]) + resource.provider = provider + end + end + end + + def enable + @property_flush[:ensure] = :enabled + end + + def exists? + @property_hash[:ensure] == :enabled + end + + def disable + @property_flush[:ensure] = :disabled + end + + def validate + choco_version = Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) + if PuppetX::Chocolatey::ChocolateyCommon.file_exists?(PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command) && choco_version < Gem::Version.new(FEATURE_MINIMUM_SUPPORTED_CHOCO_VERSION) + raise Puppet::ResourceError, "Chocolatey version must be '#{FEATURE_MINIMUM_SUPPORTED_CHOCO_VERSION}' to manage configuration values with Puppet. Detected '#{choco_version}' as your version. Please upgrade Chocolatey to use this resource." + end + end + + mk_resource_methods + + def flush + args = [] + args << 'feature' + + command = 'enable' + command = 'disable' if @property_flush[:ensure] == :disabled + + args << command + args << '--name' << resource[:name] + + Puppet::Util::Execution.execute([command(:chocolatey), *args]) + + @property_hash.clear + @property_flush.clear + + self.class.features + @property_hash = query + end + +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateysource/windows.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateysource/windows.rb new file mode 100644 index 000000000..784dfc511 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/chocolateysource/windows.rb @@ -0,0 +1,197 @@ +require 'puppet/type' +require 'pathname' +require 'rexml/document' + +Puppet::Type.type(:chocolateysource).provide(:windows) do + confine :operatingsystem => :windows + defaultfor :operatingsystem => :windows + + require Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/chocolatey/chocolatey_common' + include PuppetX::Chocolatey::ChocolateyCommon + + MINIMUM_SUPPORTED_CHOCO_VERSION = '0.9.9.0' + MINIMUM_SUPPORTED_CHOCO_VERSION_PRIORITY = '0.9.9.9' + + commands :chocolatey => PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command + + def initialize(value={}) + super(value) + @property_flush = {} + end + + def properties + if @property_hash.empty? + @property_hash = query || { :ensure => ( :absent )} + @property_hash[:ensure] = :absent if @property_hash.empty? + end + @property_hash.dup + end + + def query + self.class.sources.each do |source| + return source.properties if @resource[:name][/\A\S*/].downcase == source.name.downcase + end + + return {} + end + + def self.get_sources + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + + choco_config = PuppetX::Chocolatey::ChocolateyCommon.choco_config_file + raise Puppet::ResourceError, "Config file not found for Chocolatey. Please make sure you have Chocolatey installed." if choco_config.nil? + raise Puppet::ResourceError, "An install was detected, but was unable to locate config file at #{choco_config}." unless PuppetX::Chocolatey::ChocolateyCommon.file_exists?(choco_config) + + Puppet.debug("Gathering sources from '#{choco_config}'.") + config = REXML::Document.new File.new(choco_config, 'r') + + config.elements.to_a( '//source' ) + end + + def self.get_source(element) + source = {} + return source if element.nil? + + source[:name] = element.attributes['id'].downcase if element.attributes['id'] + source[:location] = element.attributes['value'].downcase if element.attributes['value'] + + disabled = false + disabled = element.attributes['disabled'].downcase == 'true' if element.attributes['disabled'] + source[:ensure] = :present + source[:ensure] = :disabled if disabled + + source[:priority] = 0 + source[:priority] = element.attributes['priority'].downcase if element.attributes['priority'] + + source[:user] = '' + source[:user] = element.attributes['user'].downcase if element.attributes['user'] + + Puppet.debug("Loaded source '#{source.inspect}'.") + + source + end + + def self.sources + @sources ||= get_sources.collect do |item| + source = get_source(item) + new(source) + end + end + + def self.refresh_sources + @sources = nil + self.sources + end + + def self.instances + sources + end + + def self.prefetch(resources) + instances.each do |provider| + if (resource = resources[provider.name]) + resource.provider = provider + end + end + end + + def create + @property_flush[:ensure] = :present + end + + def exists? + @property_hash[:ensure] == :present + end + + def disable + @property_flush[:ensure] = :disabled + end + + def destroy + @property_flush[:ensure] = :absent + end + + def validate + choco_version = Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) + if PuppetX::Chocolatey::ChocolateyCommon.file_exists?(PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command) && choco_version < Gem::Version.new(MINIMUM_SUPPORTED_CHOCO_VERSION) + raise Puppet::ResourceError, "Chocolatey version must be '#{MINIMUM_SUPPORTED_CHOCO_VERSION}' to manage configuration values with Puppet. Detected '#{choco_version}' as your version. Please upgrade Chocolatey to use this resource." + end + + if choco_version < Gem::Version.new(MINIMUM_SUPPORTED_CHOCO_VERSION_PRIORITY) && resource[:priority] && resource[:priority] != 0 + Puppet.warning("Chocolatey is unable to manage priority for sources when version is less than #{MINIMUM_SUPPORTED_CHOCO_VERSION_PRIORITY}. The value you set will be ignored.") + end + + # location is always filled in with puppet resource, but + # resource[:location] is always empty (because it has a different + # code path where validation occurs before all properties/params + # have been set), resulting in errors + # location is always :absent when a manifest runs this with a missing + # `location => value` + location_check = location + # location could be :absent, which mk_resource_method will set it to + # resource[:location] is nil when running puppet resource + # if you remove `location => value` + location_check = resource[:location] if location_check == :absent + if (resource[:ensure] == :present && (location_check.nil? || location_check.strip == '')) + raise ArgumentError, "A non-empty location must be specified when ensure => present." + end + + if resource[:password] && resource[:password] != '' + Puppet.debug("The password is not ensurable, so Puppet is unable to change the value using chocolateysource resource. As a workaround, a password change can be in the form of an exec. Reference Chocolateysource[#{resource[:name]}]") + end + end + + mk_resource_methods + + def flush + args = [] + args << 'source' + + # look at the hash, then flush if present. + # If all else fails, looks at resource[:ensure] + property_ensure = @property_hash[:ensure] + property_ensure = @property_flush[:ensure] if @property_flush[:ensure] + property_ensure = resource[:ensure] if property_ensure.nil? + + command = 'add' + command = 'remove' if property_ensure == :absent + command = 'disable' if property_ensure == :disabled + + args << command + args << '--name' << resource[:name] + + if command == 'add' + args << '--source' << resource[:location] + + if resource[:user] && resource[:user] != '' + args << '--user' << resource[:user] + args << '--password' << resource[:password] + end + + choco_gem_version = Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) + if choco_gem_version >= Gem::Version.new(MINIMUM_SUPPORTED_CHOCO_VERSION_PRIORITY) + args << '--priority' << resource[:priority] + end + end + + begin + Puppet::Util::Execution.execute([command(:chocolatey), *args]) + rescue Puppet::ExecutionFailure + raise Puppet::Error, "An error occurred running choco. Unable to set Chocolatey source configuration for #{self.inspect}" + end + + if property_ensure == :present + begin + Puppet::Util::Execution.execute([command(:chocolatey), 'source', 'enable', '--name', resource[:name]]) + rescue Puppet::ExecutionFailure + raise Puppet::Error, "An error occurred running choco. Unable to set Chocolatey source configuration for #{self.inspect}" + end + end + + @property_hash.clear + @property_flush.clear + + self.class.refresh_sources + @property_hash = query + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/package/chocolatey.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/package/chocolatey.rb new file mode 100644 index 000000000..d01ad0d5a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/provider/package/chocolatey.rb @@ -0,0 +1,280 @@ +require 'puppet/provider/package' +require 'pathname' +require Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/chocolatey/chocolatey_install' + +Puppet::Type.type(:package).provide(:chocolatey, :parent => Puppet::Provider::Package) do + + desc "Manages packages using Chocolatey (Windows package manager). + + The syntax for Chocolatey using the puppet provider is a much + closer match to *nix package managers, bringing a more agnostic + approach to package management across platforms. Chocolatey packages + usually contain all of the logic to install software silently on a + Windows machine, much like RPM (yum) or DPKG (apt). + + Installs can be as simple as + + package {'git': + ensure => latest, + } + + See the ReadMe for more information." + + confine :operatingsystem => :windows + has_feature :installable + has_feature :uninstallable + has_feature :upgradeable + has_feature :versionable + has_feature :install_options + has_feature :uninstall_options + has_feature :holdable + #has_feature :package_settings + + require Pathname.new(__FILE__).dirname + '../../../' + 'puppet_x/chocolatey/chocolatey_common' + include PuppetX::Chocolatey::ChocolateyCommon + + commands :chocolatey => PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command + + def initialize(value={}) + super(value) + end + + def print() + notice("The value is: '${name}'") + end + + def self.is_compiled_choco? + Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) >= Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon::FIRST_COMPILED_CHOCO_VERSION) + end + + def is_compiled_choco? + self.class.is_compiled_choco? + end + + def install + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + choco_exe = is_compiled_choco? + + # always unhold on install + unhold if choco_exe + + args = [] + + # also will need to address -sidebyside or -m in the install args to allow + # multiple versions to be installed. + args << 'install' + + should = @resource.should(:ensure) + case should + when true, false, Symbol + args << @resource[:name][/\A\S*/] + else + args.clear + if choco_exe + args << 'upgrade' + else + args << 'update' + end + + # Add the package version + args << @resource[:name][/\A\S*/] << '-version' << @resource[:ensure] + end + + if choco_exe + args << '-y' + end + + if @resource[:source] + args << '-source' << @resource[:source] + end + + args << @resource[:install_options] + + if Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) >= Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon::MINIMUM_SUPPORTED_CHOCO_VERSION_EXIT_CODES) + args << '--ignore-package-exit-codes' + end + + chocolatey(*args) + end + + def uninstall + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + choco_exe = is_compiled_choco? + + # always unhold on uninstall + unhold if choco_exe + + args = 'uninstall', @resource[:name][/\A\S*/] + + if choco_exe + args << '-fy' + end + + choco_version = Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) + if !choco_exe || choco_version >= Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon::MINIMUM_SUPPORTED_CHOCO_UNINSTALL_SOURCE) + if @resource[:source] + args << '-source' << @resource[:source] + end + end + + args << @resource[:uninstall_options] + + if Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) >= Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon::MINIMUM_SUPPORTED_CHOCO_VERSION_EXIT_CODES) + args << '--ignore-package-exit-codes' + end + + chocolatey(*args) + end + + def update + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + choco_exe = is_compiled_choco? + + # always unhold on upgrade + unhold if choco_exe + + if choco_exe + args = 'upgrade', @resource[:name][/\A\S*/], '-y' + else + args = 'update', @resource[:name][/\A\S*/] + end + + if @resource[:source] + args << '-source' << @resource[:source] + end + + args << @resource[:install_options] + + if Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon.choco_version) >= Gem::Version.new(PuppetX::Chocolatey::ChocolateyCommon::MINIMUM_SUPPORTED_CHOCO_VERSION_EXIT_CODES) + args << '--ignore-package-exit-codes' + end + + if self.query + chocolatey(*args) + else + self.install + end + end + + # from puppet-dev mailing list + # Puppet will call the query method on the instance of the package + # provider resource when checking if the package is installed already or + # not. + # It's a determination for one specific package, the package modeled by + # the resource the method is called on. + # Query provides the information for the single package identified by @Resource[:name]. + def query + self.class.instances.each do |package| + return package.properties if @resource[:name][/\A\S*/].downcase == package.name.downcase + end + + return nil + end + + def self.listcmd + args = [] + args << 'list' + args << '-lo' + if is_compiled_choco? + args << '-r' + end + + [command(:chocolatey), *args] + end + + def self.instances + packages = [] + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + choco_exe = is_compiled_choco? + begin + pins = [] + pin_output = nil unless choco_exe + #don't add -r yet, as there is an issue in 0.9.9.9/0.9.9.10 that returns full list plus pins + pin_output = Puppet::Util::Execution.execute([command(:chocolatey), 'pin', 'list']) if choco_exe + unless pin_output.nil? + pin_output.split("\n").each { |pin| pins << pin.split('|')[0] } + end + + execpipe(listcmd) do |process| + process.each_line do |line| + line.chomp! + if line.empty? or line.match(/Reading environment variables.*/); next; end + raise Puppet::Error, "At least one source must be enabled." if line.match(/Unable to search for packages.*/) + if choco_exe + values = line.split('|') + else + values = line.split(' ') + end + values[1] = :held if pins.include? values[0] + packages << new({ :name => values[0].downcase, :ensure => values[1], :provider => self.name }) + end + end + rescue Puppet::ExecutionFailure + return nil + end + + packages + end + + def latestcmd + choco_exe = is_compiled_choco? + if choco_exe + args = 'upgrade', '--noop', @resource[:name][/\A\S*/], '-r' + else + args = 'version', @resource[:name][/\A\S*/] + end + + if @resource[:source] + args << '-source' << @resource[:source] + end + + unless choco_exe + args << '| findstr /R "latest" | findstr /V "latestCompare"' + end + + [command(:chocolatey), *args] + end + + def latest + package_ver = '' + PuppetX::Chocolatey::ChocolateyCommon.set_env_chocolateyinstall + begin + execpipe(latestcmd) do |process| + process.each_line do |line| + line.chomp! + if line.empty?; next; end + if is_compiled_choco? + values = line.split('|') + package_ver = values[2] + else + # Example: ( latest : 2013.08.19.155043 ) + values = line.split(':').collect(&:strip).delete_if(&:empty?) + package_ver = values[1] + end + end + end + rescue Puppet::ExecutionFailure + return nil + end + + package_ver + end + + def hold + raise ArgumentError, 'Only choco v0.9.9+ can use ensure => held' unless is_compiled_choco? + + install + + args = 'pin', 'add', '-n', @resource[:name][/\A\S*/] + + chocolatey(*args) + end + + def unhold + return unless is_compiled_choco? + + Puppet::Util::Execution.execute([command(:chocolatey), 'pin','remove', '-n', @resource[:name][/\A\S*/]], :failonfail => false) + end + + +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyconfig.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyconfig.rb new file mode 100644 index 000000000..ba4d4a65a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyconfig.rb @@ -0,0 +1,85 @@ +require 'puppet/type' +require 'pathname' + +Puppet::Type.newtype(:chocolateyconfig) do + + @doc = <<-'EOT' + Allows managing config settings for Chocolatey. + Configuration values provide settings for users + to configure aspects of Chocolatey and the way it + functions. Similar to features, except allow for user + configured values. Requires 0.9.10+. Learn more about + config at https://chocolatey.org/docs/commands-config + EOT + + ensurable do + newvalue(:present) { provider.create } + newvalue(:absent) { provider.destroy } + defaultto :present + + def retrieve + provider.properties[:ensure] + end + + end + + newparam(:name) do + desc "The name of the config setting. Used for uniqueness. + Puppet is not able to easily manage any values that + include Password in the key name in them as they + will be encrypted in the configuration file." + + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty name must be specified." + end + end + + isnamevar + + munge do |value| + value.downcase + end + + def insync?(is) + is.downcase == should.downcase + end + end + + newproperty(:value) do + desc "The value of the config setting. If the + name includes 'password', then the value is + not ensurable due to being encrypted in the + configuration file." + + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty value must be specified. To unset value, use ensure => absent" + end + end + + def insync?(is) + if (resource[:name] =~ /password/i) + # If name contains password, it is + # always in sync if there is a value + return (is.nil? || is.empty?) == (should.nil? || should.empty?) + else + return is.downcase == should.downcase + end + end + end + + validate do + if self[:ensure] != :absent + raise ArgumentError, "Unless ensure => absent, value is required." if self[:value].nil? || self[:value].empty? + end + + if provider.respond_to?(:validate) + provider.validate + end + end + + autorequire(:exec) do + ['install_chocolatey_official'] + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyfeature.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyfeature.rb new file mode 100644 index 000000000..da5ebcc7f --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateyfeature.rb @@ -0,0 +1,57 @@ +require 'puppet/type' +require 'pathname' + +Puppet::Type.newtype(:chocolateyfeature) do + + @doc = <<-'EOT' + Allows managing features for Chocolatey. Features are + configuration that act as feature flippers to turn on or + off certain aspects of how Chocolatey works. + Learn more about features at + https://chocolatey.org/docs/commands-feature + + EOT + + newparam(:name) do + desc "The name of the feature. Used for uniqueness." + + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty name must be specified." + end + end + + isnamevar + + munge do |value| + value.downcase + end + + def insync?(is) + is.downcase == should.downcase + end + end + + ensurable do + newvalue(:enabled) { provider.enable } + newvalue(:disabled) { provider.disable } + + def retrieve + provider.properties[:ensure] + end + end + + validate do + if self[:ensure].nil? && provider.properties[:ensure].nil? + raise ArgumentError, "Invalid value for ensure. Valid values are enabled or disabled." + end + + if provider.respond_to?(:validate) + provider.validate + end + end + + autorequire(:exec) do + ['install_chocolatey_official'] + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateysource.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateysource.rb new file mode 100644 index 000000000..fcd4531b9 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet/type/chocolateysource.rb @@ -0,0 +1,141 @@ +require 'puppet/type' +require 'pathname' + +Puppet::Type.newtype(:chocolateysource) do + + @doc = <<-'EOT' + Allows managing sources for Chocolatey. A source can be a + folder, a CIFS share, a NuGet Http OData feed, or a full + Package Gallery. Learn more about sources at + https://chocolatey.org/docs/how-to-host-feed + + EOT + + ensurable do + newvalue(:present) { provider.create } + newvalue(:disabled) { provider.disable } + newvalue(:absent) { provider.destroy } + defaultto :present + + def retrieve + provider.properties[:ensure] + end + + end + + newparam(:name) do + desc "The name of the source. Used for uniqueness." + + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty name must be specified." + end + end + + isnamevar + + munge do |value| + value.downcase + end + + def insync?(is) + is.downcase == should.downcase + end + end + + newproperty(:location) do + desc "The location of the source repository. Can be a url pointing to + an OData feed (like chocolatey/chocolatey_server), a CIFS (UNC) share, + or a local folder. Required when `ensure => present` (the default for + `ensure`)." + + validate do |value| + if value.nil? or value.empty? + raise ArgumentError, "A non-empty location must be specified." + end + end + + def insync?(is) + is.downcase == should.downcase + end + end + + newproperty(:user) do + desc "Optional user name for authenticated feeds. + Requires at least Chocolatey v0.9.9.0. + Defaults to `nil`. Specifying an empty value is the + same as setting the value to nil or not specifying + the property at all." + + def insync?(is) + is.downcase == should.downcase + end + + defaultto '' + end + + newparam(:password) do + desc "Optional user password for authenticated feeds. + Not ensurable. Value is not able to be checked + with current value. If you need to update the password, + update another setting as well. + Requires at least Chocolatey v0.9.9.0. + Defaults to `nil`. Specifying an empty value is the + same as setting the value to nil or not specifying + the property at all." + + defaultto '' + end + + newproperty(:priority) do + desc "Optional priority for explicit feed order when + searching for packages across multiple feeds. + The lower the number the higher the priority. + Sources with a 0 priority are considered no priority + and are added after other sources with a priority + number. + Requires at least Chocolatey v0.9.9.9. + Defaults to 0." + + validate do |value| + if value.nil? + raise ArgumentError, "A non-empty priority must be specified." + end + raise ArgumentError, "An integer is necessary for priority. Specify 0 or remove for no priority." unless resource.is_numeric?(value) + end + + defaultto(0) + end + + validate do + if (!self[:user].nil? && self[:user].strip != '' && (self[:password].nil? || self[:password] == '')) || ((self[:user].nil? || self[:user].strip == '') && !self[:password].nil? && self[:password] != '') + raise ArgumentError, "If specifying user/password, you must specify both values." + end + + if provider.respond_to?(:validate) + provider.validate + end + end + + autorequire(:exec) do + ['install_chocolatey_official'] + end + + def munge_boolean(value) + case value + when true, "true", :true + :true + when false, "false", :false + :false + else + fail("munge_boolean only takes booleans") + end + end + + def is_numeric?(value) + # this is what stdlib does. Not sure if we want to emulate or not. + #numeric = %r{^-?(?:(?:[1-9]\d*)|0)$} + #if value.is_a? Integer or (value.is_a? String and value.match numeric) + Float(value) != nil rescue false + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_common.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_common.rb new file mode 100644 index 000000000..ebebceee2 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_common.rb @@ -0,0 +1,90 @@ +require 'pathname' +require Pathname.new(__FILE__).dirname + 'chocolatey_version' +require Pathname.new(__FILE__).dirname + 'chocolatey_install' + +module PuppetX + module Chocolatey + module ChocolateyCommon + + ## determines if C# version of choco + FIRST_COMPILED_CHOCO_VERSION = '0.9.9.0' + MINIMUM_SUPPORTED_CHOCO_VERSION_EXIT_CODES = '0.9.10.0' + MINIMUM_SUPPORTED_CHOCO_UNINSTALL_SOURCE = '0.9.10.0' + + def file_exists?(path) + File.exist?(path) + end + module_function :file_exists? + + def chocolatey_command + if Puppet::Util::Platform.windows? + # When attempting to find the choco command executable, the following + # paths are checked: + # - Start with the install_path. If choco is found with environment + # variables through the registry or a check on the + # ChocolateyInstall env var (first install of Choco may only have + # this), then use that path. + # - Next look to the most commonly used install location (ProgramData) + # - Fall back to the older install location for older installations + # - If all else fails, attempt to find Chocolatey in the default place + # it installs + chocoInstallPath = PuppetX::Chocolatey::ChocolateyInstall.install_path + + chocopath = (chocoInstallPath if (chocoInstallPath && file_exists?("#{chocoInstallPath}\\bin\\choco.exe"))) || + ('C:\ProgramData\chocolatey' if file_exists?('C:\ProgramData\chocolatey\bin\choco.exe')) || + ('C:\Chocolatey' if file_exists?('C:\Chocolatey\bin\choco.exe')) || + "#{ENV['ALLUSERSPROFILE']}\\chocolatey" + + chocopath += '\bin\choco.exe' + else + chocopath = 'choco.exe' + end + + chocopath + end + module_function :chocolatey_command + + def set_env_chocolateyinstall + ENV['ChocolateyInstall'] = PuppetX::Chocolatey::ChocolateyInstall.install_path + end + module_function :set_env_chocolateyinstall + + def choco_version + @chocoversion ||= self.strip_beta_from_version(PuppetX::Chocolatey::ChocolateyVersion.version) + end + module_function :choco_version + + def self.strip_beta_from_version(value) + return nil if value.nil? + + value.split(/-/)[0] + end + + def choco_config_file + chocoInstallPath = PuppetX::Chocolatey::ChocolateyInstall.install_path + choco_config = "#{chocoInstallPath}\\config\\chocolatey.config" + + # choco may be installed, but a config file doesn't exist until the + # first run of choco - trigger that by checking the version + choco_run_ensure_config = choco_version + + return choco_config if file_exists?(choco_config) + + old_choco_config = "#{chocoInstallPath}\\chocolateyinstall\\chocolatey.config" + + return old_choco_config if file_exists?(old_choco_config) + + return nil + end + module_function :choco_config_file + + # clears the cached values + def clear_cached_values + @chocoversion = nil + @compiled_choco = nil + end + module_function :clear_cached_values + + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_install.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_install.rb new file mode 100644 index 000000000..66c835af1 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_install.rb @@ -0,0 +1,34 @@ +module PuppetX + module Chocolatey + class ChocolateyInstall + + def self.install_path + value = nil + + if Puppet::Util::Platform.windows? + require 'win32/registry' + + begin + hive = Win32::Registry::HKEY_LOCAL_MACHINE + hive.open('SYSTEM\CurrentControlSet\Control\Session Manager\Environment', Win32::Registry::KEY_READ | 0x100) do |reg| + value = reg['ChocolateyInstall'] + end + rescue Win32::Registry::Error => e + value = nil + end + end + + # If machine level is not set, use process or user as the intended + # location where Chocolatey would be installed. + # Since it is technically possible that Chocolatey could exist on + # non-Windows installations, we don't want to confine this + # to just Windows. + if value.nil? + value = ENV['ChocolateyInstall'] + end + + value + end + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_version.rb b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_version.rb new file mode 100644 index 000000000..5e3478484 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/lib/puppet_x/chocolatey/chocolatey_version.rb @@ -0,0 +1,32 @@ +require 'pathname' +require Pathname.new(__FILE__).dirname + 'chocolatey_install' + +module PuppetX + module Chocolatey + class ChocolateyVersion + + OLD_CHOCO_MESSAGE = "Please run chocolatey /? or chocolatey help - chocolatey v" + + def self.version + version = nil + choco_path = "#{PuppetX::Chocolatey::ChocolateyInstall.install_path}\\bin\\choco.exe" + if Puppet::Util::Platform.windows? && File.exist?(choco_path) + begin + # call `choco -v` + # - new choco will output a single value e.g. `0.9.9` + # - old choco is going to return the default output e.g. `Please run chocolatey /?` + version = Puppet::Util::Execution.execute("#{choco_path} -v").gsub(OLD_CHOCO_MESSAGE,'') + # - other messages, such as upgrade warnings or warnings about + # installing the licensed extension once the license is installed + # may show up when running this comamnd. Remove those as well + version = version.split(/\r\n|\n|\r/).last.strip unless version.nil? + rescue StandardError => e + version = '0' + end + end + + version + end + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/manifests/config.pp b/modules/utilities/windows/repository_managers/chocolatey/manifests/config.pp new file mode 100644 index 000000000..bd51307bc --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/manifests/config.pp @@ -0,0 +1,34 @@ +# chocolatey::config - Private class used for configuration +class chocolatey::config { + assert_private() + + # this will require a second converge when choco is not + # installed the first time through. This is on purpose + # as we don't want to try to set these values for a + # version less than 0.9.9 and we don't know what the + # user may link to - it could be an older version of + # Chocolatey + + $_choco_version = $chocolatey::chocolatey_version ? { + undef => '0', + default => $chocolatey::chocolatey_version + } + +# lint:ignore:80chars + if versioncmp($_choco_version, '0.9.9.0') >= 0 and versioncmp($_choco_version, '0.9.10.0') < 0 { + $_choco_exe_path = "${chocolatey::choco_install_location}\\bin\\choco.exe" + + $_enable_autouninstaller = $chocolatey::enable_autouninstaller ? { + false => 'disable', + default => 'enable' + } + + exec { "chocolatey_autouninstaller_${_enable_autouninstaller}": + path => $::path, + command => "${_choco_exe_path} feature -r ${_enable_autouninstaller} -n autoUninstaller", + unless => "cmd.exe /c ${_choco_exe_path} feature list -r | findstr /B /I /C:\"autoUninstaller - [${_enable_autouninstaller}d]\"", + environment => ["ChocolateyInstall=${::chocolatey::choco_install_location}"] + } + } +# lint:endignore +} diff --git a/modules/utilities/windows/repository_managers/chocolatey/manifests/init.pp b/modules/utilities/windows/repository_managers/chocolatey/manifests/init.pp new file mode 100644 index 000000000..d2729dc12 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/manifests/init.pp @@ -0,0 +1,104 @@ +# chocolatey - Used for managing installation and configuration +# of Chocolatey itself. +# +# @author Rob Reynolds, Rich Siegel, and puppet-chocolatey contributors +# +# @example Default - This will by default ensure Chocolatey is installed and ready for use. +# include chocolatey +# +# @example Override default install location +# class {'chocolatey': +# choco_install_location => 'D:\secured\choco', +# } +# +# @example Use an internal Chocolatey.nupkg for installation +# class {'chocolatey': +# chocolatey_download_url => 'https://internalurl/to/chocolatey.nupkg', +# use_7zip => false, +# choco_install_timeout_seconds => 2700, +# } +# +# @example Use a file chocolatey.0.9.9.9.nupkg for installation +# class {'chocolatey': +# chocolatey_download_url => 'file:///c:/location/of/chocolatey.0.9.9.9.nupkg', +# use_7zip => false, +# choco_install_timeout_seconds => 2700, +# } +# +# @example Log chocolatey bootstrap installer script output +# class {'chocolatey': +# log_output => true, +# } +# +# @example Disable autouninstaller (use when less than 0.9.9.8) +# class {'chocolatey': +# enable_autouninstaller => false, +# } +# +# @param [String] choco_install_location Where Chocolatey install should be +# located. This needs to be an absolute path starting with a drive letter +# e.g. `c:\`. Defaults to the currently detected install location based on +# the `ChocolateyInstall` environment variable, falls back to +# `'C:\ProgramData\chocolatey'`. +# @param [Boolean] use_7zip Whether to use built-in shell or allow installer +# to download 7zip to extract `chocolatey.nupkg` during installation. +# Defaults to `false`. +# @param [Integer] choco_install_timeout_seconds How long in seconds should +# be allowed for the install of Chocolatey (including .NET Framework 4 if +# necessary). Defaults to `1500` (25 minutes). +# @param [String] chocolatey_download_url A url that will return +# `chocolatey.nupkg`. This must be a url, but not necessarily an OData feed. +# Any old url location will work. Defaults to +# `'https://chocolatey.org/api/v2/package/chocolatey/'`. +# @param [Boolean] enable_autouninstaller [Deprecated] - Should auto +# uninstaller be turned on? Auto uninstaller is what allows Chocolatey to +# automatically manage the uninstall of software from Programs and Features +# without necessarily requiring a `chocolateyUninstall.ps1` file in the +# package. Defaults to `true`. Setting is ignored in Chocolatey v0.9.10+. +# @param [Boolean] log_output Log output from the installer. Defaults to +# `false`. +# @param [String] chocolatey_version chocolatey version, falls back to +# `$::chocolateyversion`. +class chocolatey ( + $choco_install_location = $::chocolatey::params::install_location, + $use_7zip = $::chocolatey::params::use_7zip, + $choco_install_timeout_seconds = $::chocolatey::params::install_timeout_seconds, + $chocolatey_download_url = $::chocolatey::params::download_url, + $enable_autouninstaller = $::chocolatey::params::enable_autouninstaller, + $log_output = false, + $chocolatey_version = $::chocolatey::params::chocolatey_version +) inherits ::chocolatey::params { + + +validate_string($choco_install_location) +# lint:ignore:140chars +validate_re($choco_install_location, '^\w\:', +"Please use a full path for choco_install_location starting with a local drive. Reference choco_install_location => '${choco_install_location}'." +) +# lint:endignore + + validate_bool($use_7zip) + validate_integer($choco_install_timeout_seconds) + + validate_string($chocolatey_download_url) +# lint:ignore:140chars + validate_re($chocolatey_download_url,['^http\:\/\/','^https\:\/\/','file\:\/\/\/'], + "For chocolatey_download_url, if not using the default '${::chocolatey::params::download_url}', please use a Http/Https/File Url that downloads 'chocolatey.nupkg'." + ) +# lint:endignore + + validate_bool($enable_autouninstaller) + + if ((versioncmp($::clientversion, '3.4.0') >= 0) and (!defined('$::serverversion') or versioncmp($::serverversion, '3.4.0') >= 0)) { + class { '::chocolatey::install': } -> + class { '::chocolatey::config': } + + contain '::chocolatey::install' + contain '::chocolatey::config' + } else { + anchor {'before_chocolatey':} -> + class { '::chocolatey::install': } -> + class { '::chocolatey::config': } -> + anchor {'after_chocolatey':} + } +} diff --git a/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp b/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp new file mode 100644 index 000000000..4a7fb9c76 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp @@ -0,0 +1,27 @@ +# chocolatey::install - Private class used for install of Chocolatey +class chocolatey::install { + assert_private() + + $download_url = $::chocolatey::chocolatey_download_url + $unzip_type = $::chocolatey::use_7zip ? { + true => '7zip', + default => 'windows' + } + + registry_value { 'ChocolateyInstall environment value': + ensure => present, + path => 'HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment\ChocolateyInstall', + type => 'string', + data => $chocolatey::choco_install_location, + } + + exec { 'install_chocolatey_official': + command => template('chocolatey/InstallChocolatey.ps1.erb'), + creates => "${::chocolatey::choco_install_location}\\bin\\choco.exe", + provider => powershell, + timeout => $::chocolatey::choco_install_timeout_seconds, + logoutput => $::chocolatey::log_output, + environment => ["ChocolateyInstall=${::chocolatey::choco_install_location}"], + require => Registry_value['ChocolateyInstall environment value'], + } +} diff --git a/modules/utilities/windows/repository_managers/chocolatey/manifests/params.pp b/modules/utilities/windows/repository_managers/chocolatey/manifests/params.pp new file mode 100644 index 000000000..5d6f91be8 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/manifests/params.pp @@ -0,0 +1,9 @@ +# chocolatey::params - Default parameters +class chocolatey::params { + $install_location = $::choco_install_path # default is C:\ProgramData\chocolatey + $download_url = 'https://chocolatey.org/api/v2/package/chocolatey/' + $use_7zip = false + $install_timeout_seconds = 1500 + $enable_autouninstaller = true + $chocolatey_version = $::chocolateyversion +} diff --git a/modules/utilities/windows/repository_managers/chocolatey/metadata.json b/modules/utilities/windows/repository_managers/chocolatey/metadata.json new file mode 100644 index 000000000..5701a6278 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/metadata.json @@ -0,0 +1,53 @@ +{ + "name": "puppetlabs-chocolatey", + "version": "2.0.1", + "author": "Puppet Inc", + "summary": "Chocolatey package provider for Puppet", + "license": "Apache-2.0", + "source": "https://github.com/puppetlabs/puppetlabs-chocolatey", + "project_page": "https://github.com/puppetlabs/puppetlabs-chocolatey", + "issues_url": "https://tickets.puppet.com/browse/MODULES", + "dependencies": [ + {"name":"puppetlabs/stdlib","version_requirement":">= 4.6.0 < 5.0.0"}, + {"name":"puppetlabs/powershell","version_requirement":">= 1.0.1 < 3.0.0"}, + {"name":"puppetlabs/registry","version_requirement":">= 1.0.0 < 3.0.0"} + ], + "data_provider": null, + "description": "Chocolatey package provider for Puppet", + "tags": [ + "microsoft", + "powershell", + ".NET Framework", + ".Net", + "dot_net", + "chocolatey", + "package", + "package manager", + "chocolatey for business", + "chocolatey professional" + ], + "requirements": [ + { + "name": "pe", + "version_requirement": ">= 3.0.0 < 2016.4.0" + }, + { + "name": "puppet", + "version_requirement": ">= 3.0.0 < 5.0.0" + } + ], + "operatingsystem_support": [ + { + "operatingsystem": "Windows", + "operatingsystemrelease": [ + "Server 2008", + "Server 2008 R2", + "Server 2012", + "Server 2012 R2", + "7", + "8.1", + "10" + ] + } + ] +} diff --git a/modules/utilities/windows/repository_managers/chocolatey/secgen_metadata.xml b/modules/utilities/windows/repository_managers/chocolatey/secgen_metadata.xml new file mode 100644 index 000000000..7484a39c0 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + Chocolatey install + Jason Keighley + Apache v2 + A Chocolatey installation + + repository_managers + windows + + + + + \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/classes/config_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/config_spec.rb new file mode 100644 index 000000000..e85f69b07 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/config_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +RSpec.describe 'chocolatey' do + context 'contains config.pp' do + context 'with older choco installed' do + let(:facts) { + { + :chocolateyversion => '0.9.8.33', + :choco_install_path => 'C:\ProgramData\chocolatey', + } + } + + [true, false].each do |param_value| + feature_enable = 'enable' + feature_enable = 'disable' if !param_value + + context "enable_autouninstaller => #{param_value}" do + let(:params) {{ :enable_autouninstaller => param_value }} + + it { is_expected.not_to contain_exec("chocolatey_autouninstaller_#{feature_enable}") } + + it { + is_expected.not_to contain_exec("chocolatey_autouninstaller_#{feature_enable}").with_command("C:\\ProgramData\\chocolatey\\bin\\choco.exe feature -r #{feature_enable} -n autoUninstaller") + } + end + end + end + + context 'without choco installed' do + let(:facts) { + { + :chocolateyversion => '0', + :choco_install_path => 'C:\ProgramData\chocolatey', + } + } + + [true, false].each do |param_value| + feature_enable = 'enable' + feature_enable = 'disable' if !param_value + + context "enable_autouninstaller => #{param_value}" do + let(:params) {{ :enable_autouninstaller => param_value }} + + it { is_expected.not_to contain_exec("chocolatey_autouninstaller_#{feature_enable}") } + + it { + is_expected.not_to contain_exec("chocolatey_autouninstaller_#{feature_enable}").with_command("C:\\ProgramData\\chocolatey\\bin\\choco.exe feature -r #{feature_enable} -n autoUninstaller") + } + end + end + end + + context 'with choco.exe installed' do + let(:facts) { + { + :chocolateyversion => '0.9.9.8', + :choco_install_path => 'C:\ProgramData\chocolatey', + } + } + + [true, false].each do |param_value| + feature_enable = 'enable' + feature_enable = 'disable' if !param_value + + context "enable_autouninstaller => #{param_value}" do + let(:params) {{ :enable_autouninstaller => param_value }} + + it { is_expected.to contain_exec("chocolatey_autouninstaller_#{feature_enable}") } + + it { + is_expected.to contain_exec("chocolatey_autouninstaller_#{feature_enable}").with_command("C:\\ProgramData\\chocolatey\\bin\\choco.exe feature -r #{feature_enable} -n autoUninstaller") + } + end + end + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/classes/coverage_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/coverage_spec.rb new file mode 100644 index 000000000..12513b83c --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/coverage_spec.rb @@ -0,0 +1 @@ +at_exit { RSpec::Puppet::Coverage.report! } diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/classes/init_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/init_spec.rb new file mode 100644 index 000000000..180903928 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/init_spec.rb @@ -0,0 +1,200 @@ +require 'spec_helper' + +describe 'chocolatey' do + let(:facts) { + { + :chocolateyversion => '0.9.9.8', + :choco_install_path => 'C:\ProgramData\chocolatey', + } + } + + [{}].each do |params| + context "#{params}" do + let(:params) { params } + + it 'should compile successfully' do + catalogue + end + + #it { is_expected.to compile } + #it { is_expected.to compile.with_all_deps } + it { is_expected.to contain_class('chocolatey') } + it { is_expected.to contain_class('chocolatey::params') } + it { is_expected.to contain_class('chocolatey::install') } + it { is_expected.to contain_class('chocolatey::config') } + end + end + + context "chocolatey_download_url =>" do + ['https://chocolatey.org/api/v2/package/chocolatey/','http://location','file:///c:/somwhere/chocolatey.nupkg'].each do |param_value| + context "#{param_value}" do + let (:params) {{ + :chocolatey_download_url => param_value + }} + + it 'should compile successfully' do + catalogue + end + end + end + + if Puppet.version < '4.0.0' + invalid_url_values = ['\\\\ciflocation\\share','bob',"4",'',3] + not_a_string_values = [false] + else + invalid_url_values = ['\\\\ciflocation\\share','bob',"4",''] + not_a_string_values = [false, 3] + end + + invalid_url_values.each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :chocolatey_download_url => param_value + }} + + let(:error_message) { /use a Http\/Https\/File Url that downloads/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + + not_a_string_values.each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :chocolatey_download_url => param_value + }} + + let(:error_message) { /is not a string/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + end + + context "choco_install_location =>" do + ['C:\\ProgramData\\chocolatey','D:\\somewhere'].each do |param_value| + context "#{param_value}" do + let (:params) {{ + :choco_install_location => param_value + }} + + it 'should compile successfully' do + catalogue + end + end + end + + if Puppet.version < '4.0.0' + [false].each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :choco_install_location => param_value + }} + + let(:error_message) { /is not a string/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + + #1 is actually a string before v4. + [1,'https://somewhere','\\\\overhere',''].each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :choco_install_location => param_value + }} + + let(:error_message) { /Please use a full path for choco_install_location/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + else + [1,false].each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :choco_install_location => param_value + }} + + let(:error_message) { /is not a string/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + + ['https://somewhere','\\\\overhere',''].each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :choco_install_location => param_value + }} + + let(:error_message) { /Please use a full path for choco_install_location/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + end + end + + context "choco_install_timeout_seconds =>" do + [1500,8000,"1",'30'].each do |param_value| + context "#{param_value}" do + let (:params) {{ + :choco_install_timeout_seconds => param_value + }} + + it 'should compile successfully' do + catalogue + end + end + end + + ['string',false,''].each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + :choco_install_timeout_seconds => param_value + }} + + let(:error_message) { /Expected first argument to be an Integer/ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + end + + ['use_7zip','enable_autouninstaller'].each do |boolean_param| + context "#{boolean_param} =>" do + [true, false].each do |param_value| + context "#{param_value}" do + let (:params) {{ + boolean_param.to_sym => param_value + }} + + it 'should compile successfully' do + catalogue + end + end + end + + ['true','false','bob',3,"4",''].each do |param_value| + context "#{param_value} (invalid scenario)" do + let (:params) {{ + boolean_param.to_sym => param_value + }} + + let(:error_message) { /is not a boolean./ } + it { + expect { catalogue }.to raise_error(Puppet::Error, error_message) + } + end + end + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/classes/install_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/install_spec.rb new file mode 100644 index 000000000..b1a14e6ef --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/classes/install_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +RSpec.describe 'chocolatey' do + + let(:facts) { + { + :chocolateyversion => '0.9.9.8', + :choco_install_path => 'C:\ProgramData\chocolatey', + } + } + + context 'contains install.pp' do + ['c:\local_folder', "C:\\ProgramData\\chocolatey"].each do |param_value| + context "choco_install_location => #{param_value}" do + let(:params) {{ :choco_install_location => param_value }} + + it { is_expected.to contain_exec('install_chocolatey_official').with_creates("#{param_value}\\bin\\choco.exe") } + end + end + + + [1500, 35].each do |param_value| + context "choco_install_timeout_seconds => #{param_value}" do + let(:params) {{ :choco_install_timeout_seconds => param_value }} + + it { is_expected.to contain_exec('install_chocolatey_official').with_timeout("#{param_value}") } + end + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/spec_helper.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/spec_helper.rb new file mode 100644 index 000000000..72c407699 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/spec_helper.rb @@ -0,0 +1,64 @@ +#require 'ruby-prof' +#RubyProf.start + +IDEAL_CONSOLE_WIDTH = 72 +def horizontal_rule(width = 5) + '=' * [width, IDEAL_CONSOLE_WIDTH].min +end + +require 'puppetlabs_spec_helper/module_spec_helper' + +# require dependencies +gems = [ + #'minitest/autorun', # http://docs.seattlerb.org/minitest/ + #'minitest/unit', # https://github.com/freerange/mocha#bundler + 'mocha', # http://gofreerange.com/mocha/docs/Mocha/Configuration.html + 'puppet', +] +begin + gems.each {|gem| require gem} +rescue => e + # http://goo.gl/r3nFG + # emphasize dependency failures in case a task spews lots of output + warn horizontal_rule(e.message.length) + warn e.class + warn e.message + warn horizontal_rule(e.message.length) + exit(1) +end + +RSpec.configure do |c| + # set the environment variable before files are loaded, otherwise it is too late + ENV['ChocolateyInstall'] = 'c:\blah' + + begin + Win32::Registry.any_instance.stubs(:[]).with('Bind') + Win32::Registry.any_instance.stubs(:[]).with('Domain') + Win32::Registry.any_instance.stubs(:[]).with('ChocolateyInstall').raises(Win32::Registry::Error.new(2), 'file not found yo') + rescue + # we don't care + end + + # https://www.relishapp.com/rspec/rspec-core/v/2-12/docs/mock-framework-integration/mock-with-mocha! + c.mock_framework = :mocha + # see output for all failures + c.fail_fast = false + c.expect_with :rspec do |e| + e.syntax = [:should, :expect] + end + c.raise_errors_for_deprecations! + + c.after :suite do + #result = RubyProf.stop + # Print a flat profile to text + #printer = RubyProf::FlatPrinter.new(result) + #printer.print(STDOUT) + end +end + +# We need this because the RAL uses 'should' as a method. This +# allows us the same behaviour but with a different method name. +class Object + alias :must :should + alias :must_not :should_not +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/choco_install_path_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/choco_install_path_spec.rb new file mode 100644 index 000000000..fb1869558 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/choco_install_path_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' +require 'facter' +require 'puppet_x/chocolatey/chocolatey_install' + +describe 'choco_install_path fact' do + subject(:fact) { Facter.fact(:choco_install_path) } + + before :each do + Facter.clear + Facter.clear_messages + end + + context "on Windows", :if => Puppet::Util::Platform.windows? do + it "should return the output of PuppetX::Chocolatey::ChocolateyInstall.install_path" do + expected_value = 'C:\somewhere' + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns(expected_value) + + subject.value.must == expected_value + end + + it "should return the default path when PuppetX::Chocolatey::ChocolateyInstall.install_path is nil" do + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns(nil) + + subject.value.must == 'C:\ProgramData\chocolatey' + end + end + + after :each do + Facter.clear + Facter.clear_messages + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/chocolateyversion_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/chocolateyversion_spec.rb new file mode 100644 index 000000000..c6f6f143a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/facter/chocolateyversion_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' +require 'facter' +require 'puppet_x/chocolatey/chocolatey_version' + +describe 'chocolateyversion fact' do + subject(:fact) { Facter.fact(:chocolateyversion) } + + before :each do + Facter.clear + Facter.clear_messages + end + + context "on Windows", :if => Puppet::Util::Platform.windows? do + it "should return the output of PuppetX::Chocolatey::ChocolateyVersion.version" do + expected_value = '1.2.3' + PuppetX::Chocolatey::ChocolateyVersion.expects(:version).returns(expected_value) + + subject.value.must == expected_value + end + + it "should return the default of 0 when PuppetX::Chocolatey::ChocolateyVersion.version is nil" do + PuppetX::Chocolatey::ChocolateyVersion.expects(:version).returns(nil) + + subject.value.must == '0' + end + end + + after :each do + Facter.clear + Facter.clear_messages + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyconfig/windows_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyconfig/windows_spec.rb new file mode 100644 index 000000000..696181351 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyconfig/windows_spec.rb @@ -0,0 +1,268 @@ +require 'spec_helper' +require 'stringio' +require 'puppet/type/chocolateyconfig' +require 'puppet/provider/chocolateyconfig/windows' +require 'rexml/document' + +provider = Puppet::Type.type(:chocolateyconfig).provider(:windows) +describe provider do + let (:name) { 'configItem' } + let (:resource) { Puppet::Type.type(:chocolateyconfig).new(:provider => :windows, :name => name, :value => "yes") } + let (:choco_config) { 'c:\choco.config' } + let (:choco_install_path) { 'c:\dude\bin\choco.exe' } + let (:choco_config_contents) { <<-'EOT' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EOT + } + + + let (:minimum_supported_version) {'0.9.10.0'} + let (:last_unsupported_version) {'0.9.9.12'} + + before :each do + PuppetX::Chocolatey::ChocolateyInstall.stubs(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.stubs(:choco_version).returns(minimum_supported_version) + + @provider = provider.new(resource) + resource.provider = @provider + + # Stub all file and config tests + provider.stubs(:healthcheck) + end + + context "verify provider" do + it "should be an instance of Puppet::Type::Chocolateyconfig::ProviderWindows" do + @provider.must be_an_instance_of Puppet::Type::Chocolateyconfig::ProviderWindows + end + + it "should have a create method" do + @provider.should respond_to(:create) + end + + it "should have an exists? method" do + @provider.should respond_to(:exists?) + end + + it "should have a destroy method" do + @provider.should respond_to(:destroy) + end + + it "should have a properties method" do + @provider.should respond_to(:properties) + end + + it "should have a query method" do + @provider.should respond_to(:query) + end + end + + context "properties" do + context ":value" do + #it "should default to nil" do + # resource[:value].should be_nil + #end + + it "should accept c:\\cache" do + resource[:value] = 'c:\cache' + end + + it "should accept 2700" do + resource[:value] = '2700' + end + + it "should accept 'value with spaces'" do + resource[:value] = 'value with spaces' + end + end + end + + context "self.get_configs" do + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:set_env_chocolateyinstall) + end + + it "should error when the config file location is null" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(nil) + + expect { + provider.get_configs + }.to raise_error(Puppet::ResourceError, /Config file not found for Chocolatey/) + end + + it "should error when the config file is not found" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(false) + + expect { + provider.get_configs + }.to raise_error(Puppet::ResourceError, /was unable to locate config file at/) + end + + context "when getting configs from the config file" do + configs = [] + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(true) + File.expects(:new).with(choco_config,"r").returns choco_config_contents + + configs = provider.get_configs + end + + it "should match the count of configs in the config" do + configs.count.must eq 11 + + end + + it "should contain xml elements" do + configs[0].must be_an_instance_of REXML::Element + end + end + end + + context "self.get_config" do + let (:element) { REXML::Element.new('add') } + element_key = "cacheLocation" + element_value= "c:\\cache" + element_description = "Cache location if not TEMP folder." + + before :each do + element.add_attributes( { "key" => element_key, + "value" => element_value, + "description" => element_description, + } ) + end + + it "should return nil config when element is nil" do + provider.get_config(nil).must be == {} + end + + it "should convert an element to a config" do + config = provider.get_config(element) + + config[:name].must eq element_key + config[:value].must eq element_value + config[:description].must eq element_description + config[:ensure].must eq :present + end + end + + context ".validation" do + it "should not error when Chocolatey is not installed" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).returns(false).at_least(0) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_install_path).returns(false).at_least(0) + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns('') + + resource.provider.validate + end + + it "should not error when the minimum version is met" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).returns(false).at_least(0) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_install_path).returns(true).at_least(0) + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + + resource.provider.validate + end + + it "should error when the minimum version is not met" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).returns(true).at_least(0) + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(last_unsupported_version) + + expect { + resource.provider.validate + }.to raise_error(Puppet::ResourceError, /Chocolatey version must be '0.9.10.0' to manage configuration values. Detected '#{last_unsupported_version}'/) + end + end + + context ".flush" do + resource_name = "yup" + resource_value = "this" + resource_ensure = :present + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:set_env_chocolateyinstall).at_most_once + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config).at_most_once + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(true).at_most_once + File.expects(:new).with(choco_config,"r").returns(choco_config_contents).at_most_once + + resource[:name] = resource_name + resource[:value] = resource_value + resource[:ensure] = resource_ensure + end + + it "should ensure a config setting is set" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'config', 'set', + '--name', resource_name, + '--value', resource_value + ]) + + resource.flush + end + + it "should ensure a config setting is removed" do + resource.provider.destroy + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'config', 'unset', + '--name', resource_name + ]) + + resource.flush + end + + it "should provide an error message when choco execution fails" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'config', 'set', + '--name', resource_name, + '--value', resource_value + ]).raises(Puppet::ExecutionFailure, "Nooooo") + + expect { resource.flush }.to raise_error(Puppet::Error, /Unable to set Chocolateyconfig/) + end + + + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyfeature/windows_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyfeature/windows_spec.rb new file mode 100644 index 000000000..c5ac9dd67 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateyfeature/windows_spec.rb @@ -0,0 +1,170 @@ +require 'spec_helper' +require 'stringio' +require 'puppet/type/chocolateyfeature' +require 'puppet/provider/chocolateyfeature/windows' +require 'rexml/document' + +provider = Puppet::Type.type(:chocolateyfeature).provider(:windows) +describe provider do + let (:name) { 'allowglobalconfirmation' } + let (:resource) { Puppet::Type.type(:chocolateyfeature).new(:provider => :windows, :name => name, :ensure => 'enabled' ) } + let (:choco_config) { 'c:\choco.config' } + let (:choco_install_path) { 'c:\dude\bin\choco.exe' } + let (:choco_config_contents) { <<-'EOT' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EOT + } + + let (:minimum_supported_version) {'0.9.9.0'} + + before :each do + PuppetX::Chocolatey::ChocolateyInstall.stubs(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.stubs(:choco_version).returns(minimum_supported_version) + + @provider = provider.new(resource) + resource.provider = @provider + + # Stub all file and config tests + provider.stubs(:healthcheck) + end + + context "verify provider" do + it "should be an instance of Puppet::Type::Chocolateyfeature::ProviderWindows" do + + @provider.must be_an_instance_of Puppet::Type::Chocolateyfeature::ProviderWindows + end + + it "should have a enable method" do + @provider.should respond_to(:enable) + end + + it "should have an exists? method" do + @provider.should respond_to(:exists?) + end + + it "should have a disable method" do + @provider.should respond_to(:disable) + end + + it "should have a properties method" do + @provider.should respond_to(:properties) + end + + it "should have a query method" do + @provider.should respond_to(:query) + end + end + + context "self.get_features" do + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:set_env_chocolateyinstall) + end + + it "should error when the config file location is null" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(nil) + + expect { + provider.get_features + }.to raise_error(Puppet::ResourceError, /Config file not found for Chocolatey/) + end + + it "should error when the config file is not found" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(false) + + expect { + provider.get_features + }.to raise_error(Puppet::ResourceError, /was unable to locate config file at/) + end + + context "when getting features from the config file" do + features = [] + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(true) + File.expects(:new).with(choco_config,"r").returns choco_config_contents + + features = provider.get_features + end + + it "should match the count of features in the config" do + features.count.must eq 14 + + end + + it "should contain xml elements" do + features[0].must be_an_instance_of REXML::Element + end + end + end + + context "self.get_feature" do + let (:element) { REXML::Element.new('feature') } + element_name = "default" + element_enabled = 'true' + + before :each do + element.add_attributes( { "name" => element_name, "enabled" => element_enabled, } ) + end + + it "should return nil feature when element is nil" do + provider.get_feature(nil).must be == {} + end + + it "should convert an element to a feature" do + feature = provider.get_feature(element) + + feature[:name].must eq element_name + feature[:ensure].must eq :enabled + end + + it "when feature is disabled" do + element.delete_attribute('enabled') + element.add_attribute('enabled', 'false') + + feature = provider.get_feature(element) + feature[:ensure].must eq :disabled + end + end + +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateysource/windows_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateysource/windows_spec.rb new file mode 100644 index 000000000..d27d3290d --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/chocolateysource/windows_spec.rb @@ -0,0 +1,607 @@ +require 'spec_helper' +require 'stringio' +require 'puppet/type/chocolateysource' +require 'puppet/provider/chocolateysource/windows' +require 'rexml/document' + +provider = Puppet::Type.type(:chocolateysource).provider(:windows) +describe provider do + let (:name) { 'sourcename' } + let (:location) { 'c:\packages' } + let (:resource) { Puppet::Type.type(:chocolateysource).new(:provider => :windows, :name => name, :location => location) } + let (:choco_config) { 'c:\choco.config' } + let (:choco_install_path) { 'c:\dude\bin\choco.exe' } + let (:choco_config_contents) { <<-'EOT' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + EOT + } + + let (:newer_choco_version) {'0.9.10.0'} + let (:minimum_supported_version_priority) {'0.9.9.9'} + let (:last_unsupported_version_priority) {'0.9.9.8'} + let (:minimum_supported_version) {'0.9.9.0'} + let (:last_unsupported_version) {'0.9.8.33'} + + before :each do + PuppetX::Chocolatey::ChocolateyInstall.stubs(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.stubs(:choco_version).returns(minimum_supported_version) + + @provider = provider.new(resource) + resource.provider = @provider + + # Stub all file and config tests + provider.stubs(:healthcheck) + end + + context "verify provider" do + it "should be an instance of Puppet::Type::Chocolateysource::ProviderWindows" do + + @provider.must be_an_instance_of Puppet::Type::Chocolateysource::ProviderWindows + end + + it "should have a create method" do + @provider.should respond_to(:create) + end + + it "should have an exists? method" do + @provider.should respond_to(:exists?) + end + + it "should have a disable method" do + @provider.should respond_to(:disable) + end + + it "should have a destroy method" do + @provider.should respond_to(:destroy) + end + + it "should have a properties method" do + @provider.should respond_to(:properties) + end + + it "should have a query method" do + @provider.should respond_to(:query) + end + end + + context "properties" do + + context ":location" do + it "should accept c:\\packages" do + resource[:location] = 'c:\packages' + end + + it "should accept http://somelocation/packages" do + resource[:location] = 'http://somelocation/packages' + end + + it "should accept \\\\unc\\share\\packages" do + resource[:location] = '\\unc\share\packages' + end + end + + context ":user" do + it "should accept 'bob'" do + resource[:user] = 'bob' + end + + it "should accept 'domain\\bob'" do + resource[:user] = 'domain\bob' + end + + it "should accept api keys like 'api123-456-243 d123'" do + resource[:user] = 'api123-456-243 d123' + end + end + + context ":password" do + it "should accept 'bob'" do + resource[:password] = 'bob' + end + + it "should accept api keys like 'api123-456-243 d123'" do + resource[:password] = 'api123-456-243 d123' + end + end + end + + context "self.get_sources" do + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:set_env_chocolateyinstall) + end + + it "should error when the config file location is null" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(nil) + + expect { + provider.get_sources + }.to raise_error(Puppet::ResourceError, /Config file not found for Chocolatey/) + end + + it "should error when the config file is not found" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(false) + + expect { + provider.get_sources + }.to raise_error(Puppet::ResourceError, /was unable to locate config file at/) + end + + context "when getting sources from the config file" do + sources = [] + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(true) + File.expects(:new).with(choco_config,"r").returns choco_config_contents + + sources = provider.get_sources + end + + it "should match the count of sources in the config" do + sources.count.must eq 3 + + end + + it "should contain xml elements" do + sources[0].must be_an_instance_of REXML::Element + end + end + end + + context "self.get_source" do + let (:element) { REXML::Element.new('source') } + element_id = "default" + element_value= "c:\\packages" + element_disabled = "false" + element_priority = "10" + element_user = "thisguy" + element_password = "super/encrypted+value==" + + + before :each do + element.add_attributes( { "id" => element_id, + "value" => element_value, + "disabled" => element_disabled, + "priority" => element_priority, + "user" => element_user, + "password" => element_password + } ) + end + + it "should return nil source when element it nil" do + provider.get_source(nil).must be == {} + end + + it "should convert an element to a source" do + source = provider.get_source(element) + + source[:name].must eq element_id + source[:location].must eq element_value + source[:priority].must eq element_priority + source[:user].must eq element_user + source[:ensure].must eq :present + end + + it "should convert a bare bones element to a source" do + element.delete_attribute('disabled') + element.delete_attribute('priority') + element.delete_attribute('user') + element.delete_attribute('password') + + source = provider.get_source(element) + + source[:name].must eq element_id + source[:location].must eq element_value + source[:ensure].must eq :present + end + + it "when source is disabled" do + element.delete_attribute('disabled') + element.add_attribute('disabled', 'true') + + source = provider.get_source(element) + source[:ensure].must eq :disabled + end + end + + context ".validation" do + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).returns(true).at_least(0) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_install_path).returns(true).at_least(0) + end + + it "should not warn when both user/password are empty" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + Puppet.expects(:warning).never + Puppet.expects(:debug).never + + resource.provider.validate + end + + it "should throw when choco version is less than the minimum supported version" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(last_unsupported_version) + + expect { + resource.provider.validate + }.to raise_error(Puppet::Error, /Chocolatey version must be '0.9.9.0' to manage configuration values with Puppet/) + end + + it "should write a debug message on password when password is not empty" do + resource[:user] = 'tim' + resource[:password] = 'tim' + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet.expects(:warning).never + Puppet.expects(:debug).with("The password is not ensurable, so Puppet is unable to change the value using chocolateysource resource. As a workaround, a password change can be in the form of an exec. Reference Chocolateysource[#{name}]") + + resource.provider.validate + end + + it "should not warn on user/password on newer choco versions" do + resource[:user] = 'tim' + resource[:password] = 'tim' + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should not warn on user/password when choco version is the minimum supported version" do + resource[:user] = 'tim' + resource[:password] = 'tim' + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should not warn if priority is not set" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should not warn if priority is not set on older unsupported versions" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(last_unsupported_version_priority) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should not warn if priority is 0 on unsupported versions" do + resource[:priority] = 0 + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(last_unsupported_version_priority) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should not warn on priority when choco version is newer than the minimum supported version" do + resource[:priority] = 10 + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should not warn on priority when choco version is the minimum supported version" do + resource[:priority] = 10 + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version_priority) + Puppet.expects(:warning).never + + resource.provider.validate + end + + it "should warn on priority when choco version is less than the minimum supported version" do + resource[:priority] = 10 + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(last_unsupported_version_priority) + Puppet.expects(:warning).with("Chocolatey is unable to manage priority for sources when version is less than 0.9.9.9. The value you set will be ignored.") + + resource.provider.validate + end + + it "should pass when ensure is not present and location is empty" do + no_location_resource = Puppet::Type.type(:chocolateysource).new(:name => 'source', :ensure => :disabled ) + no_location_resource.provider = provider.new(no_location_resource) + + no_location_resource.provider.validate + end + + it "should fail when ensure => present and location is empty" do + expect { + no_location_resource = Puppet::Type.type(:chocolateysource).new(:name => 'source') + no_location_resource.provider = provider.new(no_location_resource) + + no_location_resource.provider.validate + }.to raise_error(Exception, /non-empty location/) + # check for just an exception here + # In some versions of Puppet, this comes back as ArgumentError + # In other versions of Puppet, this comes back as Puppet::Error + end + end + + context ".flush" do + resource_name = "yup" + resource_location = "loc" + resource_ensure = :present + resource_priority = 10 + resource_user = "thatguy" + resource_password = "secrets!" + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.expects(:set_env_chocolateyinstall).at_most_once + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_config_file).returns(choco_config).at_most_once + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(choco_config).returns(true).at_most_once + File.expects(:new).with(choco_config,"r").returns(choco_config_contents).at_most_once + + resource[:name] = resource_name + resource[:location] = resource_location + resource[:ensure] = resource_ensure + end + + it "should ensure a source is present with minimal values set" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--priority', 0, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', 'yup' + ]) + + resource.flush + end + + it "should ensure a source is present with all values set" do + resource[:priority] = resource_priority + resource[:user] = resource_user + resource[:password] = resource_password + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--user', resource_user, + '--password', resource_password, + '--priority', resource_priority, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name + ]) + + resource.flush + end + + it "should set priority when present" do + resource[:priority] = resource_priority + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--priority', resource_priority, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name + ]) + + resource.flush + end + + it "should set user and password when user is present" do + resource[:user] = resource_user + resource[:password] = resource_password + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--user', resource_user, + '--password', resource_password, + '--priority', 0, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name + ]) + + resource.flush + end + + it "should set user and password when choco version is newer than the minimum supported version" do + resource[:user] = resource_user + resource[:password] = resource_password + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--user', resource_user, + '--password', resource_password, + '--priority', 0, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name + ]) + + resource.flush + end + + it "should set user and password when choco version is the minimum supported version" do + resource[:user] = resource_user + resource[:password] = resource_password + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--user', resource_user, + '--password', resource_password, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name + ]) + + resource.flush + end + + it "should set priority when choco version is newer than the minimum supported version" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--priority', 0, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name, + ]) + + resource.flush + end + + it "should set priority when choco version is the minimum supported version" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_version_priority) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--priority', 0, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name, + ]) + + resource.flush + end + + it "should not set priority when choco version is less than the minimum supported version" do + resource[:priority] = resource_priority + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(last_unsupported_version_priority) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', resource_name, + ]) + + resource.flush + end + + it "should disable a source when ensure => disabled" do + resource[:ensure] = :disabled + resource[:name] = 'chocolatey' + resource.provider.disable + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'disable', + '--name', 'chocolatey' + ]) + + resource.flush + end + + it "should remove a source when ensure => absent" do + resource[:ensure] = :absent + resource.provider.destroy + + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).never + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'remove', + '--name', resource_name, + ]) + + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'enable', + '--name', 'yup' + ]).never + + resource.flush + end + + it "should provide an error message when choco execution fails" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(newer_choco_version) + Puppet::Util::Execution.expects(:execute).with([provider.command(:chocolatey), + 'source', 'add', + '--name', resource_name, + '--source', resource_location, + '--priority', 0, + ]).raises(Puppet::ExecutionFailure, "Nooooo") + + expect { resource.flush }.to raise_error(Puppet::Error, /Unable to set Chocolatey source/) + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/package/chocolatey_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/package/chocolatey_spec.rb new file mode 100644 index 000000000..e6fa57be1 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/provider/package/chocolatey_spec.rb @@ -0,0 +1,514 @@ +require 'spec_helper' +require 'stringio' +require 'puppet/type/package' +require 'puppet/provider/package/chocolatey' + +provider = Puppet::Type.type(:package).provider(:chocolatey) + +describe provider do + let (:resource) { Puppet::Type.type(:package).new(:provider => :chocolatey, :name => "chocolatey") } + let (:first_compiled_choco_version) {'0.9.9.0'} + let (:newer_choco_version) {'0.9.10.0'} + let (:last_posh_choco_version) {'0.9.8.33'} + let (:minimum_supported_choco_uninstall_source) {'0.9.10.0'} + let (:minimum_supported_choco_exit_codes) {'0.9.10.0'} + let (:choco_zero_ten_zero) {'0.10.0'} + + before :each do + @provider = provider.new(resource) + resource.provider = @provider + + # Stub all file and config tests + provider.stubs(:healthcheck) + Puppet::Util::Execution.stubs(:execute) + end + + it "should be an instance of Puppet::Type::Package::ProviderChocolatey" do + @provider.must be_an_instance_of Puppet::Type::Package::ProviderChocolatey + end + + it "should have an install method" do + @provider.should respond_to(:install) + end + + it "should have a latest method" do + @provider.should respond_to(:uninstall) + end + + it "should have an update method" do + @provider.should respond_to(:update) + end + + it "should have a latest method" do + @provider.should respond_to(:latest) + end + + context "parameter :source" do + it "should default to nil" do + resource[:source].should be_nil + end + + it "should accept c:\\packages" do + resource[:source] = 'c:\packages' + end + + it "should accept http://somelocation/packages" do + resource[:source] = 'http://somelocation/packages' + end + + it "should accept \\\\unc\\share\\packages" do + resource[:source] = '\\unc\share\packages' + end + end + + context "when installing" do + context "with compiled choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(true) + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.stubs(:file_exists?).with('c:\dude\bin\choco.exe').returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(first_compiled_choco_version) + # unhold is called in installs on compiled choco + Puppet::Util::Execution.stubs(:execute) + end + + it "should use install command without versioned package" do + resource[:ensure] = :present + @provider.expects(:chocolatey).with('install', 'chocolatey','-y', nil) + + @provider.install + end + + it "should call with ignore package exit codes when = 0.9.10" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_choco_exit_codes).at_least_once + resource[:ensure] = :present + @provider.expects(:chocolatey).with('install', 'chocolatey','-y', nil, '--ignore-package-exit-codes') + + @provider.install + end + + it "should call with ignore package exit codes when > 0.9.10" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(choco_zero_ten_zero).at_least_once + resource[:ensure] = :present + @provider.expects(:chocolatey).with('install', 'chocolatey','-y', nil, '--ignore-package-exit-codes') + + @provider.install + end + + it "should use upgrade command with versioned package" do + resource[:ensure] = '1.2.3' + @provider.expects(:chocolatey).with('upgrade', 'chocolatey', '-version', '1.2.3', '-y', nil) + + @provider.install + end + + it "should call install instead of upgrade if package name ends with .config" do + resource[:name] = "packages.config" + resource[:ensure] = :present + @provider.expects(:chocolatey).with('install', 'packages.config','-y', nil) + + @provider.install + end + + it "should use source if it is specified" do + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('install','chocolatey','-y', '-source', 'c:\packages', nil) + + @provider.install + end + end + + context "with posh choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(false) + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.stubs(:file_exists?).with('c:\dude\bin\choco.exe').returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(last_posh_choco_version) + end + + it "should use install command without versioned package" do + resource[:ensure] = :present + @provider.expects(:chocolatey).with('install', 'chocolatey', nil) + + @provider.install + end + + it "should use update command with versioned package" do + resource[:ensure] = '1.2.3' + @provider.expects(:chocolatey).with('update', 'chocolatey', '-version', '1.2.3', nil) + + @provider.install + end + + it "should use source if it is specified" do + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('install','chocolatey', '-source', 'c:\packages', nil) + + @provider.install + end + end + end + + context "when holding" do + context "with compiled choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(true) + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.stubs(:file_exists?).with('c:\dude\bin\choco.exe').returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(first_compiled_choco_version) + # unhold is called in installs on compiled choco + Puppet::Util::Execution.stubs(:execute) + end + + it "should use install command with held package" do + resource[:ensure] = :held + @provider.expects(:chocolatey).with('install', 'chocolatey','-y', nil) + @provider.expects(:chocolatey).with('pin', 'add', '-n', 'chocolatey') + + @provider.hold + end + end + + context "with posh choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(false) + end + + it "should throw an argument error with held package" do + resource[:ensure] = :held + + expect { @provider.hold }.to raise_error(ArgumentError, "Only choco v0.9.9+ can use ensure => held") + end + end + end + + context "when uninstalling" do + context "with compiled choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(first_compiled_choco_version) + # unhold is called in installs on compiled choco + Puppet::Util::Execution.stubs(:execute) + end + + it "should call the remove operation" do + @provider.expects(:chocolatey).with('uninstall', 'chocolatey','-fy', nil) + + @provider.uninstall + end + + it "should call with ignore package exit codes when = 0.9.10" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_choco_exit_codes).at_least_once + @provider.expects(:chocolatey).with('uninstall', 'chocolatey','-fy', nil, '--ignore-package-exit-codes') + + @provider.uninstall + end + + it "should call with ignore package exit codes when > 0.9.10" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(choco_zero_ten_zero).at_least_once + @provider.expects(:chocolatey).with('uninstall', 'chocolatey','-fy', nil, '--ignore-package-exit-codes') + + @provider.uninstall + end + + it "should use ignore source if it is specified and the version is less than 0.9.10" do + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('uninstall','chocolatey','-fy', nil) + + @provider.uninstall + end + + it "should use source if it is specified and the version is at least 0.9.10" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_choco_uninstall_source).at_least_once + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('uninstall','chocolatey', '-fy', '-source', 'c:\packages', nil, '--ignore-package-exit-codes') + + @provider.uninstall + end + + it "should use source if it is specified and the version is greater than 0.9.10" do + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(choco_zero_ten_zero).at_least_once + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('uninstall','chocolatey', '-fy', '-source', 'c:\packages', nil, '--ignore-package-exit-codes') + + @provider.uninstall + end + end + + context "with posh choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(false) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(last_posh_choco_version) + end + + it "should call the remove operation" do + @provider.expects(:chocolatey).with('uninstall', 'chocolatey', nil) + + @provider.uninstall + end + + it "should use source if it is specified" do + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('uninstall','chocolatey', '-source', 'c:\packages', nil) + + @provider.uninstall + end + end + end + + context "when updating" do + context "with compiled choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(first_compiled_choco_version) + # unhold is called in installs on compiled choco + Puppet::Util::Execution.stubs(:execute) + end + + it "should use `chocolatey upgrade` when ensure latest and package present" do + provider.stubs(:instances).returns [provider.new({ + :ensure => "1.2.3", + :name => "chocolatey", + :provider => :chocolatey, + })] + @provider.expects(:chocolatey).with('upgrade', 'chocolatey', '-y', nil) + + @provider.update + end + + it "should call with ignore package exit codes when = 0.9.10" do + provider.stubs(:instances).returns [provider.new({ + :ensure => "1.2.3", + :name => "chocolatey", + :provider => :chocolatey, + })] + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(minimum_supported_choco_exit_codes).at_least_once + resource[:ensure] = :present + @provider.expects(:chocolatey).with('upgrade', 'chocolatey','-y', nil, '--ignore-package-exit-codes') + + @provider.update + end + + it "should call with ignore package exit codes when > 0.9.10" do + provider.stubs(:instances).returns [provider.new({ + :ensure => "1.2.3", + :name => "chocolatey", + :provider => :chocolatey, + })] + PuppetX::Chocolatey::ChocolateyCommon.expects(:choco_version).returns(choco_zero_ten_zero).at_least_once + resource[:ensure] = :present + @provider.expects(:chocolatey).with('upgrade', 'chocolatey','-y', nil, '--ignore-package-exit-codes') + + @provider.update + end + + + it "should use `chocolatey install` when ensure latest and package absent" do + provider.stubs(:instances).returns [] + @provider.expects(:chocolatey).with('install', 'chocolatey', '-y', nil) + + @provider.update + end + + it "should use source if it is specified" do + provider.expects(:instances).returns [provider.new({ + :ensure => "latest", + :name => "chocolatey", + :provider => :chocolatey, + })] + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('upgrade','chocolatey', '-y', '-source', 'c:\packages', nil) + + @provider.update + end + end + + context "with posh choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(false) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(last_posh_choco_version) + end + + it "should use `chocolatey update` when ensure latest and package present" do + provider.stubs(:instances).returns [provider.new({ + :ensure => "1.2.3", + :name => "chocolatey", + :provider => :chocolatey, + })] + @provider.expects(:chocolatey).with('update', 'chocolatey', nil) + + @provider.update + end + + it "should use `chocolatey install` when ensure latest and package absent" do + provider.stubs(:instances).returns [] + @provider.expects(:chocolatey).with('install', 'chocolatey', nil) + + @provider.update + end + + it "should use source if it is specified" do + provider.expects(:instances).returns [provider.new({ + :ensure => "latest", + :name => "chocolatey", + :provider => :chocolatey, + })] + resource[:source] = 'c:\packages' + @provider.expects(:chocolatey).with('update','chocolatey', '-source', 'c:\packages', nil) + + @provider.update + end + end + end + + context "when getting latest" do + context "with compiled choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(first_compiled_choco_version) + end + + it "should use choco.exe arguments" do + # we don't care where choco is, we are concerned with the arguments that are passed to choco. + # + @provider.send(:latestcmd).drop(1).should == ['upgrade', '--noop', 'chocolatey','-r'] + end + + it "should use source if it is specified" do + resource[:source] = 'c:\packages' + @provider.send(:latestcmd).drop(1).should == ['upgrade', '--noop', 'chocolatey','-r', '-source', 'c:\packages'] + #@provider.expects(:chocolatey).with('upgrade', '--noop', 'chocolatey','-r', '-source', 'c:\packages') + + #@provider.latest + end + end + + context "with posh choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(false) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(last_posh_choco_version) + end + + it "should use posh arguments" do + @provider.send(:latestcmd).drop(1).should == ['version', 'chocolatey', '| findstr /R "latest" | findstr /V "latestCompare"'] + end + + it "should use source if it is specified" do + resource[:source] = 'c:\packages' + @provider.send(:latestcmd).drop(1).should == ['version', 'chocolatey', '-source', 'c:\packages', '| findstr /R "latest" | findstr /V "latestCompare"'] + #@provider.expects(:chocolatey).with('version', 'chocolatey', '-source', 'c:\packages', '| findstr /R "latest" | findstr /V "latestCompare"') + + #@provider.latest + end + end + end + + context "query" do + it "should return a hash when chocolatey and the package are present" do + provider.expects(:instances).returns [provider.new({ + :ensure => "1.2.5", + :name => "chocolatey", + :provider => :chocolatey, + })] + + @provider.query.should == { + :ensure => "1.2.5", + :name => "chocolatey", + :provider => :chocolatey, + } + end + + it "should return nil when the package is missing" do + provider.expects(:instances).returns [] + + @provider.query.should == nil + end + end + + context "when fetching a package list" do + it "should invoke provider listcmd" do + provider.expects(:listcmd) + + provider.instances + end + + it "should query chocolatey" do + provider.expects(:execpipe).with() do |args| + args[1] =~ /list/ + args[2] =~ /-lo/ + end + + provider.instances + end + + context "self.instances" do + it "should return nil on error" do + provider.expects(:execpipe).raises(Puppet::ExecutionFailure.new("ERROR!")) + + provider.instances.should be_nil + end + + context "with compiled choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(true) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(first_compiled_choco_version) + end + + it "should return installed packages with their versions" do + provider.expects(:execpipe).yields(StringIO.new(%Q(package1|1.23\n\package2|2.00\n))) + + packages = (provider.instances) + + packages.length.should == 2 + + packages[0].properties.should == { + :provider => :chocolatey, + :ensure => "1.23", + :name => 'package1' + } + + packages[1].properties.should == { + :provider => :chocolatey, + :ensure => "2.00", + :name => 'package2' + } + end + + it "should return nil on error" do + provider.expects(:execpipe).yields(StringIO.new(%Q(Unable to search for packages when there are no soures enabled for packages and none were passed as arguments.\n))) + + expect { + provider.instances + }.to raise_error(Puppet::Error, /At least one source must be enabled./) + end + end + + context "with posh choco client" do + before :each do + @provider.class.stubs(:is_compiled_choco?).returns(false) + PuppetX::Chocolatey::ChocolateyVersion.stubs(:version).returns(last_posh_choco_version) + end + + it "should return installed packages with their versions" do + provider.expects(:execpipe).yields(StringIO.new(%Q(package1 1.23\n\package2 2.00\n))) + + packages = (provider.instances) + + packages.length.should == 2 + + packages[0].properties.should == { + :provider => :chocolatey, + :ensure => "1.23", + :name => 'package1' + } + + packages[1].properties.should == { + :provider => :chocolatey, + :ensure => "2.00", + :name => 'package2' + } + end + end + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyconfig_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyconfig_spec.rb new file mode 100644 index 000000000..241d891c5 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyconfig_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' +require 'puppet/type/chocolateyconfig' + +describe Puppet::Type.type(:chocolateyconfig) do + let(:resource) { Puppet::Type.type(:chocolateyconfig).new(:name => "config", :ensure => :absent) } + let(:provider) { Puppet::Provider.new(resource) } + let(:catalog) { Puppet::Resource::Catalog.new } + let (:minimum_supported_version) {'0.9.10.0'} + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.stubs(:choco_version).returns(minimum_supported_version) + + resource.provider = provider + end + + it "should be an instance of Puppet::Type::Chocolateyconfig" do + resource.must be_an_instance_of Puppet::Type::Chocolateyconfig + end + + it "parameter :name should be the name var" do + resource.parameters[:name].isnamevar?.should be_truthy + end + + #string values + ['name','value'].each do |param| + context "parameter :#{param}" do + let (:param_symbol) { param.to_sym } + + it "should not allow nil" do + expect { + resource[param_symbol] = nil + }.to raise_error(Puppet::Error, /Got nil value for #{param}/) + end + + it "should not allow empty" do + expect { + resource[param_symbol] = '' + }.to raise_error(Puppet::Error, /A non-empty #{param} must/) + end + + it "should accept any string value" do + resource[param_symbol] = 'value' + resource[param_symbol] = "c:/thisstring-location/value/somefile.txt" + resource[param_symbol] = "c:\\thisstring-location\\value\\somefile.txt" + end + end + end + + context "param :ensure" do + it "should accept 'present'" do + resource[:ensure] = 'present' + end + + it "should accept present" do + resource[:ensure] = :present + end + + it "should accept absent" do + resource[:ensure] = :absent + end + + it "should reject any other value" do + expect { + resource[:ensure] = :whenever + }.to raise_error(Puppet::Error, /Invalid value :whenever. Valid values are/) + end + end + + it "should autorequire Exec[install_chocolatey_official] when in the catalog" do + exec = Puppet::Type.type(:exec).new(:name => "install_chocolatey_official", :path => "nope") + catalog.add_resource resource + catalog.add_resource exec + + reqs = resource.autorequire + reqs.count.must == 1 + reqs[0].source.must == exec + reqs[0].target.must == resource + end + + context ".validate" do + it "should pass when ensure => absent with no value" do + resource[:ensure] = :absent + + resource.validate + end + + it "should pass when ensure => present with a value" do + resource[:ensure] = :present + resource[:value] = 'yo' + + resource.validate + end + + it "should fail when ensure => present with no value" do + resource[:ensure] = :present + + expect { + resource.validate + }.to raise_error(ArgumentError, /Unless ensure => absent, value is required/) + end + end + +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyfeature_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyfeature_spec.rb new file mode 100644 index 000000000..caac211e3 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateyfeature_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' +require 'puppet/type/chocolateyfeature' + +describe Puppet::Type.type(:chocolateyfeature) do + let(:resource) { Puppet::Type.type(:chocolateyfeature).new(:name => "chocolateyfeature", :ensure => "enabled" ) } + let(:provider) { Puppet::Provider.new(resource) } + let(:catalog) { Puppet::Resource::Catalog.new } + let (:minimum_supported_version) {'0.9.9.0'} + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.stubs(:choco_version).returns(minimum_supported_version) + + resource.provider = provider + resource[:ensure] = 'enabled' + end + + it "should be an instance of Puppet::Type::Chocolateyfeature" do + resource.must be_an_instance_of Puppet::Type::Chocolateyfeature + end + + it "parameter :name should be the name var" do + resource.parameters[:name].isnamevar?.should be_truthy + end + + context "parameter :name" do + let (:param_symbol) { :name } + + it "should accept any string value" do + resource[param_symbol] = 'value' + resource[param_symbol] = "c:/thisstring-location/value/somefile.txt" + resource[param_symbol] = "c:\\thisstring-location\\value\\somefile.txt" + end + end + + context "param :ensure" do + it "should accept 'enabled'" do + resource[:ensure] = 'enabled' + end + + it "should accept enabled" do + resource[:ensure] = :enabled + end + + it "should accept 'disabled'" do + resource[:ensure] = 'disabled' + end + + it "should accept :disabled" do + resource[:ensure] = :disabled + end + + it "should reject any other value" do + expect { + resource[:ensure] = :whenever + }.to raise_error(Puppet::Error, /Invalid value :whenever. Valid values are/) + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateysource_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateysource_spec.rb new file mode 100644 index 000000000..8f38df958 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet/type/chocolateysource_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' +require 'puppet/type/chocolateysource' + +describe Puppet::Type.type(:chocolateysource) do + let(:resource) { Puppet::Type.type(:chocolateysource).new(:name => 'source', :location => 'c:\packages') } + let(:provider) { Puppet::Provider.new(resource) } + let(:catalog) { Puppet::Resource::Catalog.new } + let (:minimum_supported_version) {'0.9.9.0'} + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.stubs(:choco_version).returns(minimum_supported_version) + + resource.provider = provider + end + + it "should be an instance of Puppet::Type::Chocolateysource" do + resource.must be_an_instance_of Puppet::Type::Chocolateysource + end + + it "parameter :name should be the name var" do + resource.parameters[:name].isnamevar?.should be_truthy + end + + #string values + ['name','location','user','password'].each do |param| + context "parameter :#{param}" do + let (:param_symbol) { param.to_sym } + + it "should accept any string value" do + resource[param_symbol] = 'value' + resource[param_symbol] = "c:/thisstring-location/value/somefile.txt" + resource[param_symbol] = "c:\\thisstring-location\\value\\somefile.txt" + end + end + end + + #numeric values + ['priority'].each do |param| + context "parameter :#{param}" do + let (:param_symbol) { param.to_sym } + + it "should accept any numeric value" do + resource[param_symbol] = 0 + resource[param_symbol] = 10 + end + + it "should accept any string that represents a numeric value" do + resource[param_symbol] = '1' + resource[param_symbol] = '0' + end + + it "should not accept other string values" do + expect { + resource[param_symbol] = 'value' + }.to raise_error(Puppet::Error, /An integer is necessary for #{param}/) + end + + it "should not accept symbol values" do + expect { + resource[param_symbol] = :whenever + }.to raise_error(Puppet::Error, /An integer is necessary for #{param}/) + end + end + end + + context "param :ensure" do + it "should accept 'present'" do + resource[:ensure] = 'present' + end + + it "should accept present" do + resource[:ensure] = :present + end + + it "should accept :disabled" do + resource[:ensure] = :disabled + end + + it "should accept absent" do + resource[:ensure] = :absent + end + + it "should reject any other value" do + expect { + resource[:ensure] = :whenever + }.to raise_error(Puppet::Error, /Invalid value :whenever. Valid values are/) + end + end + + it "should autorequire Exec[install_chocolatey_official] when in the catalog" do + exec = Puppet::Type.type(:exec).new(:name => "install_chocolatey_official", :path => "nope") + catalog.add_resource resource + catalog.add_resource exec + + reqs = resource.autorequire + reqs.count.must == 1 + reqs[0].source.must == exec + reqs[0].target.must == resource + end + + context ".validate" do + it "should pass when both user/password are empty" do + resource.validate + end + + it "should pass when both user/password have a value" do + resource[:user] = 'tim' + resource[:password] = 'tim' + + resource.validate + end + + it "should fail when user has a value but password does not" do + resource[:user] = 'tim' + + expect { + resource.validate + }.to raise_error(ArgumentError, /you must specify both values/) + end + + it "should fail when password has a value but user does not" do + resource[:password] = 'tim' + + expect { + resource.validate + }.to raise_error(ArgumentError, /you must specify both values/) + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_common_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_common_spec.rb new file mode 100644 index 000000000..9e0aece4a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_common_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'puppet_x/chocolatey/chocolatey_install' +require 'puppet_x/chocolatey/chocolatey_common' + +describe 'Chocolatey Common' do + + let (:first_compiled_choco_version) {'0.9.9.0'} + let (:newer_choco_version) {'0.9.10.0'} + let (:last_posh_choco_version) {'0.9.8.33'} + + before :each do + PuppetX::Chocolatey::ChocolateyCommon.stubs(:set_env_chocolateyinstall) + end + + context ".chocolatey_command" do + it "should find chocolatey install location based on PuppetX::Chocolatey::ChocolateyInstall", :if => Puppet.features.microsoft_windows? do + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('c:\dude\bin\choco.exe').returns(true) + + PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command.should == 'c:\dude\bin\choco.exe' + end + + it "should find chocolatey install location based on default location", :if => Puppet.features.microsoft_windows? do + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns('c:\dude') + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('c:\dude\bin\choco.exe').returns(false) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('C:\ProgramData\chocolatey\bin\choco.exe').returns(false) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('C:\Chocolatey\bin\choco.exe').returns(false) + + PuppetX::Chocolatey::ChocolateyCommon.chocolatey_command.should == "#{ENV['ALLUSERSPROFILE']}\\chocolatey\\bin\\choco.exe" + end + end + + context ".choco_version" do + it "should return PuppetX::Chocolatey::ChocolateyVersion.version" do + expected = '0.9.9.0.1' + PuppetX::Chocolatey::ChocolateyVersion.expects(:version).returns(expected) + PuppetX::Chocolatey::ChocolateyCommon.clear_cached_values + + PuppetX::Chocolatey::ChocolateyCommon.choco_version.must eq expected + end + end + + context ".choco_config_file" do + let (:choco_install_loc) { 'c:\dude' } + + it "should return the normal config file location when found" do + expected = 'c:\dude\config\chocolatey.config' + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns(choco_install_loc) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(expected).returns(true) + + PuppetX::Chocolatey::ChocolateyCommon.choco_config_file.must eq expected + end + + it "should return the old config file location for older installs" do + expected = 'c:\dude\chocolateyinstall\chocolatey.config' + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns(choco_install_loc) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('c:\dude\config\chocolatey.config').returns(false) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with(expected).returns(true) + + PuppetX::Chocolatey::ChocolateyCommon.choco_config_file.must eq expected + end + + it "should return nil when the config cannot be found" do + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns(choco_install_loc) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('c:\dude\config\chocolatey.config').returns(false) + PuppetX::Chocolatey::ChocolateyCommon.expects(:file_exists?).with('c:\dude\chocolateyinstall\chocolatey.config').returns(false) + + PuppetX::Chocolatey::ChocolateyCommon.choco_config_file.must be_nil + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_install_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_install_spec.rb new file mode 100644 index 000000000..171438308 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_install_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' +require 'puppet_x/chocolatey/chocolatey_install' + +describe 'Chocolatey Install Location' do + + context 'on Windows', :if => Puppet::Util::Platform.windows? do + + it "should return install path from registry if it exists" do + expected_value = 'C:\somewhere' + Win32::Registry.any_instance.expects(:[]).with('ChocolateyInstall').returns(expected_value) + + PuppetX::Chocolatey::ChocolateyInstall.install_path.must == expected_value + end + + it "should return the environment variable ChocolateyInstall if it exists" do + Win32::Registry.any_instance.expects(:[]).with('ChocolateyInstall').raises(Win32::Registry::Error.new(2), 'file not found yo') + + # this is a placeholder, it is already set in spec_helper + ENV['ChocolateyInstall'] = 'c:\blah' + + PuppetX::Chocolatey::ChocolateyInstall.install_path.must == 'c:\blah' + end + + it "should return nil if the environment variable does not exist" do + Win32::Registry.any_instance.expects(:[]).with('ChocolateyInstall').raises(Win32::Registry::Error.new(2), 'file not found yo') + ENV['ChocolateyInstall'] = nil + + PuppetX::Chocolatey::ChocolateyInstall.install_path.must be_nil + end + end + + context 'on Linux', :if => Puppet.features.posix? do + it "should return the environment variable ChocolateyInstall if it exists" do + # this is a placeholder, it is already set in spec_helper + ENV['ChocolateyInstall'] = 'c:\blah' + + PuppetX::Chocolatey::ChocolateyInstall.install_path.must == 'c:\blah' + end + + it "should return nil if the ChocolateyInstall variable does not exist" do + ENV['ChocolateyInstall'] = nil + + PuppetX::Chocolatey::ChocolateyInstall.install_path.must be_nil + end + end + + after :each do + # setting the values back + ENV['ChocolateyInstall'] = 'c:\blah' + end + +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_version_spec.rb b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_version_spec.rb new file mode 100644 index 000000000..57e5618c7 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/spec/unit/puppet_x/chocolatey/chocolatey_version_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' +require 'puppet_x/chocolatey/chocolatey_version' + +describe 'Chocolatey Version' do + + context 'on Windows', :if => Puppet::Util::Platform.windows? do + + context "when Chocolatey is installed" do + before :each do + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns('c:\dude') + File.expects(:exist?).with('c:\dude\bin\choco.exe').returns(true) + end + + it "should return the value from running choco -v" do + expected_value = '1.2.3' + Puppet::Util::Execution.expects(:execute).returns(expected_value) + + PuppetX::Chocolatey::ChocolateyVersion.version.must == expected_value + end + + it "should handle cleaning up spaces" do + expected_value = '1.2.3' + Puppet::Util::Execution.expects(:execute).returns(' ' + expected_value + ' ') + + PuppetX::Chocolatey::ChocolateyVersion.version.must == expected_value + end + + it "should handle older versions of choco" do + expected_value = '1.2.3' + Puppet::Util::Execution.expects(:execute).returns('Please run chocolatey /? or chocolatey help - chocolatey v' + expected_value) + + PuppetX::Chocolatey::ChocolateyVersion.version.must == expected_value + end + + it "should handle other messages that return with version call" do + expected_value = '1.2.3' + Puppet::Util::Execution.expects(:execute).returns("Error setting some value.\nPlease set this value yourself\r\nsound good?\r" + expected_value) + + PuppetX::Chocolatey::ChocolateyVersion.version.must == expected_value + end + + it "should handle a trailing line break" do + expected_value = '1.2.3' + Puppet::Util::Execution.expects(:execute).returns(expected_value + "\r\n") + + PuppetX::Chocolatey::ChocolateyVersion.version.must == expected_value + end + + it "should handle 0.9.8.33 of choco" do + expected_value = '1.2.3' + Puppet::Util::Execution.expects(:execute).returns('!!ATTENTION!! +The next version of Chocolatey (v0.9.9) will require -y to perform + behaviors that change state without prompting for confirmation. Start + using it now in your automated scripts. + + For details on the all new Chocolatey, visit http://bit.ly/new_choco +Please run chocolatey /? or chocolatey help - chocolatey v' + expected_value) + + PuppetX::Chocolatey::ChocolateyVersion.version.must == expected_value + end + end + + context "When Chocolatey is not installed" do + before :each do + PuppetX::Chocolatey::ChocolateyInstall.expects(:install_path).returns(nil) + File.expects(:exist?).with('\bin\choco.exe').returns(false) + end + + it "should return nil" do + PuppetX::Chocolatey::ChocolateyVersion.version.must be_nil + end + end + + end + + context 'on Linux', :if => Puppet.features.posix? do + it "should return nil on a non-windows system" do + PuppetX::Chocolatey::ChocolateyVersion.version.must be_nil + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/templates/InstallChocolatey.ps1.erb b/modules/utilities/windows/repository_managers/chocolatey/templates/InstallChocolatey.ps1.erb new file mode 100644 index 000000000..5a2bff645 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/templates/InstallChocolatey.ps1.erb @@ -0,0 +1,151 @@ +# ============================================================================== +# Copyright 2011 - Present RealDimensions Software, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use +# this file except in compliance with the License. You may obtain a copy of the +# License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. +# ============================================================================== + +$ErrorActionPreference = 'Stop' + +# For some reason try/catch wrapping only ensures +# that none of this script runs at all +# https://tickets.puppetlabs.com/browse/MODULES-2634 +#try { + +# variables +$url = '<%= @download_url %>' +$unzipMethod = '<%= @unzip_type %>' +if ($env:TEMP -eq $null) { + $env:TEMP = Join-Path $env:SystemDrive 'temp' +} +$chocTempDir = Join-Path $env:TEMP "chocolatey" +$tempDir = Join-Path $chocTempDir "chocInstall" +if (![System.IO.Directory]::Exists($tempDir)) {[System.IO.Directory]::CreateDirectory($tempDir)} +$file = Join-Path $tempDir "chocolatey.zip" +$chocErrorLog = Join-Path $tempDir "chocError.log" + +# PowerShell v2/3 caches the output stream. Then it throws errors due +# to the FileStream not being what is expected. Fixes "The OS handle's +# position is not what FileStream expected. Do not use a handle +# simultaneously in one FileStream and in Win32 code or another +# FileStream." + +# This only works with the ConsoleHost (PowerShell InternalHost) +function Fix-PowerShellOutputRedirectionBug { + try{ + # http://www.leeholmes.com/blog/2008/07/30/workaround-the-os-handles-position-is-not-what-filestream-expected/ plus comments + $bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField" + $objectRef = $host.GetType().GetField("externalHostRef", $bindingFlags).GetValue($host) + $bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetProperty" + $consoleHost = $objectRef.GetType().GetProperty("Value", $bindingFlags).GetValue($objectRef, @()) + [void] $consoleHost.GetType().GetProperty("IsStandardOutputRedirected", $bindingFlags).GetValue($consoleHost, @()) + $bindingFlags = [Reflection.BindingFlags] "Instance,NonPublic,GetField" + $field = $consoleHost.GetType().GetField("standardOutputWriter", $bindingFlags) + $field.SetValue($consoleHost, [Console]::Out) + [void] $consoleHost.GetType().GetProperty("IsStandardErrorRedirected", $bindingFlags).GetValue($consoleHost, @()) + $field2 = $consoleHost.GetType().GetField("standardErrorWriter", $bindingFlags) + $field2.SetValue($consoleHost, [Console]::Error) + } catch { + Write-Output "Unable to apply redirection fix. Error: $_" + } +} + +Fix-PowerShellOutputRedirectionBug + +# This should help when certain organizations have issues installing Chocolatey +# Attempt to set highest encryption available for SecurityProtocol. +# PowerShell will not set this by default (until maybe .NET 4.6.x). This +# will typically produce a message for PowerShell v2 (just an info +# message though) +try { + # Set TLS 1.2 (3072), then TLS 1.1 (768), then TLS 1.0 (192), finally SSL 3.0 (48) + # Use integers because the enumeration values for TLS 1.2 and TLS 1.1 won't + # exist in .NET 4.0, even though they are addressable if .NET 4.5+ is + # installed (.NET 4.5 is an in-place upgrade). + [System.Net.ServicePointManager]::SecurityProtocol = 3072 -bor 768 -bor 192 -bor 48 +} catch { + Write-Output "Unable to set PowerShell to use TLS 1.2 and TLS 1.1 due to old .NET Framework installed. If you see underlying connection closed or trust errors, you may need to do one or more of the following: (1) upgrade to .NET Framework 4.5 and PowerShell v3 and/or (2) specify internal Chocolatey package location (see https://forge.puppet.com/puppetlabs/chocolatey#manage-chocolatey-installation)." +} + +function Download-File { +param ( + [string]$url, + [string]$file + ) + Write-Output "Downloading $url to $file" + $downloader = new-object System.Net.WebClient + $downloader.Proxy.Credentials=[System.Net.CredentialCache]::DefaultNetworkCredentials; + $downloader.DownloadFile($url, $file) +} + +# download the package +Download-File $url $file + +if ($unzipMethod -eq '7zip') { + # download 7zip + Write-Output "Download 7Zip commandline tool" + $7zaExe = Join-Path $tempDir '7za.exe' + + Download-File 'https://chocolatey.org/7za.exe' "$7zaExe" + + # unzip the package + Write-Output "Extracting $file to $tempDir..." + Start-Process "$7zaExe" -ArgumentList "x -o`"$tempDir`" -y `"$file`"" -Wait -NoNewWindow +} else { + if ($PSVersionTable.PSVersion.Major -lt 5) { + $shellApplication = new-object -com shell.application + $zipPackage = $shellApplication.NameSpace($file) + $destinationFolder = $shellApplication.NameSpace($tempDir) + $destinationFolder.CopyHere($zipPackage.Items(),0x10) + } else { + Expand-Archive -Path "$file" -DestinationPath "$tempDir" -Force | Out-Null + } +} + +# call chocolatey install +Write-Output "Installing chocolatey on this machine" +$toolsFolder = Join-Path $tempDir "tools" +$chocInstallPS1 = Join-Path $toolsFolder "chocolateyInstall.ps1" + +if ($PSVersionTable.PSVersion.Major -gt 2) { + & $chocInstallPS1 +} else { + $output = Invoke-Expression $chocInstallPS1 + $output + Write-Output "Any errors that occured during install or upgrade are logged here: $chocoErrorLog" + $error | out-file $chocErrorLog +} + +Write-Output 'Ensuring chocolatey commands are on the path' +$chocInstallVariableName = "ChocolateyInstall" +$chocoPath = [Environment]::GetEnvironmentVariable($chocInstallVariableName, [System.EnvironmentVariableTarget]::User) +if ($chocoPath -eq $null -or $chocoPath -eq '') { + $chocoPath = 'C:\ProgramData\Chocolatey' +} + +$chocoBinPath = Join-Path $chocoPath 'bin' + +if ($($env:Path).ToLower().Contains($($chocoBinPath).ToLower()) -eq $false) { + $env:Path = [Environment]::GetEnvironmentVariable('Path',[System.EnvironmentVariableTarget]::Machine); +} + +Write-Output 'Ensuring chocolatey.nupkg is in the lib folder' +$chocoPkgDir = Join-Path $chocoPath 'lib\chocolatey' +$nupkg = Join-Path $chocoPkgDir 'chocolatey.nupkg' +if (![System.IO.Directory]::Exists($chocoPkgDir)) { [System.IO.Directory]::CreateDirectory($chocoPkgDir); } +Copy-Item "$file" "$nupkg" -Force -ErrorAction SilentlyContinue + +#} +#catch +#{ +# Write-Host "$($_.Exception.Message)" +# exit 1 +#} diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/00_pe_install.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/00_pe_install.rb new file mode 100644 index 000000000..40bcd4bea --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/00_pe_install.rb @@ -0,0 +1,20 @@ +require 'master_manipulator' +test_name 'MODULES-3138 - C48 - Install Puppet Enterprise' + +# Check for a master before continuing +if master == nil + fail_test("Master is not set, are you using a host configuration that has a master?") +end + +# Init +step 'Install PE' +install_pe + +step 'Disable Node Classifier' +disable_node_classifier(master) + +step 'Disable Environment Caching' +disable_env_cache(master) + +step 'Restart Puppet Server' +restart_puppet_server(master) diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/01_chocolatey_module.install.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/01_chocolatey_module.install.rb new file mode 100644 index 000000000..efccc025b --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/01_chocolatey_module.install.rb @@ -0,0 +1,25 @@ +test_name 'MODULES-3138 - C97814 - Install Pre-suite Acceptance Test' + +# Beaker option set if "BEAKER_FORGE_HOST" environment variable is present +staging = { :module_name => 'puppetlabs-chocolatey' } +if options[:forge_host] + # Check to see if module version is specified. + staging[:version] = ENV['MODULE_VERSION'] if ENV['MODULE_VERSION'] + step 'Install Chocolatey Module from Forge' + install_dev_puppet_module_on(master, staging) +else + step 'Install Chocolatey Module Dependencies' + %w(puppetlabs-stdlib puppetlabs-powershell badgerious/windows_env).each do |dep| + on(master, puppet("module install #{dep}")) + end +end + +step 'Install Chocolatey Module' +proj_root = File.expand_path(File.join(File.dirname(__FILE__), '../../../')) +local = { :module_name => 'chocolatey', :source => proj_root} + +# Check to see if module version is specified. +staging[:version] = ENV['MODULE_VERSION'] if ENV['MODULE_VERSION'] + +# in CI install from staging forge, otherwise from local +install_dev_puppet_module_on(master, local) diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/02_chocolatey_application_install.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/02_chocolatey_application_install.rb new file mode 100644 index 000000000..782d130ee --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/pre-suite/02_chocolatey_application_install.rb @@ -0,0 +1,47 @@ +require 'master_manipulator' +require 'chocolatey_helper' +test_name 'MODULES-3043 - C97739 - Install Client on Virgin System' + +chocolatey_pp = < 'file:///C:/chocolatey.nupkg', + use_7zip => false, + } +MANIFEST + +chocoVersion = /[0-9]+[\d'.']*/ + +# Setup +step 'Inject "site.pp" on Master' +site_pp = create_site_pp(master, :manifest => chocolatey_pp) +inject_site_pp(master, get_site_pp_path(master), site_pp) + +#Test +confine_block(:to, :platform => 'windows') do + + agents.each do |agent| + opts = { + :acceptable_exit_codes => [0, 2] + } + + url = get_latest_chocholatey_download_url + + step 'Download chocolatey nuget package' do + curl_on(agent, "#{url} > C:/chocolatey.nupkg") + end + + step 'should apply chocolatey manifest and install choco.exe' do + on(agent, puppet('agent -t --environment production'), opts) do |result| + assert_no_match(/Error:/, result.stderr, 'Unexpected error was detected!') + end + end + + step 'should have valid version of Chocolatey' do + on(agent, 'C:/ProgramData/chocolatey/bin/choco.exe -v', :acceptable_exit_codes => 0) do |result| + assert_match(chocoVersion, result.stdout, 'Expected: ' + chocoVersion.to_s + ' but got ' + result.stdout) + end + end + + end + +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/tests/hello.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/tests/hello.rb new file mode 100644 index 000000000..682072efb --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/acceptance/tests/hello.rb @@ -0,0 +1,7 @@ +test_name "Hello Test" + +step "Say Hello" + +hosts.each do |host| + on(host, "echo hello!") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/configs/.gitignore b/modules/utilities/windows/repository_managers/chocolatey/tests/configs/.gitignore new file mode 100644 index 000000000..f67aa100f --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/configs/.gitignore @@ -0,0 +1,3 @@ +# Ignore all configs in this directory, they will be created by beaker-hostgenerator +* +!.gitignore diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/lib/chocolatey_helper.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/lib/chocolatey_helper.rb new file mode 100644 index 000000000..742de3fcd --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/lib/chocolatey_helper.rb @@ -0,0 +1,50 @@ +require 'net/http' +require 'uri' +require 'nokogiri' + +$chocolatey_latest_info_url = "http://nexus.delivery.puppetlabs.net/service/local/nuget/choco-pipeline-tests/Packages()?$filter=((Id%20eq%20%27chocolatey%27)%20and%20(not%20IsPrerelease))%20and%20IsLatestVersion" + +# Extract the url for the latest Puppet hosted version of Chocolatey +# +# ==== Returns +# +# +string+ - url from the feed/content->src of the $chocolatey_latest_info_url +# +# ==== Raises +# +# URI::InvalidURIError +# +# ==== Examples +# +# url = get_latest_chocholatey_download_url; + +def get_latest_chocholatey_download_url() + uri = URI.parse($chocolatey_latest_info_url) + + response = Net::HTTP.get_response(uri) + xml_str = Nokogiri::XML(response.body) + + src_url = xml_str.css('//feed//content').attr('src') + + return src_url +end + +def config_file_location + 'c:\\ProgramData\\chocolatey\\config\\chocolatey.config' +end + +def backup_config + step 'Backup default configuration file' + on(agents, "cmd.exe /c \"copy #{config_file_location} #{config_file_location}.bkp\"") +end + +def reset_config + step 'Reset configuration file to default' + on(agents, "cmd.exe /c \"move #{config_file_location}.bkp #{config_file_location}\"") +end + +def get_xml_value(xpath, file_text) + doc = Nokogiri::XML(file_text) + + doc.xpath(xpath) +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/00_install_certs.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/00_install_certs.rb new file mode 100644 index 000000000..bdcf8b99a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/00_install_certs.rb @@ -0,0 +1,93 @@ +test_name "Install CA Certs" +confine(:to, :platform => 'windows') + +GEOTRUST_GLOBAL_CA = <<-EOM +-----BEGIN CERTIFICATE----- +MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i +YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG +EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg +R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 +9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq +fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv +iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU +1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ +bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW +MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA +ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l +uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn +Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS +tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF +PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un +hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV +5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== +-----END CERTIFICATE----- +EOM + +USERTRUST_NETWORK_CA = <<-EOM +-----BEGIN CERTIFICATE----- +MIIEdDCCA1ygAwIBAgIQRL4Mi1AAJLQR0zYq/mUK/TANBgkqhkiG9w0BAQUFADCB +lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt +SGFyZHdhcmUwHhcNOTkwNzA5MTgxMDQyWhcNMTkwNzA5MTgxOTIyWjCBlzELMAkG +A1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2UgQ2l0eTEe +MBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExhodHRwOi8v +d3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3QtSGFyZHdh +cmUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCx98M4P7Sof885glFn +0G2f0v9Y8+efK+wNiVSZuTiZFvfgIXlIwrthdBKWHTxqctU8EGc6Oe0rE81m65UJ +M6Rsl7HoxuzBdXmcRl6Nq9Bq/bkqVRcQVLMZ8Jr28bFdtqdt++BxF2uiiPsA3/4a +MXcMmgF6sTLjKwEHOG7DpV4jvEWbe1DByTCP2+UretNb+zNAHqDVmBe8i4fDidNd +oI6yqqr2jmmIBsX6iSHzCJ1pLgkzmykNRg+MzEk0sGlRvfkGzWitZky8PqxhvQqI +DsjfPe58BEydCl5rkdbux+0ojatNh4lz0G6k0B4WixThdkQDf2Os5M1JnMWS9Ksy +oUhbAgMBAAGjgbkwgbYwCwYDVR0PBAQDAgHGMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFKFyXyYbKJhDlV0HN9WFlp1L0sNFMEQGA1UdHwQ9MDswOaA3oDWGM2h0 +dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZpcnN0LUhhcmR3YXJlLmNy +bDAxBgNVHSUEKjAoBggrBgEFBQcDAQYIKwYBBQUHAwUGCCsGAQUFBwMGBggrBgEF +BQcDBzANBgkqhkiG9w0BAQUFAAOCAQEARxkP3nTGmZev/K0oXnWO6y1n7k57K9cM +//bey1WiCuFMVGWTYGufEpytXoMs61quwOQt9ABjHbjAbPLPSbtNk28Gpgoiskli +CE7/yMgUsogWXecB5BKV5UU0s4tpvc+0hY91UZ59Ojg6FEgSxvunOxqNDYJAB+gE +CJChicsZUN/KHAG8HQQZexB2lzvukJDKxA4fFm517zP4029bHpbj4HR3dHuKom4t +3XbWOTCC8KucUvIqx69JXn7HaOWCgchqJ/kniCrVWFCVH/A7HFe7fRQ5YiuayZSS +KqMiDP+JJn1fIytH1xUdqWqeUQ0qUZ6B+dQ7XnASfxAynB67nfhmqA== +-----END CERTIFICATE----- +EOM + +EQUIFAX_CA = <<-EOM +-----BEGIN CERTIFICATE----- +MIIDIDCCAomgAwIBAgIENd70zzANBgkqhkiG9w0BAQUFADBOMQswCQYDVQQGEwJV +UzEQMA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2Vy +dGlmaWNhdGUgQXV0aG9yaXR5MB4XDTk4MDgyMjE2NDE1MVoXDTE4MDgyMjE2NDE1 +MVowTjELMAkGA1UEBhMCVVMxEDAOBgNVBAoTB0VxdWlmYXgxLTArBgNVBAsTJEVx +dWlmYXggU2VjdXJlIENlcnRpZmljYXRlIEF1dGhvcml0eTCBnzANBgkqhkiG9w0B +AQEFAAOBjQAwgYkCgYEAwV2xWGcIYu6gmi0fCG2RFGiYCh7+2gRvE4RiIcPRfM6f +BeC4AfBONOziipUEZKzxa1NfBbPLZ4C/QgKO/t0BCezhABRP/PvwDN1Dulsr4R+A +cJkVV5MW8Q+XarfCaCMczE1ZMKxRHjuvK9buY0V7xdlfUNLjUA86iOe/FP3gx7kC +AwEAAaOCAQkwggEFMHAGA1UdHwRpMGcwZaBjoGGkXzBdMQswCQYDVQQGEwJVUzEQ +MA4GA1UEChMHRXF1aWZheDEtMCsGA1UECxMkRXF1aWZheCBTZWN1cmUgQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MQ0wCwYDVQQDEwRDUkwxMBoGA1UdEAQTMBGBDzIwMTgw +ODIyMTY0MTUxWjALBgNVHQ8EBAMCAQYwHwYDVR0jBBgwFoAUSOZo+SvSspXXR9gj +IBBPM5iQn9QwHQYDVR0OBBYEFEjmaPkr0rKV10fYIyAQTzOYkJ/UMAwGA1UdEwQF +MAMBAf8wGgYJKoZIhvZ9B0EABA0wCxsFVjMuMGMDAgbAMA0GCSqGSIb3DQEBBQUA +A4GBAFjOKer89961zgK5F7WF0bnj4JXMJTENAKaSbn+2kmOeUJXRmm/kEd5jhW6Y +7qj/WsjTVbJmcVfewCHrPSqnI0kBBIZCe/zuf6IWUrVnZ9NA2zsmWLIodz2uFHdh +1voqZiegDfqnc1zqcPGUIWVEX/r87yloqaKHee9570+sB3c4 +-----END CERTIFICATE----- +EOM + +hosts.each do |host| + step "Installing Geotrust CA cert" + create_remote_file(host, "geotrustglobal.pem", GEOTRUST_GLOBAL_CA) + on host, "chmod 644 geotrustglobal.pem" + on host, "cmd /c certutil -v -addstore Root `cygpath -w geotrustglobal.pem`" + + step "Installing Usertrust Network CA cert" + create_remote_file(host, "usertrust-network.pem", USERTRUST_NETWORK_CA) + on host, "chmod 644 usertrust-network.pem" + on host, "cmd /c certutil -v -addstore Root `cygpath -w usertrust-network.pem`" + + step "Installing Equifax CA cert" + create_remote_file(host, "equifax.pem", EQUIFAX_CA) + on host, "chmod 644 equifax.pem" + on host, "cmd /c certutil -v -addstore Root `cygpath -w equifax.pem`" +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/01_puppet_agent_install.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/01_puppet_agent_install.rb new file mode 100644 index 000000000..774ff4541 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/01_puppet_agent_install.rb @@ -0,0 +1,13 @@ +test_name 'Install Puppet Agent' + +confine(:to, :platform => 'windows') + +step 'Install Puppet Agent' +if ENV['BEAKER_PUPPET_AGENT_VERSION'] + install_puppet_agent_on(agents, :version => ENV['BEAKER_PUPPET_AGENT_VERSION']) +else + install_puppet_agent_on(agents) +end + +step 'Prevent Puppet Service from Running' +on(agents, puppet('resource service puppet ensure=stopped enable=false')) diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/02_chocolatey_module_install.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/02_chocolatey_module_install.rb new file mode 100644 index 000000000..ffe350b2e --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/02_chocolatey_module_install.rb @@ -0,0 +1,27 @@ +test_name 'MODULES-3138 - C97813 - Install Pre-suite Reference Test' + +confine(:to, :platform => 'windows') + +# Init +proj_root = File.expand_path(File.join(File.dirname(__FILE__), '../../../')) + +staging = { :module_name => 'puppetlabs-chocolatey' } +local = { :module_name => 'chocolatey', :source => proj_root } + +# Beaker option set if "BEAKER_FORGE_HOST" environment variable is present +agents.each do |agent| + if options[:forge_host] + # Check to see if module version is specified. + staging[:version] = ENV['MODULE_VERSION'] if ENV['MODULE_VERSION'] + step 'Install Chocolatey Module from Forge' + install_dev_puppet_module_on(agent, staging) + else + step 'Install Chocolatey Module Dependencies' + %w(puppetlabs-stdlib puppetlabs-powershell badgerious/windows_env).each do |dep| + on(agent, puppet("module install #{dep}")) + end + step 'Install Chocolatey Module from Local Source' + # in CI install from staging forge, otherwise from local + install_dev_puppet_module_on(agent, local) + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/03_chocolatey_application_install.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/03_chocolatey_application_install.rb new file mode 100644 index 000000000..9ca358d80 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/pre-suite/03_chocolatey_application_install.rb @@ -0,0 +1,39 @@ +require 'chocolatey_helper' + +test_name 'MODULES-3043 - C97739 - Install Client on Virgin System' + +confine(:to, :platform => 'windows') + +chocolatey_pp = < 'file:///C:/chocolatey.nupkg', + use_7zip => false, + } +MANIFEST + + +chocoVersion = /[0-9]+[\d'.']*/ + +agents.each do |agent| + opts = { + :acceptable_exit_codes => [0, 2] + } + + url = get_latest_chocholatey_download_url; + + step 'Download chocolatey nuget package' do + curl_on(agent, "#{url} > C:/chocolatey.nupkg") + end + + step 'should apply chocolatey manifest and install choco.exe' do + apply_manifest_on(agent, chocolatey_pp, opts) do |result| + assert_no_match(/Error:/, result.stderr, 'Unexpected error was detected!') + end + end + + step 'should have valid version of Chocolatey' do + on(agent, 'C:/ProgramData/chocolatey/bin/choco.exe -v', :acceptable_exit_codes => 0) do |result| + assert_match(chocoVersion, result.stdout, 'Expected: ' + chocoVersion.to_s + ' but got ' + result.stdout) + end + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_new_config_item.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_new_config_item.rb new file mode 100644 index 000000000..a4446b4d9 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_new_config_item.rb @@ -0,0 +1,29 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Add New Config Item' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'hello123': + ensure => present, + value => 'this guy', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/this guy/, get_xml_value("//config/add[@key='hello123']/@value", result.output).to_s, 'Value did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_value_to_existing_config.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_value_to_existing_config.rb new file mode 100644 index 000000000..4e707d99c --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/add_value_to_existing_config.rb @@ -0,0 +1,29 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Add a Value to an Existing Config Setting' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'proxy': + ensure => present, + value => 'https://somewhere', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/https\:\/\/somewhere/, get_xml_value("//config/add[@key='proxy']/@value", result.output).to_s, 'Value did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/change_config_value.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/change_config_value.rb new file mode 100644 index 000000000..f08b34305 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/change_config_value.rb @@ -0,0 +1,46 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Config Settings Change Config Value' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'proxyUser': + value => 'bob', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest to setup' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/bob/, get_xml_value("//config/add[@key='proxyUser']/@value", result.output).to_s, 'Value did not match') + end +end + +# arrange +chocolatey_src_change = <<-PP + chocolateyconfig {'proxyuser': + value => 'tim', + } +PP + +# act +step 'Apply manifest to change config setting' +apply_manifest(chocolatey_src_change, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/tim/, get_xml_value("//config/add[@key='proxyUser']/@value", result.output).to_s, 'Value did not change') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/ensure_config_value_with_password_in_name.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/ensure_config_value_with_password_in_name.rb new file mode 100644 index 000000000..e9dbd294a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/ensure_config_value_with_password_in_name.rb @@ -0,0 +1,50 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Ensure Config Value with Password In Name' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'proxypassword': + value => 'secrect', + } +PP + +# teardown +teardown do + reset_config +end + +password = '' + +# act +step 'Apply manifest to setup proxyPassword' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + password = get_xml_value("//config/add[@key='proxyPassword']/@value", result.output).to_s + assert_match(/.+/, password, 'Value did not match') + end +end + +# arrange +chocolatey_src_change = <<-PP + chocolateyconfig {'proxypassword': + value => 'secrect2', + } +PP + +# act +step 'Apply manifest to attempt to change proxyPassword - should have no effect' +apply_manifest(chocolatey_src_change, :catch_failures => true) + +step 'Verify results' +# should have no effect +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(password, get_xml_value("//config/add[@key='proxyPassword']/@value", result.output).to_s, 'Value should not have changed') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_appy_bad_manifest.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_appy_bad_manifest.rb new file mode 100644 index 000000000..a11080a10 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_appy_bad_manifest.rb @@ -0,0 +1,25 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Fail to Apply Bad Manifest' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'bob': + ensure => sad, + value => 'yes', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply Manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/Error: Parameter ensure failed on Chocolateyconfig\[bob\]: Invalid value "sad"/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_set_present_without_value.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_set_present_without_value.rb new file mode 100644 index 000000000..6d65dc5fa --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/fail_to_set_present_without_value.rb @@ -0,0 +1,24 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Fail to Set Present With No Value' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'bob': + ensure => present, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply Manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/Error: Validation of Chocolateyconfig\[bob\] failed: Unless ensure => absent, value is required/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_config_value_with_password_in_name.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_config_value_with_password_in_name.rb new file mode 100644 index 000000000..84b1f35a6 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_config_value_with_password_in_name.rb @@ -0,0 +1,50 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Config Settings Remove Value with Password in Name' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'proxypassword': + value => 'secrect', + } +PP + +# teardown +teardown do + reset_config +end + +password = '' + +# act +step 'Apply manifest to setup proxyPassword' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + password = get_xml_value("//config/add[@key='proxyPassword']/@value", result.output).to_s + assert_match(/.+/, password, 'Value did not match') + end +end + +# arrange +chocolatey_src_change = <<-PP + chocolateyconfig {'proxypassword': + ensure => absent, + } +PP + +# act +step 'Apply manifest to remove proxyPassword' +apply_manifest(chocolatey_src_change, :catch_failures => true) + +step 'Verify results' +# should have no effect +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_not_match(/.+/, get_xml_value("//config/add[@key='proxyPassword']/@value", result.output).to_s, 'Value should have been removed') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_value_from_config.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_value_from_config.rb new file mode 100644 index 000000000..6bebb8d4a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyconfig/remove_value_from_config.rb @@ -0,0 +1,28 @@ +require 'chocolatey_helper' +test_name 'MODULES-3035 - Remove Value From Config Setting' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyconfig {'commandExecutionTimeoutSeconds': + ensure => absent, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_not_match(/.+/, get_xml_value("//config/add[@key='commandExecutionTimeoutSeconds']/@value", result.output).to_s, 'Value did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_disabled_feature.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_disabled_feature.rb new file mode 100644 index 000000000..80dbd9d7b --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_disabled_feature.rb @@ -0,0 +1,35 @@ +require 'chocolatey_helper' +test_name 'MODULES-3034 - Disable an Already Disabled Feature' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyfeature {'failOnAutoUninstaller': + ensure => disabled, + } +PP + +# teardown +teardown do + reset_config +end + +# verify prior +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/false/, get_xml_value("//features/feature[@name='failOnAutoUninstaller']/@enabled", result.output).to_s, 'Was not disabled by default, please adjust test to find another value.') + end +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/false/, get_xml_value("//features/feature[@name='failOnAutoUninstaller']/@enabled", result.output).to_s, 'Was not found disabled') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_enabled_feature.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_enabled_feature.rb new file mode 100644 index 000000000..e2ae3e515 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/disable_enabled_feature.rb @@ -0,0 +1,35 @@ +require 'chocolatey_helper' +test_name 'MODULES-3034 - Disable an Enabled Feature' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyfeature {'checksumFiles': + ensure => disabled, + } +PP + +# teardown +teardown do + reset_config +end + +#verify prior +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//features/feature[@name='checksumFiles']/@enabled", result.output).to_s, 'Was not enabled by default, please adjust test to find another value.') + end +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/false/, get_xml_value("//features/feature[@name='checksumFiles']/@enabled", result.output).to_s, 'Was not found disabled') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_disabled_feature.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_disabled_feature.rb new file mode 100644 index 000000000..95c2730c4 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_disabled_feature.rb @@ -0,0 +1,35 @@ +require 'chocolatey_helper' +test_name 'MODULES-3034 - Enable a Disabled Feature' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyfeature {'failOnAutoUninstaller': + ensure => enabled, + } +PP + +# teardown +teardown do + reset_config +end + +# verify prior +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/false/, get_xml_value("//features/feature[@name='failOnAutoUninstaller']/@enabled", result.output).to_s, 'Was not disabled by default, please adjust test to find another value.') + end +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//features/feature[@name='failOnAutoUninstaller']/@enabled", result.output).to_s, 'Was not found enabled') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_enabled_feature.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_enabled_feature.rb new file mode 100644 index 000000000..9928ccc1b --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/enable_enabled_feature.rb @@ -0,0 +1,35 @@ +require 'chocolatey_helper' +test_name 'MODULES-3034 - Enable Already Enabled Feature' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyfeature {'checksumFiles': + ensure => enabled, + } +PP + +# teardown +teardown do + reset_config +end + +#verify prior +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//features/feature[@name='checksumFiles']/@enabled", result.output).to_s, 'Was not enabled by default, please adjust test to find another value.') + end +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//features/feature[@name='checksumFiles']/@enabled", result.output).to_s, 'Was not found enabled') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_enable_nonexistent_feature.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_enable_nonexistent_feature.rb new file mode 100644 index 000000000..05367d19b --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_enable_nonexistent_feature.rb @@ -0,0 +1,24 @@ +require 'chocolatey_helper' +test_name 'MODULES-3034 - Enable non-existent feature' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyfeature {'idontexistfeature123123': + ensure => enabled, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/returned 1: Feature 'idontexistfeature123123' not found/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_remove_feature.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_remove_feature.rb new file mode 100644 index 000000000..b7be83f30 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateyfeature/fail_to_remove_feature.rb @@ -0,0 +1,25 @@ +require 'chocolatey_helper' +test_name 'MODULES-3034 - Attempt to remove feature' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateyfeature {'checksumFiles': + ensure => absent, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/Error: Parameter ensure failed on Chocolateyfeature\[checksumFiles\]: Invalid value \"absent\"/, stderr, "stderr did not match expected") +end + diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package.rb new file mode 100644 index 000000000..654b3bfb5 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package.rb @@ -0,0 +1,62 @@ +require 'chocolatey_helper' +require 'beaker-windows' +test_name 'MODULES-3037 - 97729 Install known good package via manifest and remove via manifest' +confine(:to, :platform => 'windows') + +# arrange +package_name = 'vlc' +package_exe_path = %{C:\\'Program Files\\VideoLAN\\VLC\\vlc.exe'} +software_uninstall_command = %{cmd.exe /C C:\\'Program Files\\VideoLAN\\VLC\\uninstall.exe' /S} + +chocolatey_package_manifest = <<-PP + package { "#{package_name}": + ensure => present, + provider => chocolatey, + source => 'http://nexus.delivery.puppetlabs.net/service/local/nuget/choco-pipeline-tests/' + } +PP + +# teardown +teardown do + on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| + if (result.output =~ /True/i) + retry_on(agent, exec_ps_cmd(software_uninstall_command)) + end + end + #TODO: should we validate that the software was removed successfully here? +end + +#validate +step "should not have valid version of #{package_name}" +on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| + assert_match(/False/i, result.output, "#{package_name} was present before application of manifest.") +end + + +#act +step 'Apply manifest' +apply_manifest(chocolatey_package_manifest, :catch_failures => true) do |result| + assert_match(/Notice\: \/Stage\[main\]\/Main\/Package\[#{package_name}\]\/ensure\: created/, result.stdout, "stdout did not report package creation of #{package_name}") +end + +#validate +step "should have valid version of #{package_name}" +on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| + assert_match(/True/i, result.output, "#{package_name} was not present after application of manifest.") +end + +#arrange +chocolatey_package_manifest = <<-PP + package { "#{package_name}": + ensure => absent, + provider => chocolatey, + } +PP + +#act +step "Uninstall #{package_name} package via manifest" +apply_manifest(chocolatey_package_manifest, :catch_failures => true) do |result| +#validate + assert_match(/Stage\[main\]\/Main\/Package\[#{package_name}\]\/ensure\: removed/, result.stdout, "stdout did not report package removal of #{package_name}") +end + diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package_utf-8.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package_utf-8.rb new file mode 100644 index 000000000..90d01f88e --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateypackage/install_and_remove_good_package_utf-8.rb @@ -0,0 +1,65 @@ +# require 'chocolatey_helper' +# require 'beaker-windows' +# test_name 'MODULES-3037 - C97738 Install known good package with utf-8 via manifest and remove via manifest' +# confine(:to, :platform => 'windows') +# +# # arrange +# package_name = '竹ChocolateyGUIÖ' +# package_exe_path = %{C:\\'Program Files (x86)\\ChocolateyGUI\\ChocolateyGUI.exe'} +# software_uninstall_command = %{msiexec /x C:\\ProgramData\\chocolatey\\lib\\竹ChocolateyGUIÖ\\tools\\竹ChocolateyGUIÖ.msi /q}.force_encoding("ASCII-8BIT") +# +# chocolatey_package_manifest = <<-PP +# package { "#{package_name}": +# ensure => present, +# provider => chocolatey, +# source => 'http://nexus.delivery.puppetlabs.net/service/local/nuget/choco-pipeline-tests/' +# } +# PP +# +# # teardown +# teardown do +# on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| +# if (result.output =~ /True/i) +# on(agent, exec_ps_cmd(software_uninstall_command)) +# end +# end +# on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| +# assert_match(/False/i, result.output, "#{package_name} was present after uninstall.") +# end +# end +# +# #validate +# step "should not have valid version of #{package_name}" +# on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| +# assert_match(/False/i, result.output, "#{package_name} was present before application of manifest.") +# end +# +# +# #act +# step 'Apply manifest' +# apply_manifest(chocolatey_package_manifest, :catch_failures => true) do |result| +# assert_match(/Notice\: \/Stage\[main\]\/Main\/Package\[#{package_name}\]\/ensure\: created/, result.stdout, "stdout did not report package creation of #{package_name}") +# end +# +# #validate +# step "should have valid version of #{package_name}" +# on(agent, exec_ps_cmd("test-path #{package_exe_path}")) do |result| +# assert_match(/True/i, result.output, "#{package_name} was not present after application of manifest.") +# end +# +# #arrange +# chocolatey_package_manifest = <<-PP +# package { "#{package_name}": +# ensure => absent, +# provider => chocolatey, +# } +# PP +# +# #act +# step "Uninstall #{package_name} package via manifest" +# apply_manifest(chocolatey_package_manifest, :catch_failures => true) do |result| +# #validate +# expect_failure('Expected to fail because of MODULES-3541') do +# assert_match(/Stage\[main\]\/Main\/Package\[#{package_name}\]\/ensure\: removed/, result.stdout, "stdout did not report package removal of #{package_name}") +# end +# end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_priority_to_existing_source.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_priority_to_existing_source.rb new file mode 100644 index 000000000..98e53ae2e --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_priority_to_existing_source.rb @@ -0,0 +1,30 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Priority to an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + priority => 1, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/1/, get_xml_value("//sources/source[@id='chocolatey']/@priority", result.output).to_s, 'Priority did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_all_options.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_all_options.rb new file mode 100644 index 000000000..b9abee607 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_all_options.rb @@ -0,0 +1,36 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Source With All Options' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'test': + ensure => present, + location => 'c:\\packages', + priority => 2, + user => 'bob', + password => 'yes', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/c:\\packages/, get_xml_value("//sources/source[@id='test']/@value", result.output).to_s, 'Location did not match') + assert_match(/2/, get_xml_value("//sources/source[@id='test']/@priority", result.output).to_s, 'Priority did not match') + assert_match(/bob/, get_xml_value("//sources/source[@id='test']/@user", result.output).to_s, 'User did not match') + assert_match(/.+/, get_xml_value("//sources/source[@id='test']/@password", result.output).to_s, 'Password was not saved') + assert_match(/false/, get_xml_value("//sources/source[@id='test']/@disabled", result.output).to_s, 'Disabled did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_minimal.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_minimal.rb new file mode 100644 index 000000000..79f7e9df0 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_minimal.rb @@ -0,0 +1,29 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Source Minimal' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'test': + location => 'c:\\packages', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/c:\\packages/, get_xml_value("//sources/source[@id='test']/@value", result.output).to_s, 'Location did not match') + assert_match(/false/, get_xml_value("//sources/source[@id='test']/@disabled", result.output).to_s, 'Disabled did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_normal.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_normal.rb new file mode 100644 index 000000000..b7361a8e1 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_source_normal.rb @@ -0,0 +1,30 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Source Happy Path' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'test': + ensure => present, + location => 'c:\\packages', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/c:\\packages/, get_xml_value("//sources/source[@id='test']/@value", result.output).to_s, 'Location did not match') + assert_match(/false/, get_xml_value("//sources/source[@id='test']/@disabled", result.output).to_s, 'Disabled did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_user_pass_to_existing_source.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_user_pass_to_existing_source.rb new file mode 100644 index 000000000..7fddf95df --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/add_user_pass_to_existing_source.rb @@ -0,0 +1,33 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add User/Password to an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + user => 'tim', + password => 'test', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/tim/, get_xml_value("//sources/source[@id='chocolatey']/@user", result.output).to_s, 'User did not match') + # we are not able to verify password other than if it has a value - it will be encrypted in a non-verifyable way + assert_match(/.+/, get_xml_value("//sources/source[@id='chocolatey']/@password", result.output).to_s, 'Password was not saved') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_priority.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_priority.rb new file mode 100644 index 000000000..81e07a9e0 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_priority.rb @@ -0,0 +1,51 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Change Existing Priority' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + priority => 1, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest to set priority' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify priority setup was added' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/1/, get_xml_value("//sources/source[@id='chocolatey']/@priority", result.output).to_s, 'Priority setup did not match') + end +end + +# arrange +chocolatey_src_change = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + priority => 5, + } +PP + +# act +step 'Apply manifest to change priority' +apply_manifest(chocolatey_src_change, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/5/, get_xml_value("//sources/source[@id='chocolatey']/@priority", result.output).to_s, 'Priority change did not match') + end +end + diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_source_location.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_source_location.rb new file mode 100644 index 000000000..ca3171c7c --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_existing_source_location.rb @@ -0,0 +1,30 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Change Source Location for an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'c:\\packages', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/c:\\packages/, get_xml_value("//sources/source[@id='chocolatey']/@value", result.output).to_s, 'Location did not match') + #todo should also verify there are no duplicates + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_user_pass.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_user_pass.rb new file mode 100644 index 000000000..ab23923de --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/change_user_pass.rb @@ -0,0 +1,54 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Change User/Password In an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + user => 'tim', + password => 'test', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest to setup user/password' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify user/password setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/tim/, get_xml_value("//sources/source[@id='chocolatey']/@user", result.output).to_s, 'User setup did not match') + # we are not able to verify password other than if it has a value - it will be encrypted in a non-verifyable way + assert_match(/.+/, get_xml_value("//sources/source[@id='chocolatey']/@password", result.output).to_s, 'Password was not saved') + end +end + +# arrange +chocolatey_src_change = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + user => 'bob', + password => 'newpass', + } +PP + +# act +step 'Apply manifest to change user/password' +apply_manifest(chocolatey_src_change, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/bob/, get_xml_value("//sources/source[@id='chocolatey']/@user", result.output).to_s, 'User change did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source.rb new file mode 100644 index 000000000..1abda7a2a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source.rb @@ -0,0 +1,28 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Disable an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => disabled, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//sources/source[@id='chocolatey']/@disabled", result.output).to_s, 'Disabled did not match') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source_two_runs.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source_two_runs.rb new file mode 100644 index 000000000..c8618dc46 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/disable_existing_source_two_runs.rb @@ -0,0 +1,43 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Disable an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => disabled, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//sources/source[@id='chocolatey']/@disabled", result.output).to_s, 'Disabled did not match') + end +end + +# act +step 'Apply manifest a second time' +apply_manifest(chocolatey_src, :catch_failures => true) do |result| + assert_not_match(/Chocolateysource\[chocolatey\]\/user\: defined 'user' as ''/, result.stdout, "User was adjusted and should not have been") +end + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/true/, get_xml_value("//sources/source[@id='chocolatey']/@disabled", result.output).to_s, 'Disabled did not match') + + end +end + diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_apply_source_without_location.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_apply_source_without_location.rb new file mode 100644 index 000000000..5a5043e8a --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_apply_source_without_location.rb @@ -0,0 +1,24 @@ +require 'chocolatey_helper' +test_name 'MODULES-3430 - Add Source Sad Path: Fail to apply manifest without location' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify failure' + assert_match(/Error: Validation of Chocolateysource\[chocolatey\] failed: A non-empty location/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_appy_bad_manifest.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_appy_bad_manifest.rb new file mode 100644 index 000000000..85d15c873 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_appy_bad_manifest.rb @@ -0,0 +1,25 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Source Sad Path: Fail to apply bad manifest' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'test': + ensure => sad, + location => 'c:\\packages', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply Manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/Error: Parameter ensure failed on Chocolateysource\[test\]: Invalid value "sad"/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_password_without_user.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_password_without_user.rb new file mode 100644 index 000000000..7648252fc --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_password_without_user.rb @@ -0,0 +1,26 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Source Sad Path: Set password with no user' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + password => 'test', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply Manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/Error: Validation of Chocolateysource\[chocolatey\] failed: If specifying user\/password, you must specify both values/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_user_without_password.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_user_without_password.rb new file mode 100644 index 000000000..db18b0002 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/fail_to_set_user_without_password.rb @@ -0,0 +1,26 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Add Source Sad Path: Set user with no password' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + user => 'tim', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply Manifest' +apply_manifest(chocolatey_src, :expect_failures => true) do + step 'Verify Failure' + assert_match(/Error: Validation of Chocolateysource\[chocolatey\] failed: If specifying user\/password, you must specify both values/, stderr, "stderr did not match expected") +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_existing_source.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_existing_source.rb new file mode 100644 index 000000000..78552bfde --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_existing_source.rb @@ -0,0 +1,28 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Remove an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => absent, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_not_match(/chocolatey/, get_xml_value("//sources/source[@id='chocolatey']/@id", result.output).to_s, 'Source was not removed') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_priority_from_existing_source.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_priority_from_existing_source.rb new file mode 100644 index 000000000..84359bb7d --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_priority_from_existing_source.rb @@ -0,0 +1,50 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Remove Priority from an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + priority => 1, + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest to setup priority' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify priority setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/1/, get_xml_value("//sources/source[@id='chocolatey']/@priority", result.output).to_s, 'Priority did not match') + end +end + +# arrange +chocolatey_src_remove = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + } +PP + +# act +step 'Apply manifest to remove priority' +apply_manifest(chocolatey_src_remove, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/0/, get_xml_value("//sources/source[@id='chocolatey']/@priority", result.output).to_s, 'Priority change did not match') + end +end + diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_user_pass_from_existing_source.rb b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_user_pass_from_existing_source.rb new file mode 100644 index 000000000..9a2204ef6 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/reference/tests/chocolateysource/remove_user_pass_from_existing_source.rb @@ -0,0 +1,53 @@ +require 'chocolatey_helper' +test_name 'MODULES-3037 - Remove User/Password From an Existing Source' +confine(:to, :platform => 'windows') + +backup_config + +# arrange +chocolatey_src = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + user => 'tim', + password => 'test', + } +PP + +# teardown +teardown do + reset_config +end + +# act +step 'Apply manifest to setup user/password' +apply_manifest(chocolatey_src, :catch_failures => true) + +step 'Verify user/password setup' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_match(/tim/, get_xml_value("//sources/source[@id='chocolatey']/@user", result.output).to_s, 'User setup did not match') + # we are not able to verify password other than if it has a value - it will be encrypted in a non-verifyable way + assert_match(/.+/, get_xml_value("//sources/source[@id='chocolatey']/@password", result.output).to_s, 'Password was not saved') + end +end + +# arrange +chocolatey_src_remove = <<-PP + chocolateysource {'chocolatey': + ensure => present, + location => 'https://chocolatey.org/api/v2', + } +PP + +# act +step 'Apply manifest to remove user/password' +apply_manifest(chocolatey_src_remove, :catch_failures => true) + +step 'Verify results' +agents.each do |agent| + on(agent, "cmd.exe /c \"type #{config_file_location}\"") do |result| + assert_not_match(/.+/, get_xml_value("//sources/source[@id='chocolatey']/@user", result.output).to_s, 'User was not removed') + assert_not_match(/.+/, get_xml_value("//sources/source[@id='chocolatey']/@password", result.output).to_s, 'Password was not removed') + end +end diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/acceptance_tests.sh b/modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/acceptance_tests.sh new file mode 100644 index 000000000..d1c11a4e7 --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/acceptance_tests.sh @@ -0,0 +1,68 @@ +#!/bin/bash +set -e + +# Init +SCRIPT_PATH=$(pwd) +BASENAME_CMD="basename ${SCRIPT_PATH}" +SCRIPT_BASE_PATH=`eval ${BASENAME_CMD}` +declare -a ARGS + +# Argument Parsing +if [ $# -eq 0 ]; then + ARGS[0]='windows-2012r2-64mda' + ARGS[1]='http://pe-releases.puppetlabs.lan/2016.1.1/' + ARGS[2]='forge' +elif [[ $# -lt 3 || $# -gt 4 ]]; then + echo 'USAGE acceptance_tests.sh ' + exit 1 +else + ARGS=("$@") +fi + +# Figure out where we are in the directory hierarchy +if [ $SCRIPT_BASE_PATH = "test_run_scripts" ]; then + cd ../../ +fi + +# Determine if the forge is needed for the test. +if [ ${ARGS[2]} == 'forge' ]; then + echo 'Testing Module Using Forge Package' + export BEAKER_FORGE_HOST=api-module-staging.puppetlabs.com +elif [ ${ARGS[2]} == 'local' ]; then + echo 'Testing Module Using Local Code' +else + echo 'You must specify "forge" or "local" for test type!' + echo 'USAGE acceptance_tests.sh ' + exit 1 +fi + +# Determine if a module version was specified. +if [ -n "${ARGS[3]}" ]; then + echo "Using Module Version: ${ARGS[3]}" + export MODULE_VERSION=${ARGS[3]} +elif [[ $# -eq 3 && ${ARGS[2]} == 'forge' ]]; then + echo 'WARNING: Running Acceptance Tests from Forge without Module Version!' +fi + +# Sleep so the user has time to read script messages. +sleep 2 + +export pe_dist_dir=${ARGS[1]} +export GEM_SOURCE=http://rubygems.delivery.puppetlabs.net + +bundle install --without build development test --path .bundle/gems + +bundle exec beaker \ + --preserve-hosts onfail \ + --config tests/configs/${ARGS[0]} \ + --debug \ + --tests tests/acceptance/tests \ + --keyfile ~/.ssh/id_rsa-acceptance \ + --pre-suite tests/acceptance/pre-suite \ + --load-path tests/lib + +TEST_RESULT=$? + +rm -rf tmp + +exit $TEST_RESULT diff --git a/modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/reference_tests.sh b/modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/reference_tests.sh new file mode 100644 index 000000000..f48088ebd --- /dev/null +++ b/modules/utilities/windows/repository_managers/chocolatey/tests/test_run_scripts/reference_tests.sh @@ -0,0 +1,65 @@ +#!/bin/bash + +set -e + +# Init +SCRIPT_PATH=$(pwd) +BASENAME_CMD="basename ${SCRIPT_PATH}" +SCRIPT_BASE_PATH=`eval ${BASENAME_CMD}` +declare -a ARGS + +# Argument Parsing +if [ $# -eq 0 ]; then + ARGS[0]='windows-2012r2-64a' + ARGS[1]='1.4.1' + ARGS[2]='local' +elif [[ $# -lt 3 || $# -gt 4 ]]; then + echo 'USAGE reference_tests.sh ' + exit 1 +else + ARGS=("$@") +fi + +# Figure out where we are in the directory hierarchy +if [ $SCRIPT_BASE_PATH = "test_run_scripts" ]; then + cd ../../ +fi + +# Determine if the forge is needed for the test. +if [ ${ARGS[2]} == 'forge' ]; then + echo 'Testing Module Using Forge Package' + export BEAKER_FORGE_HOST=api-module-staging.puppetlabs.com +elif [ ${ARGS[2]} == 'local' ]; then + echo 'Testing Module Using Local Code' +else + echo 'You must specify "forge" or "local" for test type!' + echo 'USAGE reference_tests.sh ' + exit 1 +fi + +# Determine if a module version was specified. +if [ -n "${ARGS[3]}" ]; then + echo "Using Module Version: ${ARGS[3]}" + export MODULE_VERSION=${ARGS[3]} +elif [[ $# -eq 3 && ${ARGS[2]} == 'forge' ]]; then + echo 'WARNING: Running Reference Tests from Forge without Module Version!' +fi + +# Sleep so the user has time to read script messages. +sleep 2 + +export BEAKER_PUPPET_AGENT_VERSION=${ARGS[1]} +export GEM_SOURCE=http://rubygems.delivery.puppetlabs.net + +bundle install --without build development test --path .bundle/gems + +bundle exec beaker \ + --preserve-hosts onfail \ + --config tests/configs/${ARGS[0]} \ + --debug \ + --tests tests/reference/tests \ + --keyfile ~/.ssh/id_rsa-acceptance \ + --pre-suite tests/reference/pre-suite \ + --load-path tests/lib \ + --type aio + \ No newline at end of file diff --git a/modules/utilities/windows/text_editor/notepadplusplus/manifests/install.pp b/modules/utilities/windows/text_editor/notepadplusplus/manifests/install.pp new file mode 100644 index 000000000..75a555700 --- /dev/null +++ b/modules/utilities/windows/text_editor/notepadplusplus/manifests/install.pp @@ -0,0 +1,12 @@ +class notepadplusplus::install { + include chocolatey + + notice('Installing notepad++') + + package { 'notepadplusplus': + ensure => installed, + provider => 'chocolatey', + } + + notice('Notepad++ install finished') +} \ No newline at end of file diff --git a/modules/utilities/windows/text_editor/notepadplusplus/notepadplusplus.pp b/modules/utilities/windows/text_editor/notepadplusplus/notepadplusplus.pp new file mode 100644 index 000000000..d17804ad6 --- /dev/null +++ b/modules/utilities/windows/text_editor/notepadplusplus/notepadplusplus.pp @@ -0,0 +1 @@ +include notepadplusplus::install \ No newline at end of file diff --git a/modules/utilities/windows/text_editor/notepadplusplus/secgen_metadata.xml b/modules/utilities/windows/text_editor/notepadplusplus/secgen_metadata.xml new file mode 100644 index 000000000..fd8e5d183 --- /dev/null +++ b/modules/utilities/windows/text_editor/notepadplusplus/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Notepadplusplus install + Jason Keighley + Apache v2 + A Notepadplusplus installation + + text_editor + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/firefox/firefox.pp b/modules/utilities/windows/web_browsers/firefox/firefox.pp new file mode 100644 index 000000000..832d010f2 --- /dev/null +++ b/modules/utilities/windows/web_browsers/firefox/firefox.pp @@ -0,0 +1 @@ +include firefox::install \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/firefox/manifests/install.pp b/modules/utilities/windows/web_browsers/firefox/manifests/install.pp new file mode 100644 index 000000000..152580361 --- /dev/null +++ b/modules/utilities/windows/web_browsers/firefox/manifests/install.pp @@ -0,0 +1,10 @@ +class firefox::install { + include chocolatey + + notice('Installing Firefox') + + package { 'firefox': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/firefox/secgen_metadata.xml b/modules/utilities/windows/web_browsers/firefox/secgen_metadata.xml new file mode 100644 index 000000000..cd1582504 --- /dev/null +++ b/modules/utilities/windows/web_browsers/firefox/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Firefox install + Jason Keighley + Apache v2 + A Firefox installation + + web_browsers + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp b/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp new file mode 100644 index 000000000..b0f1ec41e --- /dev/null +++ b/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp @@ -0,0 +1 @@ +include google_chrome::install \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp b/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp new file mode 100644 index 000000000..3b58d75a3 --- /dev/null +++ b/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp @@ -0,0 +1,13 @@ +class google_chrome::configure { + # Need to ensure unique to each version of Windows, + # different versions may have different install locations + exec { 'google-chrome-initialize': + require => Package[googlechrome], + command => 'C:\windows\system32\cmd.exe /C start "" "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" https://www.google.com', + } + + exec { 'google-chrome-kill-all-processes': + require => Exec[google-chrome-initialize], + command => "$cmd_executable_install_path\\cmd.exe /C \"taskkill /F /IM chrome.exe /T\"" + } +} \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/google_chrome/manifests/install.pp b/modules/utilities/windows/web_browsers/google_chrome/manifests/install.pp new file mode 100644 index 000000000..fafd6a493 --- /dev/null +++ b/modules/utilities/windows/web_browsers/google_chrome/manifests/install.pp @@ -0,0 +1,10 @@ +class google_chrome::install { + include chocolatey + + notice('Installing google chrome') + + package { 'googlechrome': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/google_chrome/secgen_metadata.xml b/modules/utilities/windows/web_browsers/google_chrome/secgen_metadata.xml new file mode 100644 index 000000000..ecdf22d6e --- /dev/null +++ b/modules/utilities/windows/web_browsers/google_chrome/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Google Chrome install + Jason Keighley + Apache v2 + A Google Chrome installation + + web_browsers + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/internet_explorer_11/internet_explorer_11.pp b/modules/utilities/windows/web_browsers/internet_explorer_11/internet_explorer_11.pp new file mode 100644 index 000000000..50706d629 --- /dev/null +++ b/modules/utilities/windows/web_browsers/internet_explorer_11/internet_explorer_11.pp @@ -0,0 +1 @@ +include internet_explorer_11::install \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/internet_explorer_11/manifests/install.pp b/modules/utilities/windows/web_browsers/internet_explorer_11/manifests/install.pp new file mode 100644 index 000000000..d265c4366 --- /dev/null +++ b/modules/utilities/windows/web_browsers/internet_explorer_11/manifests/install.pp @@ -0,0 +1,10 @@ +class internet_explorer_11::install { + include chocolatey + + notice('Installing Internet Explorer 11') + + package { 'ie11': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/internet_explorer_11/secgen_metadata.xml b/modules/utilities/windows/web_browsers/internet_explorer_11/secgen_metadata.xml new file mode 100644 index 000000000..cc1ea294d --- /dev/null +++ b/modules/utilities/windows/web_browsers/internet_explorer_11/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Internet Explorer 11 install + Jason Keighley + Apache v2 + An Internet Explorer 11 installation + + web_browsers + windows + + + + + + Chocolatey install + + \ No newline at end of file From c4bec37107cea31d63007ac6803ec1779e901eb9 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Tue, 21 Mar 2017 19:23:55 +0000 Subject: [PATCH 02/24] Moved over ForGen internet history module need to modify into SecGen structure --- Gemfile | 1 + lib/resources/urllists/urls_history_noise | 30 ++++ lib/schemas/forensic_metadata_schema.xsd | 168 ++++++++++++++++++ .../internet_history_chrome/manifests/init.pp | 17 ++ .../secgen_metadata.xml | 13 ++ .../templates/insert_history.erb | 17 ++ .../urls_noise/manifests/.no_puppet | 0 .../urls_noise/secgen_local/local.rb | 46 +++++ .../urls_noise/t_secgen_metadata.xml | 24 +++ .../urls_noise/urls_noise.pp | 0 10 files changed, 316 insertions(+) create mode 100644 lib/resources/urllists/urls_history_noise create mode 100644 lib/schemas/forensic_metadata_schema.xsd create mode 100644 modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp create mode 100644 modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml create mode 100644 modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb create mode 100644 modules/generators/forensics/internet_artifacts/urls_noise/manifests/.no_puppet create mode 100644 modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb create mode 100644 modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml create mode 100644 modules/generators/forensics/internet_artifacts/urls_noise/urls_noise.pp diff --git a/Gemfile b/Gemfile index 97e5eecaa..70da4950c 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'wordlist' gem 'faker' gem 'forgery' gem 'redcarpet' +gem 'sqlite3' #development only gems go here group :test, :development do diff --git a/lib/resources/urllists/urls_history_noise b/lib/resources/urllists/urls_history_noise new file mode 100644 index 000000000..00251d736 --- /dev/null +++ b/lib/resources/urllists/urls_history_noise @@ -0,0 +1,30 @@ +http://www.independent.co.uk/voices/aleppo-crisis-syrian-war-bashar-al-assad-isis-more-propaganda-than-news-a7479901.html +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=14&ved=0ahUKEwj9ndTE0MHSAhUMmBoKHQrYB-4Q1ScIbjAN&url=http%3A%2F%2Fwww.independent.co.uk%2Fvoices%2Faleppo-crisis-syrian-war-bashar-al-assad-isis-more-propaganda-than-news-a7479901.html&usg=AFQjCNG6lQ4cMWcGWZ_m4rVrNbYp-mwoIg&bvm=bv.148747831,d.d2s&cad=rja +http://www.bbc.co.uk/news/uk-politics-32810887 +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=13&ved=0ahUKEwj9ndTE0MHSAhUMmBoKHQrYB-4Q1ScIbDAM&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fuk-politics-32810887&usg=AFQjCNEqQpbty5PU_3C4iiZw6AOoZP5rcQ&bvm=bv.148747831,d.d2s&cad=rja +https://www.theguardian.com/us-news/ng-interactive/2017/jan/20/donald-trump-first-100-days-president-daily-updates +http://www.express.co.uk/news +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=11&ved=0ahUKEwj9ndTE0MHSAhUMmBoKHQrYB-4QFghjMAo&url=http%3A%2F%2Fwww.express.co.uk%2Fnews&usg=AFQjCNGiC2MV8fS-ERjk3lrYasD0xIDeWA&bvm=bv.148747831,d.d2s&cad=rja +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=12&ved=0ahUKEwj9ndTE0MHSAhUMmBoKHQrYB-4Q1ScIajAL&url=https%3A%2F%2Fwww.theguardian.com%2Fus-news%2Fng-interactive%2F2017%2Fjan%2F20%2Fdonald-trump-first-100-days-president-daily-updates&usg=AFQjCNHand24H2PO66b0OFbQ-6ZYCOyqRA&bvm=bv.148747831,d.d2s&cad=rja +http://www.dailymail.co.uk/news/index.html +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=10&ved=0ahUKEwj9ndTE0MHSAhUMmBoKHQrYB-4QFghdMAk&url=http%3A%2F%2Fwww.dailymail.co.uk%2Fnews%2Findex.html&usg=AFQjCNGeWFCOn7XJalTOjKGdVTeIQK_T8w&bvm=bv.148747831,d.d2s&cad=rja +https://www.google.co.uk/search?q=bbc+news&ie=utf-8&oe=utf-8&client=firefox-b-ab&gfe_rd=cr&ei=VDS9WKrJO9DCaOf6r5gI#q=news&* +http://www.bbc.co.uk/news/business-39175740 +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=8&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQqUMIPzAH&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fbusiness-39175740&usg=AFQjCNHTVX1eg93T6kfEoVO5WtqnB9ERGg&cad=rja +http://www.bbc.co.uk/news/uk-39176538 +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=7&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQqUMIOzAG&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fuk-39176538&usg=AFQjCNF1Lsgw73VzGWkAC9a-4t9fOHoB2A&cad=rja +http://www.bbc.co.uk/news/uk-39176110 +http://www.bbc.co.uk/news/world +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=6&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQqUMINzAF&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fuk-39176110&usg=AFQjCNH75QSNyOTErnsehKkHGwN5WxqgkA&cad=rja +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=3&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQjBAILzAC&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fworld&usg=AFQjCNF5X_xXbi4RdS-n3YXetp2xyKOgZQ&cad=rja +http://www.bbc.co.uk/news/business +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=5&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQjBAIMTAE&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fbusiness&usg=AFQjCNGbcH7S94Wc1xAlQpYi2mWNP2gLhg&cad=rja +http://www.bbc.co.uk/news/world/us_and_canada +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=4&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQjBAILTAD&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fworld%2Fus_and_canada&usg=AFQjCNHfYWm_9x_ukbMz4b1vRFgm-PD5Pw&cad=rja +http://www.bbc.co.uk/news/uk +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=2&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQjBAIKzAB&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews%2Fuk&usg=AFQjCNHuNSSR7R0oMF6unGocwTpv-iLAgQ&cad=rja +http://www.bbc.co.uk/news +https://www.google.co.uk/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&ved=0ahUKEwjL95e-0MHSAhUJORoKHXXbCWcQFggjMAA&url=http%3A%2F%2Fwww.bbc.co.uk%2Fnews&usg=AFQjCNEonTvvrrbEptl1n_9nug5piXqeOQ&cad=rja +https://www.google.co.uk/search?q=bbc+news&ie=utf-8&oe=utf-8&client=firefox-b-ab&gfe_rd=cr&ei=VDS9WKrJO9DCaOf6r5gI +https://www.google.co.uk/?gws_rd=ssl +https://www.mozilla.org/en-US/firefox/51.0.1/firstrun/ \ No newline at end of file diff --git a/lib/schemas/forensic_metadata_schema.xsd b/lib/schemas/forensic_metadata_schema.xsd new file mode 100644 index 000000000..55e29282e --- /dev/null +++ b/lib/schemas/forensic_metadata_schema.xsd @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp b/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp new file mode 100644 index 000000000..89c197f34 --- /dev/null +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp @@ -0,0 +1,17 @@ +$user_account = 'vagrant' +$url_paths = ["https://www.offensive-security.com/backtrack/exploit-db-updates"] + +# file { "C:\Users\{$user_account}\AppData\Roaming\Mozilla\Firefox\Profiles\{$mozilla_profile_number}.default\places.sqlite": +# +# } + +# exec { "add-chrome-history": +# command => "", +# } + +file { 'add-chrome-history': + ensure => 'present', + path => "C:/Users/$user_account/AppData/Local/Google/Chrome/User Data/Default/History", + content => template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') + # content => inline_template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') +} \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml b/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml new file mode 100644 index 000000000..be171533f --- /dev/null +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml @@ -0,0 +1,13 @@ + + + Jason Keighley + evidence + windows + cybercrime + internet_history_chrome + + evidence/windows/cybercrime/internet_history_chrome + Creates google chrome internet history + Puppet + init.pp + \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb new file mode 100644 index 000000000..523fba220 --- /dev/null +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb @@ -0,0 +1,17 @@ +<% #puts "Hello World!" %> +<% + require 'sqlite3' + + local_user = 'vagrant' + chrome_user = 'Default' + + SQLite3::Database.new( "C:\\Users\\#{local_user}\\AppData\\Local\\Google\\Chrome\\User Data\\#{chrome_user}\\History" ) do |db| + db.execute( "select * from urls" ) do |row| + p row + end + db.execute( + "INSERT INTO urls(id, url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) +VALUES ('37', 'test_url', 'test_title', '1', '1', '1', '0', '0');" + ) + end +%> \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/manifests/.no_puppet b/modules/generators/forensics/internet_artifacts/urls_noise/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb b/modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb new file mode 100644 index 000000000..0472da946 --- /dev/null +++ b/modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb @@ -0,0 +1,46 @@ +#!/usr/bin/ruby +require_relative '../../../../lib/objects/local_string_encoder.rb' +require 'sqlite3' + +class UrlsNoiseEncoder < StringEncoder + attr_accessor :name + + def initialize + super + self.module_name = 'Url Noise Encoder' + self.name = '' + end + + def encode_all + domain = craft_domain + tld = %w(org com net co.uk).sample + + self.outputs << "#{domain}.#{tld}" + end + + # Creates a domain from the business_name + def craft_domain + domain = self.name + # replace spaces + domain = domain.downcase.tr(' ', %w(_ -).sample) + # strip punctuation and return + domain.gsub(/[^0-9a-z\s_-]/i, '') + end + + def process_options(opt, arg) + super + if opt == '--name' + self.name << arg + end + end + + def get_options_array + super + [['--name', GetoptLong::REQUIRED_ARGUMENT]] + end + + def encoding_print_string + 'name: ' + self.name.to_s + end +end + +UrlsNoiseEncoder.new.run \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml b/modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml new file mode 100644 index 000000000..87c918f68 --- /dev/null +++ b/modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml @@ -0,0 +1,24 @@ + + + + Domain Encoder + Thomas Shaw + MIT + Creates a domain based on user inputting a name. + + string_generator + domain_generator + domain + local_calculation + linux + windows + + https://github.com/stympy/faker + + + name + + domain + \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/urls_noise.pp b/modules/generators/forensics/internet_artifacts/urls_noise/urls_noise.pp new file mode 100644 index 000000000..e69de29bb From a13431fad9af89934477f861a1ab45af528c8826 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Mon, 27 Mar 2017 09:21:40 +0100 Subject: [PATCH 03/24] Moved over ForGen internet history module need to modify into SecGen structure IN PROGRESS: Creating URL generator and chrome history file generator, added forensic option to xml_report_generator. Need to find a way to efficiently pass history file from chrome_history_file_generator to chrome_history forensic module. --- Gemfile.lock | 4 +- lib/output/xml_report_generator.rb | 8 +++ lib/readers/module_reader.rb | 7 ++- lib/readers/system_reader.rb | 2 +- .../{urls_history_noise => generic_urls} | 0 lib/schemas/forensic_metadata_schema.xsd | 14 ++--- lib/schemas/scenario_schema.xsd | 20 +++--- .../internet_history_chrome.pp | 1 + .../internet_history_chrome/manifests/init.pp | 28 +++++---- .../secgen_metadata.xml | 52 ++++++++++++---- .../chrome_history_file_generator.pp} | 0 .../manifests/.no_puppet} | 0 .../secgen_local/local.rb | 52 ++++++++++++++++ .../secgen_metadata.xml | 20 ++++++ .../url_generator/manifests/.no_puppet | 0 .../url_generator/secgen_local/local.rb | 62 +++++++++++++++++++ .../url_generator/secgen_metadata.xml | 21 +++++++ .../url_generator/url_generator.pp | 0 .../urls_noise/secgen_local/local.rb | 46 -------------- .../urls_noise/t_secgen_metadata.xml | 24 ------- .../chrome_history_example.xml | 29 +++++++++ secgen.rb | 7 ++- 22 files changed, 282 insertions(+), 115 deletions(-) rename lib/resources/urllists/{urls_history_noise => generic_urls} (100%) create mode 100644 modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp rename modules/generators/forensics/internet_artifacts/{urls_noise/manifests/.no_puppet => chrome_history_file_generator/chrome_history_file_generator.pp} (100%) rename modules/generators/forensics/internet_artifacts/{urls_noise/urls_noise.pp => chrome_history_file_generator/manifests/.no_puppet} (100%) create mode 100644 modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb create mode 100644 modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml create mode 100644 modules/generators/forensics/internet_artifacts/url_generator/manifests/.no_puppet create mode 100644 modules/generators/forensics/internet_artifacts/url_generator/secgen_local/local.rb create mode 100644 modules/generators/forensics/internet_artifacts/url_generator/secgen_metadata.xml create mode 100644 modules/generators/forensics/internet_artifacts/url_generator/url_generator.pp delete mode 100644 modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb delete mode 100644 modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml create mode 100644 scenarios/simple_examples/forensic_examples/chrome_history_example.xml diff --git a/Gemfile.lock b/Gemfile.lock index bf72394ad..2f438ba9f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -48,6 +48,7 @@ GEM semantic_puppet (0.1.3) spidr (0.6.0) nokogiri (~> 1.3) + sqlite3 (1.3.13) thor (0.19.1) wordlist (0.1.1) spidr (~> 0.2) @@ -66,8 +67,9 @@ DEPENDENCIES rake rdoc redcarpet + sqlite3 wordlist yard BUNDLED WITH - 1.14.3 + 1.14.5 diff --git a/lib/output/xml_report_generator.rb b/lib/output/xml_report_generator.rb index 2e5efc630..6f993030c 100644 --- a/lib/output/xml_report_generator.rb +++ b/lib/output/xml_report_generator.rb @@ -91,6 +91,14 @@ def module_element(selected_module, xml) } end } + when 'forensic' + xml.forensic(selected_module.attributes_for_scenario_output) { + selected_module.received_inputs.each do |key,value| + xml.input({"into" => key}) { + xml.value value + } + end + } when 'network' xml.network(selected_module.attributes_for_scenario_output) else diff --git a/lib/readers/module_reader.rb b/lib/readers/module_reader.rb index 071909f03..4a4bd6b95 100644 --- a/lib/readers/module_reader.rb +++ b/lib/readers/module_reader.rb @@ -31,6 +31,11 @@ def self.read_utilities return read_modules('utility', UTILITIES_DIR, UTILITY_SCHEMA_FILE, true) end + # reads in all forensics + def self.read_forensics + return read_modules('forensic', FORENSICS_DIR, FORENSICS_SCHEMA_FILE, true) + end + # reads in all utilities def self.read_generators return read_modules('generator', GENERATORS_DIR, GENERATOR_SCHEMA_FILE, true) @@ -147,7 +152,7 @@ def self.read_modules(module_type, modules_dir, schema_file, require_puppet) # for each default input doc.xpath("/#{module_type}/default_input").each do |inputs_doc| - inputs_doc.xpath('descendant::vulnerability | descendant::service | descendant::utility | descendant::network | descendant::base | descendant::encoder | descendant::generator').each do |module_node| + inputs_doc.xpath('descendant::vulnerability | descendant::service | descendant::utility | descendant::forensic | descendant::network | descendant::base | descendant::encoder | descendant::generator').each do |module_node| # create a selector module, which is a regular module instance used as a placeholder for matching requirements module_selector = Module.new(module_node.name) diff --git a/lib/readers/system_reader.rb b/lib/readers/system_reader.rb index efc485623..dbfbc5ec2 100644 --- a/lib/readers/system_reader.rb +++ b/lib/readers/system_reader.rb @@ -64,7 +64,7 @@ def self.read_scenario(scenario_file) end # for each module selection - system_node.xpath('//vulnerability | //service | //utility | //build | //network | //base | //encoder | //generator').each do |module_node| + system_node.xpath('//vulnerability | //service | //utility | //forensic | //build | //network | //base | //encoder | //generator').each do |module_node| # create a selector module, which is a regular module instance used as a placeholder for matching requirements module_selector = Module.new(module_node.name) diff --git a/lib/resources/urllists/urls_history_noise b/lib/resources/urllists/generic_urls similarity index 100% rename from lib/resources/urllists/urls_history_noise rename to lib/resources/urllists/generic_urls diff --git a/lib/schemas/forensic_metadata_schema.xsd b/lib/schemas/forensic_metadata_schema.xsd index 55e29282e..24a3ea748 100644 --- a/lib/schemas/forensic_metadata_schema.xsd +++ b/lib/schemas/forensic_metadata_schema.xsd @@ -1,7 +1,7 @@ @@ -89,11 +89,11 @@ - - + + - - + + @@ -137,7 +137,7 @@ - + diff --git a/lib/schemas/scenario_schema.xsd b/lib/schemas/scenario_schema.xsd index 47e86363d..c570a522a 100644 --- a/lib/schemas/scenario_schema.xsd +++ b/lib/schemas/scenario_schema.xsd @@ -15,12 +15,13 @@ - - + + + - - - + + + @@ -42,9 +43,10 @@ - - - + + + + @@ -103,7 +105,7 @@ - + diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp b/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp new file mode 100644 index 000000000..0183cdb2e --- /dev/null +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp @@ -0,0 +1 @@ +include internet_history_chrome::init \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp b/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp index 89c197f34..f6b82de68 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp @@ -1,17 +1,19 @@ -$user_account = 'vagrant' -$url_paths = ["https://www.offensive-security.com/backtrack/exploit-db-updates"] +class internet_history_chrome::init { + $user_account = 'vagrant' + $url_paths = ["https://www.offensive-security.com/backtrack/exploit-db-updates"] -# file { "C:\Users\{$user_account}\AppData\Roaming\Mozilla\Firefox\Profiles\{$mozilla_profile_number}.default\places.sqlite": -# -# } + # file { "C:\Users\{$user_account}\AppData\Roaming\Mozilla\Firefox\Profiles\{$mozilla_profile_number}.default\places.sqlite": + # + # } -# exec { "add-chrome-history": -# command => "", -# } + # exec { "add-chrome-history": + # command => "", + # } -file { 'add-chrome-history': - ensure => 'present', - path => "C:/Users/$user_account/AppData/Local/Google/Chrome/User Data/Default/History", - content => template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') - # content => inline_template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') + file { 'add-chrome-history': + ensure => 'present', + path => "C:/Users/$user_account/AppData/Local/Google/Chrome/User Data/Default/History", + content => template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') + # content => inline_template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') + } } \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml b/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml index be171533f..e7f8a6c22 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml @@ -1,13 +1,41 @@ - - + + + + Internet history chrome Jason Keighley - evidence - windows - cybercrime - internet_history_chrome - - evidence/windows/cybercrime/internet_history_chrome - Creates google chrome internet history - Puppet - init.pp - \ No newline at end of file + Apache v2 + Create internet history for the Google Chrome browser + + internet_artifacts + windows + + + + + + + + chrome_history_file + + + + + + + + + + + + + + + + + + + Google Chrome install + + \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/manifests/.no_puppet b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/chrome_history_file_generator.pp similarity index 100% rename from modules/generators/forensics/internet_artifacts/urls_noise/manifests/.no_puppet rename to modules/generators/forensics/internet_artifacts/chrome_history_file_generator/chrome_history_file_generator.pp diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/urls_noise.pp b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/manifests/.no_puppet similarity index 100% rename from modules/generators/forensics/internet_artifacts/urls_noise/urls_noise.pp rename to modules/generators/forensics/internet_artifacts/chrome_history_file_generator/manifests/.no_puppet diff --git a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb new file mode 100644 index 000000000..26f13a397 --- /dev/null +++ b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb @@ -0,0 +1,52 @@ +#!/usr/bin/ruby +require_relative '../../../../../../lib/objects/local_string_generator.rb' +require 'date' +require 'sqlite3' +require 'fileutils' + +# class ChromeHistoryFileGenerator < StringGenerator +# attr_accessor :history_urls +# +# def initialize +# super +# self.module_name = 'Chrome history file generator' +# self.history_urls = '' +# end +# +# def generate + local_user = 'vagrant' + chrome_user = 'Default' + + history_urls = { + 'url_test_1' => {:url => 'test1', :title => 'test1', :visit_count => '1', :typed_count => '1', :last_visit_time => 10, :hidden => '0', :favicon_id => '0'}, + 'url_test_2' => {:url => 'test2', :title => 'test2', :visit_count => '2', :typed_count => '2', :last_visit_time => 10, :hidden => '0', :favicon_id => '0'}, + 'url_test_3' => {:url => 'test3', :title => 'test3', :visit_count => '3', :typed_count => '3', :last_visit_time => 10, :hidden => '0', :favicon_id => '0'} + } + + FileUtils.cp('../templates/History.source', '../templates/History') + + database = SQLite3::Database.new( "../templates/History" ) do |db| + history_urls.each_value do |details| + db.execute( + "INSERT INTO urls(url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) +VALUES ('#{details[:url]}', '#{details[:title]}', '#{details[:visit_count]}', '#{details[:typed_count]}', '#{details[:last_visit_time]}', '#{details[:hidden]}', '#{details[:favicon_id]}');" + ) + end + + # db.execute( "select * from urls" ) do |row| + # puts row + # # p row + # end + +# "INSERT INTO urls(url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) +# VALUES ('test_url', 'test_title', '1', '1', '1', '0', '0');" + + end + + # puts self.history_urls + # + # self.outputs << database + # end +# end +# +# ChromeHistoryFileGenerator.new.run \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml new file mode 100644 index 000000000..0d209a158 --- /dev/null +++ b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Chrome history file generator + Jason Keighley + Apache v2 + Randomly selects urls from pool of crime urls dependent on inputted crime type. + + internet_history_file_generator + windows + + + + + history_urls + + urls + \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/url_generator/manifests/.no_puppet b/modules/generators/forensics/internet_artifacts/url_generator/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/internet_artifacts/url_generator/secgen_local/local.rb b/modules/generators/forensics/internet_artifacts/url_generator/secgen_local/local.rb new file mode 100644 index 000000000..225a0ca1f --- /dev/null +++ b/modules/generators/forensics/internet_artifacts/url_generator/secgen_local/local.rb @@ -0,0 +1,62 @@ +#!/usr/bin/ruby +require_relative '../../../../../../lib/objects/local_string_generator.rb' +require 'date' + +class UrlGenerator < StringGenerator + def initialize + super + self.module_name = 'Url generator' + self.number_of_generic_urls = '10' + self.generic_urls_start_time = '3rd july 2017' + self.number_of_cybercrime_urls = '' + self.cybercrime_urls_start_time = '' + end + + def generate + + + urls = Hash.new + + # number_of_generic_urls = 10 + # generic_urls_start_time = '3rd july 2017 15:16:20' + # generic_urls_length_time = '2 days 1 hour 10 seconds' + # ROOT_DIR = File.expand_path('../../../../../../../',__FILE__) + # URLLISTS_DIR = "#{ROOT_DIR}/lib/resources/urllists" + + # Generic filler urls + generic_urls = File.readlines("#{URLLISTS_DIR}/generic_urls").sample(self.number_of_generic_urls.to_int) + + # Crime url start + # cybercrime_urls = File.readlines("#{URLLISTS_DIR}/cybercrime_urls").sample(self.number_of_cybercrime_urls).chomp + + generic_urls.each do |url| + start_time = DateTime.parse(self.generic_urls_start_time) + + # urls[url] = DateTime.new( + # rand(start_time.year..length_time.year + 1), + # rand(start_time.month..length_time.month + 1), + # rand(start_time.day..length_time.day + 1), + # rand(start_time.hour..length_time.hour + 1), + # rand(start_time.minute..length_time.minute + 1), + # rand(start_time.second..length_time.second + 1) + # ) + + # urls[url] = DateTime.new( + # rand(length_time.year - start_time.year), + # rand(length_time.month - start_time.month), + # rand(length_time.day - start_time.day), + # rand(length_time.hour - start_time.hour), + # rand(length_time.minute - start_time.minute), + # rand(length_time.second - start_time.second) + # ) + + urls[url] = { :url => url ,:title => 'test', :visit_count => '1', :typed_count => '1', :last_visit_time => start_time, :hidden => '0', :favicon_id => '0' } + end + + puts urls + + self.outputs << urls + end +end + +UrlGenerator.new.run \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/url_generator/secgen_metadata.xml b/modules/generators/forensics/internet_artifacts/url_generator/secgen_metadata.xml new file mode 100644 index 000000000..0f3991c54 --- /dev/null +++ b/modules/generators/forensics/internet_artifacts/url_generator/secgen_metadata.xml @@ -0,0 +1,21 @@ + + + + Url generator + Jason Keighley + Apache v2 + Randomly selects urls from pool of crime urls dependent on inputted crime type. + + url_generator + windows + + + + + number_of_generic_urls + number_of_cybercrime_urls + + urls + \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/url_generator/url_generator.pp b/modules/generators/forensics/internet_artifacts/url_generator/url_generator.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb b/modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb deleted file mode 100644 index 0472da946..000000000 --- a/modules/generators/forensics/internet_artifacts/urls_noise/secgen_local/local.rb +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/ruby -require_relative '../../../../lib/objects/local_string_encoder.rb' -require 'sqlite3' - -class UrlsNoiseEncoder < StringEncoder - attr_accessor :name - - def initialize - super - self.module_name = 'Url Noise Encoder' - self.name = '' - end - - def encode_all - domain = craft_domain - tld = %w(org com net co.uk).sample - - self.outputs << "#{domain}.#{tld}" - end - - # Creates a domain from the business_name - def craft_domain - domain = self.name - # replace spaces - domain = domain.downcase.tr(' ', %w(_ -).sample) - # strip punctuation and return - domain.gsub(/[^0-9a-z\s_-]/i, '') - end - - def process_options(opt, arg) - super - if opt == '--name' - self.name << arg - end - end - - def get_options_array - super + [['--name', GetoptLong::REQUIRED_ARGUMENT]] - end - - def encoding_print_string - 'name: ' + self.name.to_s - end -end - -UrlsNoiseEncoder.new.run \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml b/modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml deleted file mode 100644 index 87c918f68..000000000 --- a/modules/generators/forensics/internet_artifacts/urls_noise/t_secgen_metadata.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - Domain Encoder - Thomas Shaw - MIT - Creates a domain based on user inputting a name. - - string_generator - domain_generator - domain - local_calculation - linux - windows - - https://github.com/stympy/faker - - - name - - domain - \ No newline at end of file diff --git a/scenarios/simple_examples/forensic_examples/chrome_history_example.xml b/scenarios/simple_examples/forensic_examples/chrome_history_example.xml new file mode 100644 index 000000000..e6e98423e --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/chrome_history_example.xml @@ -0,0 +1,29 @@ + + + + + + + + + storage_server + + + + + + + + + + + + + + + + + + diff --git a/secgen.rb b/secgen.rb index 13b004f7b..cc73995fe 100644 --- a/secgen.rb +++ b/secgen.rb @@ -59,6 +59,10 @@ def build_config(scenario, out_dir, options) all_available_utilities = ModuleReader.read_utilities Print.std "#{all_available_utilities.size} utility modules loaded" + Print.info 'Reading available forensic modules...' + all_available_forensics = ModuleReader.read_forensics + Print.std "#{all_available_forensics.size} forensic modules loaded" + Print.info 'Reading available generator modules...' all_available_generators = ModuleReader.read_generators Print.std "#{all_available_generators.size} generator modules loaded" @@ -74,7 +78,8 @@ def build_config(scenario, out_dir, options) Print.info 'Resolving systems: randomising scenario...' # for each system, select modules all_available_modules = all_available_bases + all_available_builds + all_available_vulnerabilties + - all_available_services + all_available_utilities + all_available_generators + all_available_encoders + all_available_networks + all_available_services + all_available_utilities + all_available_forensics + all_available_generators + + all_available_encoders + all_available_networks # update systems with module selections systems.map! {|system| system.module_selections = system.resolve_module_selection(all_available_modules) From ac41834e828d257c696bd7a996a8d5b384f2c03a Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Mon, 3 Apr 2017 11:45:20 +0100 Subject: [PATCH 04/24] Moved over ForGen internet history module need to modify into SecGen structure IN PROGRESS: Creating URL generator and chrome history file generator, added forensic option to xml_report_generator. Need to find a way to efficiently pass history file from chrome_history_file_generator to chrome_history forensic module. ERRORING: Recieving error ==> storage_server: Error: Could not find class internet_history_chrome::init for vagrant-2008r2.lan on node vagrant-2008r2.lan Need to look into vagrant/puppet not findign forensics internet history class --- lib/templates/Puppetfile.erb | 2 +- .../internet_history_chrome/files/History | 0 .../templates/insert_history.erb | 37 ++++++++++++------- .../templates/insert_history.erb.old | 17 +++++++++ 4 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History create mode 100644 modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb.old diff --git a/lib/templates/Puppetfile.erb b/lib/templates/Puppetfile.erb index dddc7bfcf..1059d0728 100644 --- a/lib/templates/Puppetfile.erb +++ b/lib/templates/Puppetfile.erb @@ -13,7 +13,7 @@ mod 'SecGen-secgen_functions', :path => '<%= SECGEN_FUNCTIONS_PUPPET_DIR %>' <% @currently_processing_system.module_selections.each do |selected_module| -%> <% case selected_module.module_type - when 'vulnerability', 'service', 'utility', 'build' -%> + when 'vulnerability', 'service', 'utility', 'build', 'forensic' -%> mod 'SecGen-<%= selected_module.module_path_name %>/<%= selected_module.module_path_end %>', :path => '<%="#{ROOT_DIR}/#{selected_module.module_path}"%>' <% end -%> <% end -%> diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History b/modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History new file mode 100644 index 000000000..e69de29bb diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb index 523fba220..fd7ff1c4a 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb @@ -1,17 +1,28 @@ -<% #puts "Hello World!" %> -<% - require 'sqlite3' +<% #require 'json' + #$secgen_parameters = JSON.parse(@json_inputs) + #$server_name = $secgen_parameters['server_name'].first + #$welcome_msg = $secgen_parameters['welcome_msg'].first + # + #if $secgen_parameters['url'] + # $business_name = $secgen_parameters['business_name'].first + # $welcome_msg = "Welcome to the #{$business_name} FTP server!" + #end - local_user = 'vagrant' - chrome_user = 'Default' + require 'sqlite' - SQLite3::Database.new( "C:\\Users\\#{local_user}\\AppData\\Local\\Google\\Chrome\\User Data\\#{chrome_user}\\History" ) do |db| - db.execute( "select * from urls" ) do |row| - p row - end - db.execute( - "INSERT INTO urls(id, url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) + local_user = 'vagrant' + chrome_user = 'Default' + + SQLite3::Database.new( "/media/user/3TB_internal_drive/Documents/SecGen/modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History" ) do |db| + db.execute( "select * from urls" ) do |row| + puts row + end + db.execute( + "INSERT INTO urls(id, url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) VALUES ('37', 'test_url', 'test_title', '1', '1', '1', '0', '0');" - ) - end + ) + db.execute( "select * from urls" ) do |row| + puts row + end + end %> \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb.old b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb.old new file mode 100644 index 000000000..523fba220 --- /dev/null +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb.old @@ -0,0 +1,17 @@ +<% #puts "Hello World!" %> +<% + require 'sqlite3' + + local_user = 'vagrant' + chrome_user = 'Default' + + SQLite3::Database.new( "C:\\Users\\#{local_user}\\AppData\\Local\\Google\\Chrome\\User Data\\#{chrome_user}\\History" ) do |db| + db.execute( "select * from urls" ) do |row| + p row + end + db.execute( + "INSERT INTO urls(id, url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) +VALUES ('37', 'test_url', 'test_title', '1', '1', '1', '0', '0');" + ) + end +%> \ No newline at end of file From 4acc43323a6b52ad93e3f21aa72a0ac95a9a0bc2 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sat, 15 Apr 2017 18:25:26 +0100 Subject: [PATCH 05/24] Basic timestamp modules --- lib/templates/Puppetfile.erb | 1 + .../create_directory/create_directory.pp | 7 +++ .../create_directory/manifests/init.pp | 6 +++ .../create_directory/secgen_metadata.xml | 23 +++++++++ .../create_file/create_file.pp | 9 ++++ .../create_file/manifests/init.pp | 7 +++ .../create_file/secgen_metadata.xml | 28 +++++++++++ .../change_timestamp_all_main_times.pp | 14 ++++++ .../change_timestamp_creation_time.pp | 6 +++ .../change_timestamp_last_access_time.pp | 6 +++ .../change_timestamp_last_write_time.pp | 6 +++ .../manifests/init.pp | 16 ++++++ .../secgen_metadata.xml | 32 ++++++++++++ .../change_timestamp_creation_time.pp | 14 ++++++ .../manifests/init.pp | 9 ++++ .../secgen_metadata.xml | 32 ++++++++++++ .../change_timestamp_last_access_time.pp | 14 ++++++ .../manifests/init.pp | 9 ++++ .../secgen_metadata.xml | 32 ++++++++++++ .../change_timestamp_last_write_time.pp | 14 ++++++ .../manifests/init.pp | 9 ++++ .../secgen_metadata.xml | 32 ++++++++++++ .../generate_random_time.pp} | 0 .../generate_random_time/manifests/.no_puppet | 0 .../secgen_local/local.rb | 24 +++++++++ .../generate_random_time/secgen_metadata.xml | 21 ++++++++ .../simple_timestamp_example.xml | 49 +++++++++++++++++++ scenarios/windows_scenario.xml | 17 ++++++- 28 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 modules/forensics/windows/file_manipulation/create_directory/create_directory.pp create mode 100644 modules/forensics/windows/file_manipulation/create_directory/manifests/init.pp create mode 100644 modules/forensics/windows/file_manipulation/create_directory/secgen_metadata.xml create mode 100644 modules/forensics/windows/file_manipulation/create_file/create_file.pp create mode 100644 modules/forensics/windows/file_manipulation/create_file/manifests/init.pp create mode 100644 modules/forensics/windows/file_manipulation/create_file/secgen_metadata.xml create mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/change_timestamp_all_main_times.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/init.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml create mode 100644 modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_creation_time/manifests/init.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml create mode 100644 modules/forensics/windows/timestamps/change_timestamp_last_access_time/change_timestamp_last_access_time.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_last_access_time/manifests/init.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml create mode 100644 modules/forensics/windows/timestamps/change_timestamp_last_write_time/change_timestamp_last_write_time.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_last_write_time/manifests/init.pp create mode 100644 modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml rename modules/{forensics/windows/internet_artifacts/internet_history_chrome/files/History => generators/forensics/time/generate_random_time/generate_random_time.pp} (100%) create mode 100644 modules/generators/forensics/time/generate_random_time/manifests/.no_puppet create mode 100644 modules/generators/forensics/time/generate_random_time/secgen_local/local.rb create mode 100644 modules/generators/forensics/time/generate_random_time/secgen_metadata.xml create mode 100644 scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml diff --git a/lib/templates/Puppetfile.erb b/lib/templates/Puppetfile.erb index 1059d0728..94b2c82e0 100644 --- a/lib/templates/Puppetfile.erb +++ b/lib/templates/Puppetfile.erb @@ -10,6 +10,7 @@ forge "https://forgeapi.puppetlabs.com" mod 'puppetlabs-stdlib' # stdlib enables parsejson() in manifests and other useful functions mod 'SecGen-secgen_functions', :path => '<%= SECGEN_FUNCTIONS_PUPPET_DIR %>' +mod 'puppetlabs-powershell', '2.1.0' <% @currently_processing_system.module_selections.each do |selected_module| -%> <% case selected_module.module_type diff --git a/modules/forensics/windows/file_manipulation/create_directory/create_directory.pp b/modules/forensics/windows/file_manipulation/create_directory/create_directory.pp new file mode 100644 index 000000000..f2f7ca2ef --- /dev/null +++ b/modules/forensics/windows/file_manipulation/create_directory/create_directory.pp @@ -0,0 +1,7 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$new_directory_path=$secgen_parameters['new_directory_path'][0] + +class { 'create_directory': + directory_path => $new_directory_path, +} \ No newline at end of file diff --git a/modules/forensics/windows/file_manipulation/create_directory/manifests/init.pp b/modules/forensics/windows/file_manipulation/create_directory/manifests/init.pp new file mode 100644 index 000000000..4da2b660e --- /dev/null +++ b/modules/forensics/windows/file_manipulation/create_directory/manifests/init.pp @@ -0,0 +1,6 @@ +class create_directory ($directory_path) { + file { 'create_directory': + path => $directory_path, + ensure => 'directory', + } +} \ No newline at end of file diff --git a/modules/forensics/windows/file_manipulation/create_directory/secgen_metadata.xml b/modules/forensics/windows/file_manipulation/create_directory/secgen_metadata.xml new file mode 100644 index 000000000..baf7c260e --- /dev/null +++ b/modules/forensics/windows/file_manipulation/create_directory/secgen_metadata.xml @@ -0,0 +1,23 @@ + + + + Create directory + Jason Keighley + Apache v2 + Create new directory + + file_manipulation + windows + + + + + new_directory_path + + + C:\Users\vagrant\Desktop\Hello + + + \ No newline at end of file diff --git a/modules/forensics/windows/file_manipulation/create_file/create_file.pp b/modules/forensics/windows/file_manipulation/create_file/create_file.pp new file mode 100644 index 000000000..66f79fb4a --- /dev/null +++ b/modules/forensics/windows/file_manipulation/create_file/create_file.pp @@ -0,0 +1,9 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$new_file_path=$secgen_parameters['new_file_path'][0] +$new_file_contents=$secgen_parameters['new_file_contents'][0] + +class { 'create_file': + file_path => $new_file_path, + file_contents => $new_file_contents +} \ No newline at end of file diff --git a/modules/forensics/windows/file_manipulation/create_file/manifests/init.pp b/modules/forensics/windows/file_manipulation/create_file/manifests/init.pp new file mode 100644 index 000000000..59c06b08c --- /dev/null +++ b/modules/forensics/windows/file_manipulation/create_file/manifests/init.pp @@ -0,0 +1,7 @@ +class create_file ($file_path, $file_contents) { + file { 'create_file': + path => $file_path, + ensure => 'file', + content => $file_contents, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/file_manipulation/create_file/secgen_metadata.xml b/modules/forensics/windows/file_manipulation/create_file/secgen_metadata.xml new file mode 100644 index 000000000..23d615ed4 --- /dev/null +++ b/modules/forensics/windows/file_manipulation/create_file/secgen_metadata.xml @@ -0,0 +1,28 @@ + + + + Create file + Jason Keighley + Apache v2 + Create new file + + file_manipulation + windows + + + + + new_file_path + new_file_contents + + + C:\Users\vagrant\Desktop\Hello.txt + + + + Test Test Test Test Test + + + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/change_timestamp_all_main_times.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/change_timestamp_all_main_times.pp new file mode 100644 index 000000000..d11eb1b0c --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/change_timestamp_all_main_times.pp @@ -0,0 +1,14 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$all_times_file_path=$secgen_parameters['all_times_file_path'][0] +$all_times_date=$secgen_parameters['all_times_date'][0] + +file { 'ensure_path_present': + path => $all_times_file_path, + ensure => 'present' +} + +class { 'change_timestamp_all_main_times': + file_path => $all_times_file_path, + file_time => $all_times_date +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp new file mode 100644 index 000000000..3eb95c607 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp @@ -0,0 +1,6 @@ +class change_timestamp_creation_time::change_timestamp_creation_time ($file_path, $file_time) { + exec { 'change_creation_time': + command => "$((ls ${file_path}).CreationTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp new file mode 100644 index 000000000..6c8aef1a4 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp @@ -0,0 +1,6 @@ +class change_timestamp_last_access_time::change_timestamp_last_access_time ($file_path, $file_time) { + exec { 'change_last_access_time': + command => "$((ls ${file_path}).LastAccessTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp new file mode 100644 index 000000000..199e139de --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp @@ -0,0 +1,6 @@ +class change_timestamp_last_write_time::change_timestamp_last_write_time ($file_path, $file_time) { + exec { 'change_last_write_time': + command => "$((ls ${file_path}).LastWriteTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/init.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/init.pp new file mode 100644 index 000000000..2bf1c3ab4 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/init.pp @@ -0,0 +1,16 @@ +class change_timestamp_all_main_times ($file_path, $file_time) { + exec { 'change_last_write_time': + command => "$((ls ${file_path}).LastWriteTime = '${file_time}')", + provider => powershell, + } + + exec { 'change_last_access_time': + command => "$((ls ${file_path}).LastAccessTime = '${file_time}')", + provider => powershell, + } + + exec { 'change_creation_time': + command => "$((ls ${file_path}).CreationTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml new file mode 100644 index 000000000..ff4c98582 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml @@ -0,0 +1,32 @@ + + + + Change timestamp all main times + Jason Keighley + Apache v2 + Change timestamp last write time + + timestamps + windows + + + + + all_times_file_path + all_times_date + + + C:\Users\vagrant\Desktop\Hello.txt + + + + 11/25/2000 11:12:13 + + + + + + + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp b/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp new file mode 100644 index 000000000..265827e7c --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp @@ -0,0 +1,14 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$creation_time_file_path=$secgen_parameters['creation_time_file_path'][0] +$creation_time_date=$secgen_parameters['creation_time_date'][0] + +file { 'ensure_path_present': + path => $creation_time_file_path, + ensure => 'present' +} + +class { 'change_timestamp_last_access_time': + file_path => $creation_time_file_path, + file_time => $creation_time_date +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_creation_time/manifests/init.pp b/modules/forensics/windows/timestamps/change_timestamp_creation_time/manifests/init.pp new file mode 100644 index 000000000..ffd21a20e --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_creation_time/manifests/init.pp @@ -0,0 +1,9 @@ +# $file_path = 'C:\Users\vagrant\Desktop\Hello.txt' +# $file_time = '11/25/2000 11:12:13' + +class change_timestamp_creation_time ($file_path, $file_time) { + exec { 'change_creation_time': + command => "$((ls ${file_path}).CreationTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml new file mode 100644 index 000000000..6ed144d5c --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml @@ -0,0 +1,32 @@ + + + + Change timestamp creation time + Jason Keighley + Apache v2 + Change timestamp creation time + + timestamps + windows + + + + + creation_time_file_path + creation_time_date + + + C:\Users\vagrant\Desktop\Hello.txt + + + + 11/25/2000 11:12:13 + + + + + + + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/change_timestamp_last_access_time.pp b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/change_timestamp_last_access_time.pp new file mode 100644 index 000000000..3281b50ec --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/change_timestamp_last_access_time.pp @@ -0,0 +1,14 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$last_access_time_file_path=$secgen_parameters['last_access_time_file_path'][0] +$last_access_time_date=$secgen_parameters['last_access_time_date'][0] + +file { 'ensure_path_present': + path => $last_access_time_file_path, + ensure => 'present' +} + +class { 'change_timestamp_last_access_time': + file_path => $last_access_time_file_path, + file_time => $last_access_time_date +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/manifests/init.pp b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/manifests/init.pp new file mode 100644 index 000000000..1ee31ce98 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/manifests/init.pp @@ -0,0 +1,9 @@ +# $file_path = 'C:\Users\vagrant\Desktop\Hello.txt' +# $file_time = '11/25/2000 11:12:13' + +class change_timestamp_last_access_time ($file_path, $file_time) { + exec { 'change_last_access_time': + command => "$((ls ${file_path}).LastAccessTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml new file mode 100644 index 000000000..b08cb3c1e --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml @@ -0,0 +1,32 @@ + + + + Change timestamp last access time + Jason Keighley + Apache v2 + Change timestamp last access time + + timestamps + windows + + + + + last_access_time_file_path + last_access_time_date + + + C:\Users\vagrant\Desktop\Hello.txt + + + + 11/25/2000 11:12:13 + + + + + + + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/change_timestamp_last_write_time.pp b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/change_timestamp_last_write_time.pp new file mode 100644 index 000000000..e9b0b98cf --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/change_timestamp_last_write_time.pp @@ -0,0 +1,14 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$last_write_time_file_path=$secgen_parameters['last_write_time_file_path'][0] +$last_write_time_date=$secgen_parameters['last_write_time_date'][0] + +file { 'ensure_path_present': + path => $last_write_time_file_path, + ensure => 'present' +} + +class { 'change_timestamp_last_write_time': + file_path => $last_write_time_file_path, + file_time => $last_write_time_date +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/manifests/init.pp b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/manifests/init.pp new file mode 100644 index 000000000..e6d31faf0 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/manifests/init.pp @@ -0,0 +1,9 @@ +# $file_path = 'C:\Users\vagrant\Desktop\Hello.txt' +# $file_time = '11/25/2000 11:12:13' + +class change_timestamp_last_write_time ($file_path, $file_time) { + exec { 'change_last_write_time': + command => "$((ls ${file_path}).LastWriteTime = '${file_time}')", + provider => powershell, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml new file mode 100644 index 000000000..efb246b10 --- /dev/null +++ b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml @@ -0,0 +1,32 @@ + + + + Change timestamp last write time + Jason Keighley + Apache v2 + Change timestamp last write time + + timestamps + windows + + + + + last_write_time_file_path + last_write_time_date + + + C:\Users\vagrant\Desktop\Hello.txt + + + + 11/25/2000 11:12:13 + + + + + + + \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History b/modules/generators/forensics/time/generate_random_time/generate_random_time.pp similarity index 100% rename from modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History rename to modules/generators/forensics/time/generate_random_time/generate_random_time.pp diff --git a/modules/generators/forensics/time/generate_random_time/manifests/.no_puppet b/modules/generators/forensics/time/generate_random_time/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb new file mode 100644 index 000000000..6d907c3cd --- /dev/null +++ b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb @@ -0,0 +1,24 @@ +#!/usr/bin/ruby +require_relative '../../../../../../lib/objects/local_string_generator.rb' +require 'date' + +class GenerateRandomDate < StringGenerator + attr_accessor :date_start + attr_accessor :date_end + + def initialize + super + self.module_name = 'Random date generator' + self.date_start = '' + self.date_end = '' + end + + def generate + self.date_start = 0.0 if self.date_start.nil? + self.date_end = Time.now if self.date_end.nil? + random_date = Time.at(date_start.to_f + rand * (date_end.to_f - date_start.to_f)) + self.outputs << random_date + end +end + +GenerateRandomDate.new.run \ No newline at end of file diff --git a/modules/generators/forensics/time/generate_random_time/secgen_metadata.xml b/modules/generators/forensics/time/generate_random_time/secgen_metadata.xml new file mode 100644 index 000000000..09037d0ec --- /dev/null +++ b/modules/generators/forensics/time/generate_random_time/secgen_metadata.xml @@ -0,0 +1,21 @@ + + + + Generate random time + Jason Keighley + Apache v2 + Generates a random time between the given dates + + time_generator + windows + + + + + date_start + date_end + + time + \ No newline at end of file diff --git a/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml b/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml new file mode 100644 index 000000000..05e806e2a --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml @@ -0,0 +1,49 @@ + + + + + + + storage_server + + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + File contents + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + 09/23/2001 04:05:06 + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + 11/25/2002 11:12:13 + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + 10/24/2000 01:02:03 + + + + + + + diff --git a/scenarios/windows_scenario.xml b/scenarios/windows_scenario.xml index 1051e39a3..f303139a3 100644 --- a/scenarios/windows_scenario.xml +++ b/scenarios/windows_scenario.xml @@ -7,9 +7,24 @@ storage_server - + + + + C:\Users\vagrant\Desktop\Hello + + + + + + C:\Users\vagrant\Desktop\Hello + + + 11/25/2000 11:12:13 + + + From c99c12ea5ca519d967b6dd841c149e8e4d91aff6 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sat, 15 Apr 2017 20:20:06 +0100 Subject: [PATCH 06/24] Basic timestamp modules Now all timestamp modules have default randomisation. May need to fix specifying values manually. --- .../manifests/change_timestamp_creation_time.pp | 6 ------ .../manifests/change_timestamp_last_access_time.pp | 6 ------ .../manifests/change_timestamp_last_write_time.pp | 6 ------ .../change_timestamp_all_main_times/secgen_metadata.xml | 2 +- .../change_timestamp_creation_time.pp | 2 +- .../change_timestamp_creation_time/secgen_metadata.xml | 2 +- .../secgen_metadata.xml | 2 +- .../change_timestamp_last_write_time/secgen_metadata.xml | 2 +- .../time/generate_random_time/secgen_local/local.rb | 9 +++++---- .../forensic_examples/simple_timestamp_example.xml | 6 +++--- scenarios/windows_scenario.xml | 3 --- 11 files changed, 13 insertions(+), 33 deletions(-) delete mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp delete mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp delete mode 100644 modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp deleted file mode 100644 index 3eb95c607..000000000 --- a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_creation_time.pp +++ /dev/null @@ -1,6 +0,0 @@ -class change_timestamp_creation_time::change_timestamp_creation_time ($file_path, $file_time) { - exec { 'change_creation_time': - command => "$((ls ${file_path}).CreationTime = '${file_time}')", - provider => powershell, - } -} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp deleted file mode 100644 index 6c8aef1a4..000000000 --- a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_access_time.pp +++ /dev/null @@ -1,6 +0,0 @@ -class change_timestamp_last_access_time::change_timestamp_last_access_time ($file_path, $file_time) { - exec { 'change_last_access_time': - command => "$((ls ${file_path}).LastAccessTime = '${file_time}')", - provider => powershell, - } -} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp deleted file mode 100644 index 199e139de..000000000 --- a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/manifests/change_timestamp_last_write_time.pp +++ /dev/null @@ -1,6 +0,0 @@ -class change_timestamp_last_write_time::change_timestamp_last_write_time ($file_path, $file_time) { - exec { 'change_last_write_time': - command => "$((ls ${file_path}).LastWriteTime = '${file_time}')", - provider => powershell, - } -} \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml index ff4c98582..dbcbaeaf3 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml @@ -22,7 +22,7 @@ - 11/25/2000 11:12:13 + diff --git a/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp b/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp index 265827e7c..8e4ce3f8b 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp +++ b/modules/forensics/windows/timestamps/change_timestamp_creation_time/change_timestamp_creation_time.pp @@ -8,7 +8,7 @@ ensure => 'present' } -class { 'change_timestamp_last_access_time': +class { 'change_timestamp_creation_time': file_path => $creation_time_file_path, file_time => $creation_time_date } \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml index 6ed144d5c..7cc104021 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml @@ -22,7 +22,7 @@ - 11/25/2000 11:12:13 + diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml index b08cb3c1e..a879488aa 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml @@ -22,7 +22,7 @@ - 11/25/2000 11:12:13 + diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml index efb246b10..bb27159e4 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml @@ -22,7 +22,7 @@ - 11/25/2000 11:12:13 + diff --git a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb index 6d907c3cd..65e92647c 100644 --- a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb +++ b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb @@ -14,10 +14,11 @@ def initialize end def generate - self.date_start = 0.0 if self.date_start.nil? - self.date_end = Time.now if self.date_end.nil? - random_date = Time.at(date_start.to_f + rand * (date_end.to_f - date_start.to_f)) - self.outputs << random_date + # self.date_start.nil? ? self.date_start = 0.0: date_start = self.Time.parse(:date_start) + # self.date_end.nil? ? self.date_end = Time.now: date_end = self.Time.parse(:date_end) + + random_date = Time.at(self.date_start.to_f + rand * (self.date_end.to_f - self.date_start.to_f)) + self.outputs << random_date.strftime("%m/%d/%Y %H:%M:%S") end end diff --git a/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml b/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml index 05e806e2a..2a3711f74 100644 --- a/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml +++ b/scenarios/simple_examples/forensic_examples/simple_timestamp_example.xml @@ -23,7 +23,7 @@ C:\Users\vagrant\Desktop\Hello.txt - 09/23/2001 04:05:06 + @@ -31,7 +31,7 @@ C:\Users\vagrant\Desktop\Hello.txt - 11/25/2002 11:12:13 + @@ -39,7 +39,7 @@ C:\Users\vagrant\Desktop\Hello.txt - 10/24/2000 01:02:03 + diff --git a/scenarios/windows_scenario.xml b/scenarios/windows_scenario.xml index f303139a3..db36d67ee 100644 --- a/scenarios/windows_scenario.xml +++ b/scenarios/windows_scenario.xml @@ -20,9 +20,6 @@ C:\Users\vagrant\Desktop\Hello - - 11/25/2000 11:12:13 - From 6600bd1269582dace72d0462759e6e852621550e Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sat, 15 Apr 2017 20:41:19 +0100 Subject: [PATCH 07/24] Timestamp scenario module Created timestamp scenario example for all main timestamp modules --- .../simple_timestamp_example_2.xml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 scenarios/simple_examples/forensic_examples/simple_timestamp_example_2.xml diff --git a/scenarios/simple_examples/forensic_examples/simple_timestamp_example_2.xml b/scenarios/simple_examples/forensic_examples/simple_timestamp_example_2.xml new file mode 100644 index 000000000..db36d67ee --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/simple_timestamp_example_2.xml @@ -0,0 +1,28 @@ + + + + + + + storage_server + + + + + + C:\Users\vagrant\Desktop\Hello + + + + + + C:\Users\vagrant\Desktop\Hello + + + + + + + From 6fb49684e9880cb0fc8cfb9f06af3f29b5025c4a Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sun, 16 Apr 2017 10:15:05 +0100 Subject: [PATCH 08/24] Illegal image module cat Allows for the placing of cat images (represent illegal images). May have some difficulty with multiple cat images due to framework placing all base64 inputs and outputs into a single hash, this may need to be resolved for multiple modules --- lib/helpers/constants.rb | 7 +++ lib/resources/illegal_images/cats/cat1.jpg | Bin 0 -> 16059 bytes lib/resources/illegal_images/cats/cat10.jpg | Bin 0 -> 58974 bytes lib/resources/illegal_images/cats/cat11.jpg | Bin 0 -> 27725 bytes lib/resources/illegal_images/cats/cat12.jpg | Bin 0 -> 40504 bytes lib/resources/illegal_images/cats/cat13.jpg | Bin 0 -> 30643 bytes lib/resources/illegal_images/cats/cat14.jpg | Bin 0 -> 38078 bytes lib/resources/illegal_images/cats/cat2.jpg | Bin 0 -> 42271 bytes lib/resources/illegal_images/cats/cat3.jpg | Bin 0 -> 33813 bytes lib/resources/illegal_images/cats/cat4.jpg | Bin 0 -> 46432 bytes lib/resources/illegal_images/cats/cat5.jpg | Bin 0 -> 26866 bytes lib/resources/illegal_images/cats/cat7.jpg | Bin 0 -> 21030 bytes lib/resources/illegal_images/cats/cat8.jpg | Bin 0 -> 15426 bytes lib/resources/illegal_images/cats/cat9.jpg | Bin 0 -> 29924 bytes .../manifests/.no_puppet | 0 .../save_file_to_storage_module.pp | 0 .../secgen_local/local.rb | 33 +++++++++++++ .../secgen_metadata.xml | 17 +++++++ .../file_transfer_storage_module.pp | 0 .../manifests/.no_puppet | 0 .../secgen_metadata.xml | 17 +++++++ .../add_illegal_images_cats.pp | 21 +++++++++ .../add_illegal_images_cats/manifests/init.pp | 11 +++++ .../secgen_metadata.xml | 41 ++++++++++++++++ .../select_cat_image/manifests/.no_puppet | 0 .../select_cat_image/secgen_local/local.rb | 20 ++++++++ .../select_cat_image/secgen_metadata.xml | 19 ++++++++ .../select_cat_image/select_cat_image.pp | 0 .../manifests/.no_puppet | 0 .../secgen_local/local.rb | 20 ++++++++ .../select_cat_image_path/secgen_metadata.xml | 19 ++++++++ .../select_cat_image_path.pp | 0 .../secgen_local/local.rb | 4 +- .../mysql/lib/puppet/type/mysql_database.rb | 2 +- .../mysql/lib/puppet/type/mysql_grant.rb | 2 +- .../mysql/lib/puppet/type/mysql_plugin.rb | 2 +- .../mysql/lib/puppet/type/mysql_user.rb | 2 +- modules/services/unix/http/apache/Gemfile | 2 +- .../repository_managers/chocolatey/Gemfile | 2 +- .../simple_illegal_images_cats_example.xml | 34 ++++++++++++++ scenarios/windows_scenario.xml | 44 ++++++++++-------- 41 files changed, 292 insertions(+), 27 deletions(-) create mode 100644 lib/resources/illegal_images/cats/cat1.jpg create mode 100644 lib/resources/illegal_images/cats/cat10.jpg create mode 100644 lib/resources/illegal_images/cats/cat11.jpg create mode 100644 lib/resources/illegal_images/cats/cat12.jpg create mode 100644 lib/resources/illegal_images/cats/cat13.jpg create mode 100644 lib/resources/illegal_images/cats/cat14.jpg create mode 100644 lib/resources/illegal_images/cats/cat2.jpg create mode 100644 lib/resources/illegal_images/cats/cat3.jpg create mode 100644 lib/resources/illegal_images/cats/cat4.jpg create mode 100644 lib/resources/illegal_images/cats/cat5.jpg create mode 100644 lib/resources/illegal_images/cats/cat7.jpg create mode 100644 lib/resources/illegal_images/cats/cat8.jpg create mode 100644 lib/resources/illegal_images/cats/cat9.jpg create mode 100644 modules/encoders/utility/save_file_to_storage_module/manifests/.no_puppet create mode 100644 modules/encoders/utility/save_file_to_storage_module/save_file_to_storage_module.pp create mode 100644 modules/encoders/utility/save_file_to_storage_module/secgen_local/local.rb create mode 100644 modules/encoders/utility/save_file_to_storage_module/secgen_metadata.xml create mode 100644 modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/file_transfer_storage_module.pp create mode 100644 modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/manifests/.no_puppet create mode 100644 modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/secgen_metadata.xml create mode 100644 modules/forensics/windows/illegal_images/add_illegal_images_cats/add_illegal_images_cats.pp create mode 100644 modules/forensics/windows/illegal_images/add_illegal_images_cats/manifests/init.pp create mode 100644 modules/forensics/windows/illegal_images/add_illegal_images_cats/secgen_metadata.xml create mode 100644 modules/generators/forensics/illegal_images/select_cat_image/manifests/.no_puppet create mode 100644 modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb create mode 100644 modules/generators/forensics/illegal_images/select_cat_image/secgen_metadata.xml create mode 100644 modules/generators/forensics/illegal_images/select_cat_image/select_cat_image.pp create mode 100644 modules/generators/forensics/illegal_images/select_cat_image_path/manifests/.no_puppet create mode 100644 modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb create mode 100644 modules/generators/forensics/illegal_images/select_cat_image_path/secgen_metadata.xml create mode 100644 modules/generators/forensics/illegal_images/select_cat_image_path/select_cat_image_path.pp create mode 100644 scenarios/simple_examples/forensic_examples/simple_illegal_images_cats_example.xml diff --git a/lib/helpers/constants.rb b/lib/helpers/constants.rb index 150aeb487..518df596c 100644 --- a/lib/helpers/constants.rb +++ b/lib/helpers/constants.rb @@ -15,6 +15,7 @@ VULNERABILITY_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/vulnerability_metadata_schema.xsd" SERVICE_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/service_metadata_schema.xsd" UTILITY_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/utility_metadata_schema.xsd" +FORENSICS_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/forensic_metadata_schema.xsd" GENERATOR_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/generator_metadata_schema.xsd" ENCODER_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/encoder_metadata_schema.xsd" NETWORK_SCHEMA_FILE = "#{ROOT_DIR}/lib/schemas/network_metadata_schema.xsd" @@ -29,6 +30,7 @@ VULNERABILITIES_DIR = "#{MODULES_DIR}vulnerabilities/" SERVICES_DIR = "#{MODULES_DIR}services/" UTILITIES_DIR = "#{MODULES_DIR}utilities/" +FORENSICS_DIR = "#{MODULES_DIR}forensics/" GENERATORS_DIR = "#{MODULES_DIR}generators/" ENCODERS_DIR = "#{MODULES_DIR}encoders/" NETWORKS_DIR = "#{MODULES_DIR}networks/" @@ -42,10 +44,15 @@ # Path to resources WORDLISTS_DIR = "#{ROOT_DIR}/lib/resources/wordlists" IMAGES_DIR = "#{ROOT_DIR}/lib/resources/images" +URLLISTS_DIR = "#{ROOT_DIR}/lib/resources/urllists" +INTERNET_BROWSER_FILES_DIR = "#{ROOT_DIR}/lib/resources/internet_browser_files" +ILLEGAL_IMAGES_DIR = "#{ROOT_DIR}/lib/resources/illegal_images" # Path to secgen_functions puppet module SECGEN_FUNCTIONS_PUPPET_DIR = "#{MODULES_DIR}build/puppet/secgen_functions" +FILE_TRANSFER_STORAGE_MODULE_DIR = "#{ROOT_DIR}/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module" + ## PACKER CONSTANTS ## # Path to Packerfile.erb file diff --git a/lib/resources/illegal_images/cats/cat1.jpg b/lib/resources/illegal_images/cats/cat1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..921cce346f1d848378028386506e3ed13a4981c2 GIT binary patch literal 16059 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

czc|4?v!lNVN#1MUn5;3C{ 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 X^;0~ChK2$(NTdI2X=A|iw)%en8iwk_ literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat10.jpg b/lib/resources/illegal_images/cats/cat10.jpg new file mode 100644 index 0000000000000000000000000000000000000000..12d40c3a14a2ebf8fb3d7d3665f4285f2bdc4cad GIT binary patch literal 58974 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 z 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

mFXLKIU+ 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Ν#jfAtj&vphDIwrNQ7eyVq;Q#r4LqXNYz6ya|! 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≠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


5UAnefJ4T{{Z~rsBp78N?7Ew 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 jb;PalQN&JbD=W~C@_`I(nC$Q`al2KY;YBr literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat13.jpg b/lib/resources/illegal_images/cats/cat13.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9175aa549b3720d640917533ad8f656b7fd9a166 GIT binary patch literal 30643 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 literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat14.jpg b/lib/resources/illegal_images/cats/cat14.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de195b82a46a4789195b8ce5df84b072dbb3d0b2 GIT binary patch literal 38078 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�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 literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat2.jpg b/lib/resources/illegal_images/cats/cat2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..17b22c16ab7ad7890e2b2913b0bead1b9a1f684d GIT binary patch literal 42271 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 literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat3.jpg b/lib/resources/illegal_images/cats/cat3.jpg new file mode 100644 index 0000000000000000000000000000000000000000..304ae8c95cf8e29bcdc7469cf1a5e3a1f54861f4 GIT binary patch literal 33813 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 H+|U2nar@SE literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat4.jpg b/lib/resources/illegal_images/cats/cat4.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c6b5dfdd2f1e41e865f132007c07a5c55dd69ec0 GIT binary patch literal 46432 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 ?%TYqX_NjD_5z4? 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 literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat5.jpg b/lib/resources/illegal_images/cats/cat5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7ee5d83f84e5c0c138249109764283ac105181be GIT binary patch literal 26866 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

pQ$L8H^}O_svQAgQ+AYX^Q%A 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 literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat7.jpg b/lib/resources/illegal_images/cats/cat7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8c4e63baf6b2f9bf7a273a5abdad5b2c19676825 GIT binary patch literal 21030 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 z zvy!?h`bo<8isA;Pg|2_%zUz^FAoO}>;6e{c5-3ytt-$xBEBr5C_WX4a%d8^KdKt~_ z)LKnhOZ+=FQ(S|+RSW#4bfUqY#c%)x0F4z|y+^_f8q~iB-DAs;Rv&I5?Fyi926KfZ z=NfWBdp?#mp#Bpj+2LcU;|xGvJWXhZo?Zzkc(?;F@+A1cJjqSX#EpcQx_E!rGA69MeRBv-q z>$PjxPN(r%88^)L9my*`5xIFQ`Yj<=e8TWp`ayYAOZr<7A*MW`0#SIlDz+iVBq638 zBbG%GjJU5Qdb}?aqTuK_4?h- z=UvA3^c(m?6(u)4)1Kk3)=aIW=Yhk zvbMNWQ(4o)Yk=#9Pw{HEKycsH$P`~m%&4hn0^0Y^J209KWd8a_BYy2}4yMo#gb_yD$%G{sCB82ZRlh$>tUpGhc-;`Vf)$@1%7*2XT~l8|u#sRPTEi z#HD;S_{C{e+3JiN89CXH0W;_{C3bDO&c{+U_dtLhcnd5y8HP~vUKV6#f1{drEtH=v zP7Zk#G7-%Re+n{{b{em36CwVb28{k+Xqh`=}EYZ59p#%yMK(>N>M59!^cEV%v=t zRY7FA1!<`T)l=$k?H-Krz=F2czB>_^6dOL9i=U%X*Gmd6R~$I|zt592w$6D=d~Qsm zlya23D1;n@-3e*FS_q%roH@cQ<4w}(>6rqa=o}j82l5)*>8+}c+{^eJWpf59S3+Sk zIPbL8MF-%NwBDe7o0u4A_=YTE#q{;MQCLp1CZR4=A~E|S(sIvTdN2y)03qUmye{!=&WY9>!vG9Qp8oXD+03Lcv=e5|kgs|g=5Tnc;zcB$OVvpE_W?OAl|BSTcgfSUcQ({dF(6+WEyhZ@q2f?d_cic>c=9Jvi7VGX2Z;vVqPUWEw*+I=mQcQJ4jRv#I z#>&Dp(j%t?*|7p%kCAab8+T^Ar=9Y1>l&+_f?NEC)=SO-xE}uiZd(F>-nx=i)qJ@p z67mS~J{gO_z<4%1lnxp9Fx1V=SXW-~UW`4KHhhdU9voO-nlt1wGyKFNjmv}^_o3)D z=3k1hGO+F|$7<_C;dbaG2HN*Cqi(n}A6mxb@?3vh<;IAe8>axNm)I0Lp2Fvn2}+xk zT`UH$;Ge2j`v^{ZeB34&DV{hsINC)AUrwK<&sYa}Sk7O=6FEB~i5*kZodcqPBXu4g zHAK%IF+kFaH)lR-I>*cfv5Pj5tCKcN;e)2{V3!q}XCW$HdmrdUmA2{e2E=5-q)KovQcyS}LRBjI;@Fp?V3%CgRn&Y5_fksfTb$DF za&LdwW*HTJ@9;pzdACbZduL!{<-qdVy?sh*rb=GW;iBt6(>AEi^+;ajG3R4*;_D0I za%RL_*Qu+eaB^c7^zDs_Yz1w|=w)bBtLeb{)jBKx7?{_r?{ZOYS{2WO-X^~lh?g~! z=w#8iUHMy zrMAt?EzQ)Px3NoXsQhn7VK8)rawz&xn&W-x;VO=?B94m-0k7nt$B+EGp+B#= zn@zV{Mpv3ZkB+}{752wB_uC@SpT~;eMrP(H?@0wSU#XW0!Al*4uk} za+aiMr!Mm35mCKmFl8Lo#f0?0Xsyia=Pvnz*tx>GxClx^{PiKcs>qve3w4})LvP%J4nkui^dIFKFk6={3xhtvQ+Z< z{kC3D;!`KOi5hNm%ovGI3uIo3rMxdQo{#84FKfi`$nVfQd)3bisGVuTu_hqtG3ZCx_BMv#dUb&IqMQ zH{|yd4K7&@Z#7X|w5@yQd)-6Xb|*_<%GK__hP3I&tWk#2+Lwd3{1Nl!Zk{tkU}>;o zm%|aQk3Hq@?H}%>4fRP0_T|#dS%TJXSwSC-d~g>sZc?Q`Y+b1+kNPeb74$fF6!wnB zzPlfAap+0RPaQRdqoZToS@=jUYyGHl__4$Xn4R`x!$A4NJ9S>LGbU>Yhug-P4dX(7 ze!sn?F#h#$rqZPjWejP1Ji@9(a`?pZEy3>W+u%RI?9Z=9)mAl5%Q{=tY-fwVSBKJt z))i@jWjECZkx!79kOj}`TP|esFbtEg@)CQc5GS6Rj$qnE4~G-hjoIrHxRNQ41>}oE z%zMD2_Uwy$_vd$pMM=yG&S{I6(83#GMP}aqyr1eG8`K}RtQqR!e}!F{atjk7MEM&A zeYc;&U^7Oh$7tM2rx`8HXZ>~*4^7t{vd)}AGkLzcJf~{Bc~CnYb-TayYIfiO_qfyf zp1HC?Whnd)FM0V7-q{J8^7895`YKixHG49+*>C;vRJtiFaA1u4k^aLH3rV> zgp*gVMy=F2Vv*zmG&mrHZ}G7p{3DL^06)24R&nlfKr4&1@QAh&_IGK+v}nlR>RDEW z43^&+T42w2*m<3E#l~tDN9l!n?HtQ`$$v+ZV+NUE9hA$uNw9Vzv%$n~>M0oxA1$jv z{)XAUD$~2=tWhzv#PTZBIz74(>Eaq#APsBXB1itd56rBnjnJ3%lf75{4K|576%lec zbqSgpUX&bzQ6`OfM!3NyNoqv$Oa_(1Vf(U9Dl)msz5L{8`uwUhxumRmaH)mVU{nMd zmsgMc7MMPwU7Ww|t24wl^uT7Wl$Y}K!^W}|J#GPs(5q9|{cx$U%NjGhojtcF5jN|Z zgWSc%7>8LAr;oOIUeCw!&Rqt^XZll~UlL$apuuh4OzXDQ1pHvUtTLkfrb0;hkyd_5 zhXTEqYA}5aSstG9k?iC~qj^E((QB%u*X%nt8(PNFZ|`mEmjb<8ZM%x}F{^GSgrETO zHtLwvp#1O`gmDITe!FUGbMqz9*|tO(S#F78*j4-C1-JY|o+mQqUmP_fL-wn@TC5Y) zq5jFlhq$oa?Ytggk{h3M^G$>@Z~uvn{)o!&Ob+G2JilbS`CZ}EwFfl|mJ`e7fw=|l z&*Ive;3+^Weh)q%?;oH6vHh4-#k^F6wUZXRnkQVroF!)G)sC|4+C?R@`VLI3<=iy5 z?&1}5l*K_DR-pIN_#z6?&~qIrdg-_Ey)ATU$a79Ga}EMO9xy;nRm9HzBD#(8~H0@S1^ z_qDJP4Vu(G3q5u={W3I#LyLi=o>>0TZ?oFeZ3Mxg+*5ifNgrI1vc+G@1sJu_J@h4* zTU}PE!OivOV`oavC*rK7z8A*o9-v13~)wVkD3=IUAKDm1-5t5NaQ4gi;NB!6+hu6XU^s-&d| zv*F?xt?WKBvh6Fmx?&^xZ1@%H;xw9}Smj6UtGM~7Ped7x0#!}D#?b!JFC{x;36g9= z)#Lt`c}qzgRG2fP)_RJoyM!5~2$Fv5-gOENWWxZ=s5+WHs>UDJE}U&S1quw3QqjUz z%VbDzu@tLHel%mV7=V>`Rch?g|D*PTu)kcSJm_B}Tqf3jE;%A^6yxls znqtr%Qr=r)x-*jHxA5m&wjC>gX6meY@MKZ{Lycd) z@`(uD21nTE<~)qE(g99YdhQhX!~}#{*c~pq&67~L&-P4CBQ1Gg1EKyJsI;%I4$?yJ z0kpb&YaTxlqe~B$eF;0}hfEe_bzW1hg_!=SPg2c%`xACNDlC_iZ` zQ&oO90`Dv!=EgLWO*iOd1|KmFBznD6F=+1QMSCWt7ZY<#mrG^*)D5Ntj|N9PYax3` z)Va3-mUC4=37wcI)A?*?%9O<~=`mOONO?F8TLu8E|6DBx<|kDhI#n$*nOVKwN}QxQ z6KHQ|od4!N8AHitbmG?(`zryoo{Qeg4-=wQ#>p0q`2ckU2Dqv&^r$ZTx|~%@7^!S< zq1JITF^%q;2e=B*J4;PN0)bL?KEzLA#&JM55PvB!RaQI23mYZhB zd(Soi6Lp^x^`A z-F*Ll00I35l$-LmZ#%DjK*a?*68}A?@ke<~o!EA(qaBt!IGRuw95t=Z+4YP$m(?@w zNMgTz+aMs(3Kpa;lc_5(;y&(LE0Z~WwlO4p5pJi(_D^-H;n&fuxf_PZZivI7L=MYCWNsiDE0>T)YKpFq+Ze^i^n? zB&KX-%G#&7Fq6W`)yE}5wzM0@o3bZ0Uq-DzlG-ni(w}`d7Tb!srf!&C$=;Ak^0S31 zLmqxSsmn^yReALOeo~_F`c}A$Kd$)G&cv;7&-h{gA&aB(f&GJwnsnvI4yLNVSAWN3 zrC!E*w^v3Nxx$h8Z1}KSQnuz#?cbB_BeNeY6r8@lXtzaS7G`@VyOs6r-$hm8!XflWvR2)Hcx|-`uqjT zn6nUP_pb0!PJQUU8>OgEjq};J{kw7Tbl#G~D=w8k+T%NfsA&Nr=mPw|LP#gA1O=YH zMufrW##vE662pHO5Hqj$X-!c+{V867`eWxcjF2Pg8pZwccZRwmx&J^@*jwp%r!ewP zTc3MjR#N?^3BGb;?+;gu_Ni$94k9UsrIhQm@ZayNHY(b*XB86Tt1gYicL!2%9o(1e zhHRrt31EVD7ZAtX9NR-f-JfL@%FWGY)!|x;Dx0D!Y$wyBlAaNpTS2}!v)Y*$p8GntET9r93Xj(Go-Q9D0C^Z zO}Pq}UgDoyh=6TS#>i3m_7g}xAC}T-#<2|}#D5hb$i{%b?8RsAJf&fsuOynl=fXm1 zD*pfqpkv!2B)8Uo0Ir0tnL)X++$Sclio$lV%Ocs@FY80DijthqPhm_=r=!LLsvI&X ziJB712zALGr^q!VYBASW(FVnd5}lcdRi<@^Xc$zW1@SI2S>>!=MF`M8ynj~vk#lU- zmMX^&ZLQMza{q`!Y+ZuYrh@dV6*Y`Jf~INRnK4ttvr29%rNh0R&ywqXeA&?BrS*}5 zZSwCLZ%j4M#_`$EGIR%7J!FGdu_!n19+Pw4J;o#ZDQK;g01_I8=5;7(9^)f)UKJI; zTqKis+_1A?NzWj$OkV1c)3M02Yh&x;^2(^a+TK-sVcR97*r(%o?a{kVoR_*ER1%J| z=3evdfrbC<4!q-j8H=WJ5^bpLtC`Y23@-D(B%#m2uDo*_^=<&3kt(-2K1PI>I`_i_ z1ZoDj_;0w7K><4LI6Uh>67iGED3t|+PbFg-k~O#nIfB7#aQBo%Q-^gvQt(#-tO%Dj zHH9AbUhtcjG!n0qXjGUuh*Vb=!N7kRjizhQgk6;PpvNZum|QMga-fG@&L(58rxSp-cE>H+>ygU)8Dc>rUTtut1p;wFv>IaO-G;-o8*qLx_H^mXXZc zHBEI!+ila}auNG+E$GnS2^oTa0H&wR@FhA<8QD=app7@b8Xdhg0QJtKCQE;n=jxHC zI;Fv$+lvutV2U}yOj6VtX+&PxeL~Tg+M;xQu)N;p_Y|B2w^^% zxgC`6>&AWGZW~&nd7h}&b@9*%;y9hOZOxKU73inHh`>M`A$;ShIJNcU4)Ig>K8osk zpxk0dyYR(#&Y>qG&0V?Fpgd_8`gD{)zpf@!?Abdo3zrhLCGCsjSf?%}VMrPUKZp~2 zclyNfvO|s!iF_rBE}FXe)3@hL@=rZe=~*F)BuBoKi)#R$`2MI2{M-Cm;D>p9C+g{^ z!f@5A)@~WYL~<&%+bJy{fG<7Iy+@c=|-oqx-FkCM<*U1dNb z%CG9>%(SCoIYy*-(n)sk$+CZOX6nv}_qI*lYTf)#07DVH?j4R)HWQ8!dPf{96bha| zOt12U$nPl3N5N6(p9MC8_XU@`$v|HJ03|rWal(TM@TP!y1Q)x>LSF3hQ;aU~gu?F# z=nce|JM|PL#~hUN3%^PO3%nF{2I32kl7y3P0Hk-C<~93XV8Cw zguw68j-cGEN(Vb-WzavuQ=xZ-H3sEpyTU`Viy(CUmWQpDs)EktfDx=TLuE`U!c2%7T)Q+Isy&AN@aDpg)>g;s?0CaRM z*sNM`l;)6D)KX+{RGJL`07*#TGMhC`Bqp`xTQmfJN`XY+ew3P`#U?GUIOQ-qWY*H0 zeJRbR+k`bkTPuem1zQx~WNoJVAxJjLfS$;KJ_@)glN(Mjr=%*%AUoX(rD=kkT{HOR_{avbIk{Va3$xO!%7P6|`*8}pb*-~bRCXXqWigq2rN%(nlzE^)& zYHm-;wKCg-UBcz%xSJsq(azJsFqm#z?~=P~H572cQ0Zay<6~VjohZ@+@sY4R5BLjC z+As*l0rI-upSJpO%;`WTXhQ*R*luu7DRJa&G{Ke>tm1C8@``U-V)E%mLkn^N2XD8s zy9di>1)nP%&7G?tk&XwBQ!e)DYc9f-l43@eXwmZ*w>urq{xF$xvq@*SH{xe-I1VHc zu=_3F@JQyr+|LE-iaBWd!YY@v`eEZF`~8wDsKqq0L>zR9HUwj3)?Y1B-5&bCPEUk^ z*)IU;1E03*pNXQfq(ojMZ~?6u$=hYv>7}+b&Te??GqC8W3=y=*PBXSn-@@u;X@S>2r37rnljhF-pdBk`BwexAfF^ia&^r*(@0&B!4RgSuNXUs4q%7dnczg z0Bc;)9zhxTv610!qo8SaKm+U+m!(?P387?!HKax|cH5EVb^03-06QbeL!*^!zU8@A z$uTg{vAl(Kl*|DvN&wv-t})8!DVbabdQA&GCMA48@)R0@{s(_Wr+k@0w&=l(m5!~;8iWM)QjyS2*C?n*`1D41g;~Lj!blm z;ll`nnziCbc~h<4N?T#0VjeuH5)Uh09(K2Wi!*ohnE5S632TI`=HK{@3?k~Sh{ z`3gks^0nZi$`%?n9}wRvej&b90Cq+S78)-6Ls5K7ebo?%AjU3yNK^44R0>jzi(e8v zAbi2Ygq4X{L{8A)c3OIv@J`Em=Hx0>5ab<}e3;WMgxWMX!?IhjJLNit^C-EsjC^p? zmS#>tJ;6Rk-`!12AVly94j_SmxVnna%II`Gy4+mj9!SeT(%^mn0O4oHkJUlXKS|Ks zYvvtaPj;gXoz&6^9}zl4~ty{bpMP6(O^Ty+<;r=qzX?V1t3Ta;|jePg?Gl z2yRr6#Z^foNBDuqAnY*jvCebhah-Y5b4y6)#`iZPEyl!czT?gnvXnTCofx8|?on29 zeSg;$Xl%!!I${{>o$KnZxf7PX!-hsS*d9sP;dz}CYN_jtjx7$2*&0CQb+p|X9k!Z# z?b~Wvf0#6%smF%nzU!U0hPLV_oEJkW`Pv5KKFhn)I4o&H!OH3sobG;4PO^1vqW&Sb z-h?*V#y1VA84=6(95k-Bp8ZE|f@)hs*;Ok>mdwl;@<#bN-wVv2(q=%(U;)v*a5K8~ z9*XG})4?2tjv9Bvo3Z_uFROT?ExrB?xE4u9y^7YF$51fEI3rubgRzbCy6>Z%(nQy5 zCvNLa)fDvfPo$!052>0)xUwdJ>9j4hWC)rG?4PE5HZ+Foca@smfSA9^`$~J*a+(hMaH*U2tPStWcf#nU+a*H;xm1;LFRCOU* zj-(}dA~J(EtSZA@verjZQ`=BdJdq!^%Z_qUJEa*(?4whUWmK}F*yCfH%4j|k8XkKm zHIF@&Pb^CoCNLC1Lt}biiRm6>I>g9EFt9@Mm?_`xmFGh&OcBD7%3!B@F-(2ZyyJF- zgh4b?p9L^d9|Z@Tb%d@8JcVFV9|*-6&k0{p9?4rOK~}{X@QhLWBz;30vQr1joU5XY z=Y(R9{Hb1cGQx@(_9}PqR>V>JA{1W9d+ct~HMl+!o_%V+!#V`mo_>W->OH;!E>>;eY*wbmdL7ER@tiw6z+B$^R|=wc1P5<=p*VmwK__P zBbl)h_Wd{*BL{+Uy68G;&Mvib+nDJF0mhbtjQJ_!pA1eb(!Wj8rC{Ik&dH_~Jb zr;4TzN_7p>s5*XXN=~M%oy=|?W+XkJfA5vc>M=rZyX4)5a!a;)G1!+n*b44z88W1(}b^ySXhOk*d`%W3&{vZ-3|ZAvK|=bS}pn&eMO&3rr78fW<{rTn;fSZ%|?o}fKWVADVDgwsFWbsv`x2u-+SgndA` zoWH|A!jFb`y6QhIZ-pP0kJ61u7ZaBFS9nqIuJF2_%Uj_O<;~+tl^|SBSkk+~6H0t8 zq}!X}3AZ<#BT@y#W#U+0t0Sn-%2$gTuxckr@~XIPleSwfuL{_okHIM z6r?T0aRJ zRbNf)x``=U;3|?+H`y|a1Clw3X^(|QN`7`-T(r#gRAr`S>C{HSa;lUWY-Eb;r_W^eQs=U@8(W2DHn%Fbjg~1DW2FB8 zDzeHyWo?1J6^3c>mE#>urpiBL=F$5rtkdB+x_l+=jHXZ&UdfH39u~l9@Q5AwLqP6P z6l20;Xvc+g5Tq0|MBSBJMm!@FgTk^3Amubc-Ic{4@QhLq3cxv8l{7)!iWH;5F-$xJ z!0pj$*-a5QV#Par1VRS;Bg&kist)WC15}7x!YrxEnxbsT5Vyck_*z@KJxE-3QN;yG zlxiUXgazi8nt3{7sN}xEPw@~|$k--?pR0mMCt&S~8Qbi=Ot;m@C2H>W8e83(S_uf< z;zsHl7Yyuum0GyFLCBQjO!Hcvj{H07OPrUBjVz{l1`8zB6piun_xq0vyN{AMVU^F@`g5z{y+awjPX#tWbDq}98;pFwIBZEBmr`cx3sp#M z4HYe1&ms*2Y-K_u>QA;V#(Opp5<2>2(dplm7rFPt!E*qPN4fw)fLi!Yd&INR5$~Y6Hqm zHL=Cwp2ucw3n#&vr%r$wo*FxqzNzWx>M9|W)=i~{=Z-O z?#R#qdzELo8{Vr(^imbDBzG#Lj!cpM>a330IzqN8M=FxPl-VAtO7zJJ*qL3jr7P2= ze=3x({P$FSPjuEvzbclCV$GCqR%3eCM*jeHW;gJamL_ai{uPG3YuNlNEP7Uk zOupBrHOiA7oY(kB(CL>N?&*Qbs~@s+T$P~M;86!9ha$=p5rE|oDL|qH3Jr}12!Tok z5GVv@RDzZqi&BISDs>x-u)E-$kmarWcu?{&o>UE+t06)VWI6dk$Jr00_q$Np4!JcQJEk|*Bk5Mg zD9<@NpxaCU%boly4PD^#I&|35<4kNf0>hU6kf9G$u$^ z(9q;LS(OB)5G<(59H=LBNMSo6C1EIXp$W#WT z3Bo*|D;6r`TLS?(PL0K4#Y6I1i&40%QMjyFm5y0qI93==RxChtSYwKN30Sc!9S&(Y zi8Rw}<0V!Z9O(?6G207F>y-eE8iTs^Odfv0~RB1}XBzNmD4` zfQ6MZ`3n{!e2O$rA#W+sxSmSIiGass>B@u3#fgL?Dl)NRgan_v0`swSyn<-NlDbP nVq_swLRCo#Sg|qzN>Xu^MpqryEKGzcOil`-@^vg&iXs2m=49PA literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat8.jpg b/lib/resources/illegal_images/cats/cat8.jpg new file mode 100644 index 0000000000000000000000000000000000000000..836e92e13dd4ee3a011eca5d3e62b5b148a9c684 GIT binary patch literal 15426 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 literal 0 HcmV?d00001 diff --git a/lib/resources/illegal_images/cats/cat9.jpg b/lib/resources/illegal_images/cats/cat9.jpg new file mode 100644 index 0000000000000000000000000000000000000000..766a029b837d7919727b48de7dc8e50e7879f987 GIT binary patch literal 29924 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 d)@wCfz0mY`*N3*VS*alj + + + Save file to storage module + Jason Keighley + Apache v2 + Saves the given file to the storage module be used in transfer via the file { source => puppet:/// } puppet resource + + utility + windows + + file_path + + file_path + diff --git a/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/file_transfer_storage_module.pp b/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/file_transfer_storage_module.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/manifests/.no_puppet b/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/secgen_metadata.xml b/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/secgen_metadata.xml new file mode 100644 index 000000000..c43f4ed2b --- /dev/null +++ b/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + Store files for transfer + Jason Keighley + Apache v2 + Store files to be grabbed via the file { source => puppet:/// } resource to avoid file transfer issues via the source and content tags + + file_transfer_storage + windows + + + + + \ No newline at end of file diff --git a/modules/forensics/windows/illegal_images/add_illegal_images_cats/add_illegal_images_cats.pp b/modules/forensics/windows/illegal_images/add_illegal_images_cats/add_illegal_images_cats.pp new file mode 100644 index 000000000..985f51594 --- /dev/null +++ b/modules/forensics/windows/illegal_images/add_illegal_images_cats/add_illegal_images_cats.pp @@ -0,0 +1,21 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$image_path=$secgen_parameters['image_path'][0] +$original_image_filename=$secgen_parameters['original_image_filename'][0] + +class { 'add_illegal_images_cats': + image_path => $image_path, + image_binary => $original_image_filename +} + +# $image_binary = base64('decode', $image_binary) + +# file { 'add_cat_image': +# path => "$image_path", +# ensure => 'file', +# # content => $image_binary, +# # content => base64('decode', $image_binary), +# # source => '/media/user/3TB_internal_drive/Documents/SecGen/lib/resources/internet_browser_files/chrome_history_file.source', +# # source => 'puppet:///modules/add_illegal_images_cats/chrome_history_file.source' +# source => "puppet:///modules/file_transfer_storage_module/$original_image_filename" +# } \ No newline at end of file diff --git a/modules/forensics/windows/illegal_images/add_illegal_images_cats/manifests/init.pp b/modules/forensics/windows/illegal_images/add_illegal_images_cats/manifests/init.pp new file mode 100644 index 000000000..1b7498f9a --- /dev/null +++ b/modules/forensics/windows/illegal_images/add_illegal_images_cats/manifests/init.pp @@ -0,0 +1,11 @@ +class add_illegal_images_cats ($image_path, $image_binary) { + file { 'add_cat_image': + path => "$image_path", + ensure => 'file', + # content => $image_binary, + # content => base64('decode', $image_binary), + # source => '/media/user/3TB_internal_drive/Documents/SecGen/lib/resources/internet_browser_files/chrome_history_file.source', + # source => 'puppet:///modules/add_illegal_images_cats/chrome_history_file.source' + source => "puppet:///modules/file_transfer_storage_module/$original_image_filename" + } +} \ No newline at end of file diff --git a/modules/forensics/windows/illegal_images/add_illegal_images_cats/secgen_metadata.xml b/modules/forensics/windows/illegal_images/add_illegal_images_cats/secgen_metadata.xml new file mode 100644 index 000000000..24706de7b --- /dev/null +++ b/modules/forensics/windows/illegal_images/add_illegal_images_cats/secgen_metadata.xml @@ -0,0 +1,41 @@ + + + + Add illegal images cats + Jason Keighley + Apache v2 + Add illegal cat images (simulation of illegal images) + + illegal_images + windows + + + + + image_path + + + C:\Users\vagrant\Desktop\Hello.jpg + + + + + + + + + + + + + + + + + + Store files for transfer + + + \ No newline at end of file diff --git a/modules/generators/forensics/illegal_images/select_cat_image/manifests/.no_puppet b/modules/generators/forensics/illegal_images/select_cat_image/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb b/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb new file mode 100644 index 000000000..710506cc1 --- /dev/null +++ b/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb @@ -0,0 +1,20 @@ +#!/usr/bin/ruby +require_relative '../../../../../../lib/objects/local_string_generator.rb' +require 'date' + +class GenerateRandomDate < StringGenerator + attr_accessor :selected_image_path + + def initialize + super + self.module_name = 'Random cat image selector' + self.selected_image_path = Dir["#{ILLEGAL_IMAGES_DIR}/cats/*"].sample + end + + def generate + file_contents = File.binread(self.selected_image_path) + self.outputs << Base64.strict_encode64(file_contents) + end +end + +GenerateRandomDate.new.run \ No newline at end of file diff --git a/modules/generators/forensics/illegal_images/select_cat_image/secgen_metadata.xml b/modules/generators/forensics/illegal_images/select_cat_image/secgen_metadata.xml new file mode 100644 index 000000000..f081f02e7 --- /dev/null +++ b/modules/generators/forensics/illegal_images/select_cat_image/secgen_metadata.xml @@ -0,0 +1,19 @@ + + + + Select illegal image cat + Jason Keighley + Apache v2 + Selects an illegal image (cat image) to use + + illegal_image + illegal_image_generator + illegal_image_selector + windows + + + + illegal_image + \ No newline at end of file diff --git a/modules/generators/forensics/illegal_images/select_cat_image/select_cat_image.pp b/modules/generators/forensics/illegal_images/select_cat_image/select_cat_image.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/illegal_images/select_cat_image_path/manifests/.no_puppet b/modules/generators/forensics/illegal_images/select_cat_image_path/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb new file mode 100644 index 000000000..f42d48b11 --- /dev/null +++ b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb @@ -0,0 +1,20 @@ +#!/usr/bin/ruby +require_relative '../../../../../../lib/objects/local_string_generator.rb' +require 'date' + +class GenerateRandomDate < StringGenerator + attr_accessor :selected_image_path + + def initialize + super + self.module_name = 'Random cat image selector' + self.selected_image_path = Dir["#{ILLEGAL_IMAGES_DIR}/cats/*"].sample + end + + def generate + selected_image_path = Base64.strict_encode64(self.selected_image_path) + self.outputs << selected_image_path + end +end + +GenerateRandomDate.new.run \ No newline at end of file diff --git a/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_metadata.xml b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_metadata.xml new file mode 100644 index 000000000..49ea41934 --- /dev/null +++ b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_metadata.xml @@ -0,0 +1,19 @@ + + + + Select illegal image cat paths + Jason Keighley + Apache v2 + Selects an illegal image (cat image) path to use + + illegal_image + illegal_image_generator + illegal_image_selector + windows + + + + illegal_image + \ No newline at end of file diff --git a/modules/generators/forensics/illegal_images/select_cat_image_path/select_cat_image_path.pp b/modules/generators/forensics/illegal_images/select_cat_image_path/select_cat_image_path.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb index 65e92647c..094e37b63 100644 --- a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb +++ b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb @@ -14,8 +14,8 @@ def initialize end def generate - # self.date_start.nil? ? self.date_start = 0.0: date_start = self.Time.parse(:date_start) - # self.date_end.nil? ? self.date_end = Time.now: date_end = self.Time.parse(:date_end) + self.date_start.nil? ? self.date_start = 0.0: date_start = self.Time.parse(:date_start) + self.date_end.nil? ? self.date_end = Time.now: date_end = self.Time.parse(:date_end) random_date = Time.at(self.date_start.to_f + rand * (self.date_end.to_f - self.date_start.to_f)) self.outputs << random_date.strftime("%m/%d/%Y %H:%M:%S") diff --git a/modules/services/unix/database/mysql/lib/puppet/type/mysql_database.rb b/modules/services/unix/database/mysql/lib/puppet/type/mysql_database.rb index 1f94d5f88..53f7f91e1 100644 --- a/modules/services/unix/database/mysql/lib/puppet/type/mysql_database.rb +++ b/modules/services/unix/database/mysql/lib/puppet/type/mysql_database.rb @@ -3,7 +3,7 @@ ensurable - autorequire(:file) { '/root/.my.cnf' } + autorequire(:utility) { '/root/.my.cnf' } autorequire(:class) { 'mysql::server' } newparam(:name, :namevar => true) do diff --git a/modules/services/unix/database/mysql/lib/puppet/type/mysql_grant.rb b/modules/services/unix/database/mysql/lib/puppet/type/mysql_grant.rb index 999100a0c..b7d591d5f 100644 --- a/modules/services/unix/database/mysql/lib/puppet/type/mysql_grant.rb +++ b/modules/services/unix/database/mysql/lib/puppet/type/mysql_grant.rb @@ -3,7 +3,7 @@ @doc = "Manage a MySQL user's rights." ensurable - autorequire(:file) { '/root/.my.cnf' } + autorequire(:utility) { '/root/.my.cnf' } autorequire(:mysql_user) { self[:user] } def initialize(*args) diff --git a/modules/services/unix/database/mysql/lib/puppet/type/mysql_plugin.rb b/modules/services/unix/database/mysql/lib/puppet/type/mysql_plugin.rb index e8279209f..9c97cd089 100644 --- a/modules/services/unix/database/mysql/lib/puppet/type/mysql_plugin.rb +++ b/modules/services/unix/database/mysql/lib/puppet/type/mysql_plugin.rb @@ -3,7 +3,7 @@ ensurable - autorequire(:file) { '/root/.my.cnf' } + autorequire(:utility) { '/root/.my.cnf' } newparam(:name, :namevar => true) do desc 'The name of the MySQL plugin to manage.' diff --git a/modules/services/unix/database/mysql/lib/puppet/type/mysql_user.rb b/modules/services/unix/database/mysql/lib/puppet/type/mysql_user.rb index 94f36858b..8c720b98e 100644 --- a/modules/services/unix/database/mysql/lib/puppet/type/mysql_user.rb +++ b/modules/services/unix/database/mysql/lib/puppet/type/mysql_user.rb @@ -4,7 +4,7 @@ ensurable - autorequire(:file) { '/root/.my.cnf' } + autorequire(:utility) { '/root/.my.cnf' } autorequire(:class) { 'mysql::server' } newparam(:name, :namevar => true) do diff --git a/modules/services/unix/http/apache/Gemfile b/modules/services/unix/http/apache/Gemfile index 3d46720d2..839cfc68a 100644 --- a/modules/services/unix/http/apache/Gemfile +++ b/modules/services/unix/http/apache/Gemfile @@ -7,7 +7,7 @@ def gem_type(place_or_version) if place_or_version =~ /^git:/ :git elsif place_or_version =~ /^file:/ - :file + :utility else :gem end diff --git a/modules/utilities/windows/repository_managers/chocolatey/Gemfile b/modules/utilities/windows/repository_managers/chocolatey/Gemfile index 0c629e701..d8669c00e 100644 --- a/modules/utilities/windows/repository_managers/chocolatey/Gemfile +++ b/modules/utilities/windows/repository_managers/chocolatey/Gemfile @@ -7,7 +7,7 @@ def gem_type(place_or_version) if place_or_version =~ /^git:/ :git elsif place_or_version =~ /^file:/ - :file + :utility else :gem end diff --git a/scenarios/simple_examples/forensic_examples/simple_illegal_images_cats_example.xml b/scenarios/simple_examples/forensic_examples/simple_illegal_images_cats_example.xml new file mode 100644 index 000000000..8dfcacaa7 --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/simple_illegal_images_cats_example.xml @@ -0,0 +1,34 @@ + + + + + + + storage_server + + + + + + C:\Users\vagrant\Desktop\Hello.jpg + + + + + + + + + + + + + + + + + + + diff --git a/scenarios/windows_scenario.xml b/scenarios/windows_scenario.xml index db36d67ee..7721bae8c 100644 --- a/scenarios/windows_scenario.xml +++ b/scenarios/windows_scenario.xml @@ -1,28 +1,34 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.github/cliffe/SecGen/scenario"> - - - storage_server - + + + storage_server + - - - - C:\Users\vagrant\Desktop\Hello - - + + + + C:\Users\vagrant\Desktop\Hello.jpg + + - - - C:\Users\vagrant\Desktop\Hello - - + + + + + - - + + + + + + + + From 1649f397569d2337ba21f58230bb7a605b6e8a3b Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sun, 16 Apr 2017 17:55:02 +0100 Subject: [PATCH 09/24] Chrome Internet history module Allows for the insertion of the chrome History file with choice of number of generic and cybercrime urls with inputted time range. --- Gemfile.lock | 4 +- lib/output/xml_scenario_generator.rb | 6 +- .../chrome_history_file.source | Bin 0 -> 94208 bytes lib/resources/urllists/cybercrime_urls | 129 ++++ lib/resources/urllists/generic_urls | 615 +++++++++++++++++- lib/templates/Puppetfile.erb | 2 +- lib/templates/Vagrantfile.erb | 11 +- .../internet_history_chrome.pp | 61 +- .../internet_history_chrome/manifests/init.pp | 12 +- .../secgen_metadata.xml | 44 +- .../templates/insert_history.erb | 33 +- .../secgen_local/local.rb | 139 +++- .../secgen_metadata.xml | 10 +- .../templates/History.source | Bin 0 -> 94208 bytes .../url_generator/secgen_local/local.rb | 64 +- .../puppetlabs_powershell_local.pp | 0 .../secgen_metadata.xml | 17 + .../google_chrome/google_chrome.pp | 3 +- .../google_chrome/manifests/configure.pp | 14 +- scenarios/windows_scenario.xml | 19 +- 20 files changed, 1030 insertions(+), 153 deletions(-) create mode 100644 lib/resources/internet_browser_files/chrome_history_file.source create mode 100644 lib/resources/urllists/cybercrime_urls create mode 100644 modules/generators/forensics/internet_artifacts/chrome_history_file_generator/templates/History.source create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/puppetlabs_powershell_local.pp create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml diff --git a/Gemfile.lock b/Gemfile.lock index 3c4eb86d2..d4f963b42 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -55,8 +55,8 @@ GEM semantic_puppet (0.1.3) spidr (0.6.0) nokogiri (~> 1.3) - sshkey (1.9.0) sqlite3 (1.3.13) + sshkey (1.9.0) thor (0.19.1) wordlist (0.1.1) spidr (~> 0.2) @@ -76,9 +76,9 @@ DEPENDENCIES rake rdoc redcarpet - sqlite3 rmagick rqrcode + sqlite3 sshkey wordlist yard diff --git a/lib/output/xml_scenario_generator.rb b/lib/output/xml_scenario_generator.rb index 4ae6f7d56..8b1942b6e 100644 --- a/lib/output/xml_scenario_generator.rb +++ b/lib/output/xml_scenario_generator.rb @@ -72,11 +72,7 @@ def module_element(selected_module, xml) } when 'forensic' xml.forensic(selected_module.attributes_for_scenario_output) { - selected_module.received_inputs.each do |key,value| - xml.input({"into" => key}) { - xml.value value - } - end + insert_inputs_and_values(selected_module,xml) } when 'network' xml.network(selected_module.attributes_for_scenario_output) diff --git a/lib/resources/internet_browser_files/chrome_history_file.source b/lib/resources/internet_browser_files/chrome_history_file.source new file mode 100644 index 0000000000000000000000000000000000000000..85df9987b1451ccb88e729e94fff796bc130dc9b GIT binary patch literal 94208 zcmeI(U2oe|7{Kwk>5^`3mv&ocO{1F8gj$PC&{YMbT|l>GW1?$W>Ch%XmfN_gMG_a= z>6p06fGfTO-v#cFkdXKU+;D>g2ywGZ&hbm)U}xnL_536CB|azTJkRg3kL{%W;K7C) zI>x@|ckIwuEzOlQt@Mszlu9K{eCgsVeOwT~Or}r7PuYIlLn~eT{pZ=*^U}qMPN~+a z{dD&JtTprJ%(pYQr+=G%d1}47T76#muHsh~Pwh_rIQjA9`SKSNe~ZpMPM$!&e6gzE zTGRTM-EPzQ{8Q)Y6VGp2fn)o72Uh6#oxqA(R*RbSTicEGorZC5^LFErQPd-Ov9YyT z*laAzDa`(UZyyuKN>LmO|1pl0@o}WX zy3M-L_MSL?R08wv`HF5XXfdXUHL#+9WyR#P3nLScw#&KX7Gqg_H@oMm`kPBye>%!9 za9SOw8;a2ayXAVp_%RBz2y^`#~4*=wP_+ji2~BZZMC^GN_Z>o+$VgAt;B z#bb zpz(I&&iccR9piN)?Ph*ZtLU#TYMm@+vNxoa3+9U0(oPwpK<*N9x4(b3qAx9K&gh_B zyOYucwc;UVmyrSG2J>*XsxK~T{ne~)khlbAi%iAtneobUMpJyazJ2T6_3e5b@4#nM z?0VAG?BATO>T7G-v!!UtruU@V_UvYy^WK5&c7vjAGb5oC^%w~&EkZpSF+LbGu_QGX zF&Z0Nn|G6#2c@fL_Yyg7-N=k%8s^=ZivH1>R*I|eSzBBLSavV;qUTmobk<7I*4eK= zIaAfIT+zNcJ*qM2JYAgkNV3@)w0fdAxqS9|zAq2jLpwYu&N#IFmh++xf#?xh@~Fr= z`p%x?9yv{G_i5+^`S#EY?e@4vF|fFVN)OLL;hJ{0CD+uahXsvpSDcId-eG9@jvaX2 z{L2sQ;J~=Kv2}A;RHxe<6Tf%pbe-W6F^Iu;_Qf=gp9d8SfFl?7e2b{#_=a@A;vNTK zcxZK;@W3l5;qFt*`CKl$`6-j8?BSQmI<#B)m$dC5v^rkX-FHRex&5Uhm-~o5X1g)voQ{w7c==Axnb~WWqHSkJ_WGh8#&WtfvltBums@<21Acg( z9&;3kE1bd>^U+jAf9;a?Bs#*Q6F?B{O!3kZZ#bPq9baPQI?2^H-e@dGKcfN6N7eku z`@Yx7jyzQ~@~~5O3f6vMOUKuG~wq@LjBOW7n5S#r^C#(9UOWKz!!wG{pkyH6ef^ox(&~szg z5y z0Qp|#z81ms<3&WQkmAxMv?oSj$|E5I_I{1Q0*~0R#|00D+S)ApifLzyCk^btXdu5I_I{1Q0*~0R#|0 z00FfHxc^tX{$!5;0tg_000IagfB*srAfSc-_y20tnJf`N009ILKmY**5I_I{1k@Jb z|NqslKiMOI00IagfB*srAb2q1s}0tg_000IagpoRed|F1@!$r1qs5I_I{1Q0*~0R#|0Ky3j&|5v;IWRCy> z2q1s}0tg_000IagpoRdS|Ep1FvP1v@1Q0*~0R#|0009ILP+Nfef3@pR_6Q(=00Iag zfB*srAb '<%= SECGEN_FUNCTIONS_PUPPET_DIR %>' -mod 'puppetlabs-powershell', '2.1.0' +# mod 'puppetlabs-powershell', '2.1.0' <% @currently_processing_system.module_selections.each do |selected_module| -%> <% case selected_module.module_type diff --git a/lib/templates/Vagrantfile.erb b/lib/templates/Vagrantfile.erb index 4523d0fe0..e8808341f 100644 --- a/lib/templates/Vagrantfile.erb +++ b/lib/templates/Vagrantfile.erb @@ -33,7 +33,7 @@ vb.customize ['modifyvm', :id, '--hwvirtex', 'on'] " vb.customize ['modifyvm', :id, '--vtxvpid', 'on']" else " vb.customize ['modifyvm', :id, '--vtxvpid', 'off']" - end -%> + end %> <%= if (@options.has_key? :memory_per_vm) " vb.memory = #{@options[:memory_per_vm]}" elsif (@options.has_key? :total_memory) @@ -62,7 +62,7 @@ vb.customize ['modifyvm', :id, '--hwvirtex', 'on'] <% if (selected_module.attributes['platform'].first.downcase == 'windows') %> config.vm.communicator = 'winrm' config.vm.guest = :windows - config.vm.network :forwarded_port, guest: 3389, host: 3389 + # config.vm.network :forwarded_port, guest: 3389, host: 3389 config.vm.network :forwarded_port, guest: 5985, host: 5985, id: "winrm", auto_correct: true <% end %> @@ -72,7 +72,7 @@ vb.customize ['modifyvm', :id, '--hwvirtex', 'on'] <% else -%> <%= system.name %>.vm.network :<%= selected_module.attributes['type'].first %>, ip: "<%= resolve_network(selected_module.attributes['range'].first)%>" <% end -%> -<% when 'vulnerability', 'service', 'utility', 'build' -%> +<% when 'vulnerability', 'service', 'utility', 'build', 'forensic' -%> <% module_name = selected_module.module_path_name -%> <%= system.name %>.vm.provision "puppet" do | <%=module_name%> | <% # if there are facter variables to define @@ -85,9 +85,14 @@ vb.customize ['modifyvm', :id, '--hwvirtex', 'on'] <%=module_name%>.module_path = "<%="puppet/#{system.name}/modules"%>" <%=module_name%>.environment_path = "environments/" <%=module_name%>.environment = "production" + <% if (selected_module.attributes['platform'].first.downcase == 'windows') -%> + #<%=module_name%>.synced_folder_type = "smb" + <% else -%> <%=module_name%>.synced_folder_type = "rsync" + <% end -%> <%=module_name%>.manifests_path = "<%="puppet/#{system.name}/modules/#{selected_module.module_path_end}"%>" <%=module_name%>.manifest_file = "<%="#{selected_module.module_path_end}.pp"%>" + #<%=module_name%>.options = "--verbose --debug" end <% end -%> <% end -%> diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp b/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp index 0183cdb2e..2b48c362e 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/internet_history_chrome.pp @@ -1 +1,60 @@ -include internet_history_chrome::init \ No newline at end of file +notice("THE MODULE internet history chrome was LOADED") + +# include internet_history_chrome::init + +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$chrome_history_file_name=$secgen_parameters['chrome_history_file_name'][0] + +# notice("Chrome history file value") +# notice($chrome_history_file_name) +# notice("Chrome history file value end") +# +# +# notice("THE MODULE internet history chrome was LOADED #2") +# +# $user_account = 'vagrant' +# $url_paths = ["https://www.offensive-security.com/backtrack/exploit-db-updates"] +# +# file { "C:\Users\{$user_account}\AppData\Roaming\Mozilla\Firefox\Profiles\{$mozilla_profile_number}.default\places.sqlite": +# +# } +# +# exec { "add-chrome-history": +# command => "", +# } +# +# file { 'add-chrome-history': +# ensure => 'file', +# ### path => "C:/Users/$user_account/AppData/Local/Google/Chrome/User Data/Default/History", +# path => "C:/Users/$user_account/Desktop/test_file_here.txt", +# ### content => template('internet_history_chrome/insert_history.erb') +# # content => inline_template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') +# source => 'puppet:///modules/internet_history_chrome/chrome_history_file' +# } + +class { 'internet_history_chrome': + user_account => 'vagrant', + history_file_name => 'History', +} + +# file { "add-chrome-history-source": +# path => "C:/Users/$user_account/Desktop/History", +# ensure => 'file', +# # source => "puppet:///modules/internet_history_chrome/chrome_history_file", +# # source => "puppet:///modules/file_transfer_storage_module/$chrome_history_file_name", +# source => "puppet:///modules/file_transfer_storage_module/History", +# } + +# file { "add-chrome-history-source": +# path => "C:/Users/$user_account/Desktop/test_file_here_source.txt", +# ensure => 'file', +# # source => "puppet:///modules/internet_history_chrome/chrome_history_file", +# source => "puppet:///modules/internet_history_chrome/History_test_source_move", +# } + +# file { 'add-chrome-history-content': +# ensure => 'file', +# path => "C:/Users/$user_account/Desktop/test_file_here_content.txt", +# content => inline_template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') +# } \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp b/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp index f6b82de68..31c5f6353 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/manifests/init.pp @@ -1,6 +1,8 @@ -class internet_history_chrome::init { - $user_account = 'vagrant' - $url_paths = ["https://www.offensive-security.com/backtrack/exploit-db-updates"] +class internet_history_chrome ($user_account, $history_file_name) { + # notice("THE MODULE internet history chrome was LOADED #2") + + # $user_account = 'vagrant' + # $url_paths = ["https://www.offensive-security.com/backtrack/exploit-db-updates"] # file { "C:\Users\{$user_account}\AppData\Roaming\Mozilla\Firefox\Profiles\{$mozilla_profile_number}.default\places.sqlite": # @@ -13,7 +15,9 @@ file { 'add-chrome-history': ensure => 'present', path => "C:/Users/$user_account/AppData/Local/Google/Chrome/User Data/Default/History", - content => template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') + # path => 'C:/Users/vagrant/AppData/Local/Google/Chrome/User Data/Default/History', + # content => template('internet_history_chrome/insert_history.erb') # content => inline_template('evidence_windows_cybercrime_internet_history_chrome/insert_history.erb') + source => "puppet:///modules/file_transfer_storage_module/History", } } \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml b/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml index e7f8a6c22..e6ee2638a 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/secgen_metadata.xml @@ -1,8 +1,8 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.github/cliffe/SecGen/forensic"> Internet history chrome Jason Keighley Apache v2 @@ -14,28 +14,38 @@ - - + chrome_history_file_name - chrome_history_file - - + - - + + 100 + + + 3rd july 2013 15:16:20 + + + 5th june 2015 15:16:20 + + + 10 + + + 4th july 2013 12:00:00 + + + 4th july 2013 15:00:00 - - - - - - - - + + Powershell install local + Google Chrome install + + Store files for transfer + \ No newline at end of file diff --git a/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb index fd7ff1c4a..fe32e704a 100644 --- a/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb +++ b/modules/forensics/windows/internet_artifacts/internet_history_chrome/templates/insert_history.erb @@ -1,28 +1,5 @@ -<% #require 'json' - #$secgen_parameters = JSON.parse(@json_inputs) - #$server_name = $secgen_parameters['server_name'].first - #$welcome_msg = $secgen_parameters['welcome_msg'].first - # - #if $secgen_parameters['url'] - # $business_name = $secgen_parameters['business_name'].first - # $welcome_msg = "Welcome to the #{$business_name} FTP server!" - #end - - require 'sqlite' - - local_user = 'vagrant' - chrome_user = 'Default' - - SQLite3::Database.new( "/media/user/3TB_internal_drive/Documents/SecGen/modules/forensics/windows/internet_artifacts/internet_history_chrome/files/History" ) do |db| - db.execute( "select * from urls" ) do |row| - puts row - end - db.execute( - "INSERT INTO urls(id, url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) -VALUES ('37', 'test_url', 'test_title', '1', '1', '1', '0', '0');" - ) - db.execute( "select * from urls" ) do |row| - puts row - end - end -%> \ No newline at end of file +<%require 'json' -%> +<%require 'base64' -%> +<%$parsed_inputs = JSON.parse(@json_inputs) -%> +<%$chrome_history_file_binary = $parsed_inputs['chrome_history_file'].first -%> +<%=Base64.decode64($chrome_history_file_binary) %> \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb index 26f13a397..dbb5ba44e 100644 --- a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb +++ b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_local/local.rb @@ -4,49 +4,122 @@ require 'sqlite3' require 'fileutils' -# class ChromeHistoryFileGenerator < StringGenerator -# attr_accessor :history_urls -# -# def initialize -# super -# self.module_name = 'Chrome history file generator' -# self.history_urls = '' -# end -# -# def generate +class ChromeHistoryFileGenerator < StringGenerator + attr_accessor :number_of_generic_urls + attr_accessor :generic_urls_start_time + attr_accessor :generic_urls_end_time + + attr_accessor :number_of_cybercrime_urls + attr_accessor :cybercrime_urls_start_time + attr_accessor :cybercrime_urls_end_time + + def initialize + super + self.module_name = 'Chrome history file generator' + # self.history_urls = '' + self.number_of_generic_urls = '' + self.generic_urls_start_time = '' + self.generic_urls_end_time = '' + + self.number_of_cybercrime_urls = '' + self.cybercrime_urls_start_time = '' + self.cybercrime_urls_end_time = '' + end + + def get_options_array + super + [['--number_of_generic_urls', GetoptLong::REQUIRED_ARGUMENT], + ['--generic_urls_start_time', GetoptLong::REQUIRED_ARGUMENT], + ['--generic_urls_end_time', GetoptLong::REQUIRED_ARGUMENT], + ['--number_of_cybercrime_urls', GetoptLong::REQUIRED_ARGUMENT], + ['--cybercrime_urls_start_time', GetoptLong::REQUIRED_ARGUMENT], + ['--cybercrime_urls_end_time', GetoptLong::REQUIRED_ARGUMENT]] + end + + def process_options(opt, arg) + super + case opt + when '--number_of_generic_urls' + self.number_of_generic_urls << arg; + when '--generic_urls_start_time' + self.generic_urls_start_time << arg; + when '--generic_urls_end_time' + self.generic_urls_end_time << arg; + when '--number_of_cybercrime_urls' + self.number_of_cybercrime_urls << arg; + when '--cybercrime_urls_start_time' + self.cybercrime_urls_start_time << arg; + when '--cybercrime_urls_end_time' + self.cybercrime_urls_end_time << arg; + end + end + + + def generate local_user = 'vagrant' chrome_user = 'Default' + chrome_history_file_tmp_path = "#{FILE_TRANSFER_STORAGE_MODULE_DIR}/files/History" + + urls = Hash.new + + # self.number_of_generic_urls = 100 + # self.generic_urls_start_time = '3rd july 2013 15:16:20' + # self.generic_urls_end_time = '5th june 2015 15:16:20' + # + # self.number_of_cybercrime_urls = 10 + # self.cybercrime_urls_start_time = '4th july 2013 12:00:00' + # self.cybercrime_urls_end_time = '4th july 2013 15:00:00' + + # Generic filler urls + generic_urls = File.readlines("#{URLLISTS_DIR}/generic_urls").sample(self.number_of_generic_urls.to_i) - history_urls = { - 'url_test_1' => {:url => 'test1', :title => 'test1', :visit_count => '1', :typed_count => '1', :last_visit_time => 10, :hidden => '0', :favicon_id => '0'}, - 'url_test_2' => {:url => 'test2', :title => 'test2', :visit_count => '2', :typed_count => '2', :last_visit_time => 10, :hidden => '0', :favicon_id => '0'}, - 'url_test_3' => {:url => 'test3', :title => 'test3', :visit_count => '3', :typed_count => '3', :last_visit_time => 10, :hidden => '0', :favicon_id => '0'} - } + # Crime url start + cybercrime_urls = File.readlines("#{URLLISTS_DIR}/cybercrime_urls").sample(self.number_of_cybercrime_urls.to_i) - FileUtils.cp('../templates/History.source', '../templates/History') + generic_urls.each do |url| + date_start = Time.parse(self.generic_urls_start_time) + date_end = Time.parse(self.generic_urls_end_time) - database = SQLite3::Database.new( "../templates/History" ) do |db| + urls[url] = { + :url => url, + :title => url[/\/\/.*\//].gsub(/(\/)/,''), + :visit_count => rand(0..100).to_i, + :typed_count => rand(0..100).to_i, + :last_visit_time => (date_start.to_f + rand * (date_end.to_f - date_start.to_f)).to_i, + :hidden => '0', + :favicon_id => '0' + } + end + + cybercrime_urls.each do |url| + date_start = Time.parse(self.generic_urls_start_time) + date_end = Time.parse(self.generic_urls_end_time) + + urls[url] = { + :url => url, + :title => url[/\/\/.*\//].gsub(/(\/)/,''), + :visit_count => rand(0..100).to_i, + :typed_count => rand(0..100).to_i, + :last_visit_time => (date_start.to_f + rand * (date_end.to_f - date_start.to_f)).to_i, + :hidden => '0', + :favicon_id => '0' + } + end + + history_urls = Hash[urls.to_a.shuffle] + + FileUtils.cp("#{INTERNET_BROWSER_FILES_DIR}/chrome_history_file.source", chrome_history_file_tmp_path) + + database = SQLite3::Database.new( chrome_history_file_tmp_path ) do |db| history_urls.each_value do |details| db.execute( "INSERT INTO urls(url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) VALUES ('#{details[:url]}', '#{details[:title]}', '#{details[:visit_count]}', '#{details[:typed_count]}', '#{details[:last_visit_time]}', '#{details[:hidden]}', '#{details[:favicon_id]}');" ) end - - # db.execute( "select * from urls" ) do |row| - # puts row - # # p row - # end - -# "INSERT INTO urls(url, title, visit_count, typed_count, last_visit_time, hidden, favicon_id) -# VALUES ('test_url', 'test_title', '1', '1', '1', '0', '0');" - end - # puts self.history_urls - # - # self.outputs << database - # end -# end -# -# ChromeHistoryFileGenerator.new.run \ No newline at end of file + self.outputs << Base64.strict_encode64(chrome_history_file_tmp_path).split(/[\\\/]/).last + end +end + +ChromeHistoryFileGenerator.new.run \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml index 0d209a158..d53cb7d26 100644 --- a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml +++ b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/secgen_metadata.xml @@ -14,7 +14,15 @@ - history_urls + + + number_of_generic_urls + generic_urls_start_time + generic_urls_end_time + + number_of_cybercrime_urls + cybercrime_urls_start_time + cybercrime_urls_end_time urls \ No newline at end of file diff --git a/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/templates/History.source b/modules/generators/forensics/internet_artifacts/chrome_history_file_generator/templates/History.source new file mode 100644 index 0000000000000000000000000000000000000000..85df9987b1451ccb88e729e94fff796bc130dc9b GIT binary patch literal 94208 zcmeI(U2oe|7{Kwk>5^`3mv&ocO{1F8gj$PC&{YMbT|l>GW1?$W>Ch%XmfN_gMG_a= z>6p06fGfTO-v#cFkdXKU+;D>g2ywGZ&hbm)U}xnL_536CB|azTJkRg3kL{%W;K7C) zI>x@|ckIwuEzOlQt@Mszlu9K{eCgsVeOwT~Or}r7PuYIlLn~eT{pZ=*^U}qMPN~+a z{dD&JtTprJ%(pYQr+=G%d1}47T76#muHsh~Pwh_rIQjA9`SKSNe~ZpMPM$!&e6gzE zTGRTM-EPzQ{8Q)Y6VGp2fn)o72Uh6#oxqA(R*RbSTicEGorZC5^LFErQPd-Ov9YyT z*laAzDa`(UZyyuKN>LmO|1pl0@o}WX zy3M-L_MSL?R08wv`HF5XXfdXUHL#+9WyR#P3nLScw#&KX7Gqg_H@oMm`kPBye>%!9 za9SOw8;a2ayXAVp_%RBz2y^`#~4*=wP_+ji2~BZZMC^GN_Z>o+$VgAt;B z#bb zpz(I&&iccR9piN)?Ph*ZtLU#TYMm@+vNxoa3+9U0(oPwpK<*N9x4(b3qAx9K&gh_B zyOYucwc;UVmyrSG2J>*XsxK~T{ne~)khlbAi%iAtneobUMpJyazJ2T6_3e5b@4#nM z?0VAG?BATO>T7G-v!!UtruU@V_UvYy^WK5&c7vjAGb5oC^%w~&EkZpSF+LbGu_QGX zF&Z0Nn|G6#2c@fL_Yyg7-N=k%8s^=ZivH1>R*I|eSzBBLSavV;qUTmobk<7I*4eK= zIaAfIT+zNcJ*qM2JYAgkNV3@)w0fdAxqS9|zAq2jLpwYu&N#IFmh++xf#?xh@~Fr= z`p%x?9yv{G_i5+^`S#EY?e@4vF|fFVN)OLL;hJ{0CD+uahXsvpSDcId-eG9@jvaX2 z{L2sQ;J~=Kv2}A;RHxe<6Tf%pbe-W6F^Iu;_Qf=gp9d8SfFl?7e2b{#_=a@A;vNTK zcxZK;@W3l5;qFt*`CKl$`6-j8?BSQmI<#B)m$dC5v^rkX-FHRex&5Uhm-~o5X1g)voQ{w7c==Axnb~WWqHSkJ_WGh8#&WtfvltBums@<21Acg( z9&;3kE1bd>^U+jAf9;a?Bs#*Q6F?B{O!3kZZ#bPq9baPQI?2^H-e@dGKcfN6N7eku z`@Yx7jyzQ~@~~5O3f6vMOUKuG~wq@LjBOW7n5S#r^C#(9UOWKz!!wG{pkyH6ef^ox(&~szg z5y z0Qp|#z81ms<3&WQkmAxMv?oSj$|E5I_I{1Q0*~0R#|00D+S)ApifLzyCk^btXdu5I_I{1Q0*~0R#|0 z00FfHxc^tX{$!5;0tg_000IagfB*srAfSc-_y20tnJf`N009ILKmY**5I_I{1k@Jb z|NqslKiMOI00IagfB*srAb2q1s}0tg_000IagpoRed|F1@!$r1qs5I_I{1Q0*~0R#|0Ky3j&|5v;IWRCy> z2q1s}0tg_000IagpoRdS|Ep1FvP1v@1Q0*~0R#|0009ILP+Nfef3@pR_6Q(=00Iag zfB*srAb url ,:title => 'test', :visit_count => '1', :typed_count => '1', :last_visit_time => start_time, :hidden => '0', :favicon_id => '0' } + date_start = Time.parse(self.generic_urls_start_time) + date_end = Time.parse(self.generic_urls_end_time) + + urls[url] = { + :url => url, + :title => url[/\/\/.*\//].gsub(/(\/)/), + :visit_count => rand, + :typed_count => rand, + :last_visit_time => date_start.to_f + rand * (date_end.to_f - date_start.to_f), + :hidden => '0', + :favicon_id => '0' + } end - puts urls - - self.outputs << urls + # self.outputs << Base64.strict_encode64(urls.to_s) + self.outputs << Base64.encode64(urls) end end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/puppetlabs_powershell_local.pp b/modules/utilities/windows/shells/puppetlabs_powershell_local/puppetlabs_powershell_local.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml b/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml new file mode 100644 index 000000000..cf3d8d860 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + Powershell install local + Jason Keighley + Apache v2 + A local version of the powershell shell provisioner + + shells + windows + + + + + \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp b/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp index b0f1ec41e..4ccadf5dd 100644 --- a/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp +++ b/modules/utilities/windows/web_browsers/google_chrome/google_chrome.pp @@ -1 +1,2 @@ -include google_chrome::install \ No newline at end of file +include google_chrome::install +include google_chrome::configure \ No newline at end of file diff --git a/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp b/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp index 3b58d75a3..b2c2486d2 100644 --- a/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp +++ b/modules/utilities/windows/web_browsers/google_chrome/manifests/configure.pp @@ -1,4 +1,6 @@ class google_chrome::configure { + $cmd_executable_install_path = 'C:\windows\system32\cmd.exe' + # Need to ensure unique to each version of Windows, # different versions may have different install locations exec { 'google-chrome-initialize': @@ -6,8 +8,16 @@ command => 'C:\windows\system32\cmd.exe /C start "" "C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" https://www.google.com', } - exec { 'google-chrome-kill-all-processes': + exec { 'sleep-10': require => Exec[google-chrome-initialize], - command => "$cmd_executable_install_path\\cmd.exe /C \"taskkill /F /IM chrome.exe /T\"" + command => 'Start-Sleep -s 10', + provider => powershell, + } + + exec { 'google-chrome-kill-all-processes': + require => Exec[sleep-10], + # command => "$cmd_executable_install_path\\cmd.exe /C \"taskkill /F /IM chrome.exe /T\"" + # command => 'C:\windows\system32\cmd.exe /C "taskkill /F /IM chrome.exe /T"' + command => 'C:\windows\system32\cmd.exe /C "taskkill /F /IM chrome.exe /T && exit /b 0"' } } \ No newline at end of file diff --git a/scenarios/windows_scenario.xml b/scenarios/windows_scenario.xml index 7721bae8c..d0cfbe195 100644 --- a/scenarios/windows_scenario.xml +++ b/scenarios/windows_scenario.xml @@ -9,24 +9,7 @@ storage_server - - - - C:\Users\vagrant\Desktop\Hello.jpg - - - - - - - - - - - - - - + From d1152d09d18f1b6bc8fc5f6192a9e1afb199ff47 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sun, 16 Apr 2017 23:32:11 +0100 Subject: [PATCH 10/24] Added Sqlite browser install module --- .../sqlite_browser/manifests/install.pp | 10 ++++++++++ .../sqlite_browser/secgen_metadata.xml | 20 +++++++++++++++++++ .../sqlite_browser/sqlite_browser.pp | 1 + 3 files changed, 31 insertions(+) create mode 100644 modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp create mode 100644 modules/utilities/windows/database_editor/sqlite_browser/secgen_metadata.xml create mode 100644 modules/utilities/windows/database_editor/sqlite_browser/sqlite_browser.pp diff --git a/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp b/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp new file mode 100644 index 000000000..6fb04964c --- /dev/null +++ b/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp @@ -0,0 +1,10 @@ +class sqlite_browser::install { + include chocolatey + + notice('Installing Sqlite browser') + + package { 'sqlitebrowser': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/database_editor/sqlite_browser/secgen_metadata.xml b/modules/utilities/windows/database_editor/sqlite_browser/secgen_metadata.xml new file mode 100644 index 000000000..7d215d5e3 --- /dev/null +++ b/modules/utilities/windows/database_editor/sqlite_browser/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Sqlite browser install + Jason Keighley + Apache v2 + A Sqlite browser installation + + database_editor + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/database_editor/sqlite_browser/sqlite_browser.pp b/modules/utilities/windows/database_editor/sqlite_browser/sqlite_browser.pp new file mode 100644 index 000000000..f5a3887a6 --- /dev/null +++ b/modules/utilities/windows/database_editor/sqlite_browser/sqlite_browser.pp @@ -0,0 +1 @@ +include sqlite_browser::install \ No newline at end of file From 3029312cd1b7161d177a3890c3f047edc4e3d971 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sun, 16 Apr 2017 23:34:19 +0100 Subject: [PATCH 11/24] Ensured all modules done previously could accept input values by adding process_options and get_options_array methods to the generator modules. --- .../select_cat_image/secgen_local/local.rb | 12 +++++++++ .../secgen_local/local.rb | 12 +++++++++ .../secgen_local/local.rb | 25 +++++++++++++++---- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb b/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb index 710506cc1..4ef69e420 100644 --- a/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb +++ b/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb @@ -11,6 +11,18 @@ def initialize self.selected_image_path = Dir["#{ILLEGAL_IMAGES_DIR}/cats/*"].sample end + def get_options_array + super + [['--selected_image_path', GetoptLong::OPTIONAL_ARGUMENT]] + end + + def process_options(opt, arg) + super + case opt + when '--selected_image_path' + self.selected_image_path << arg; + end + end + def generate file_contents = File.binread(self.selected_image_path) self.outputs << Base64.strict_encode64(file_contents) diff --git a/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb index f42d48b11..9bbc86a9e 100644 --- a/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb +++ b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb @@ -11,6 +11,18 @@ def initialize self.selected_image_path = Dir["#{ILLEGAL_IMAGES_DIR}/cats/*"].sample end + def get_options_array + super + [['--selected_image_path', GetoptLong::OPTIONAL_ARGUMENT]] + end + + def process_options(opt, arg) + super + case opt + when '--selected_image_path' + self.selected_image_path << arg; + end + end + def generate selected_image_path = Base64.strict_encode64(self.selected_image_path) self.outputs << selected_image_path diff --git a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb index 094e37b63..7dcfba6ce 100644 --- a/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb +++ b/modules/generators/forensics/time/generate_random_time/secgen_local/local.rb @@ -1,8 +1,8 @@ #!/usr/bin/ruby require_relative '../../../../../../lib/objects/local_string_generator.rb' -require 'date' +require 'time' -class GenerateRandomDate < StringGenerator +class GenerateRandomTime < StringGenerator attr_accessor :date_start attr_accessor :date_end @@ -13,13 +13,28 @@ def initialize self.date_end = '' end + def get_options_array + super + [['--date_start', GetoptLong::OPTIONAL_ARGUMENT], + ['--date_end', GetoptLong::OPTIONAL_ARGUMENT]] + end + + def process_options(opt, arg) + super + case opt + when '--date_start' + self.date_start << arg; + when '--date_end' + self.date_end << arg; + end + end + def generate - self.date_start.nil? ? self.date_start = 0.0: date_start = self.Time.parse(:date_start) - self.date_end.nil? ? self.date_end = Time.now: date_end = self.Time.parse(:date_end) + self.date_start.empty? ? self.date_start = 0.0: self.date_start = Time.parse(self.date_start) + self.date_end.empty? ? self.date_end = Time.now: self.date_end = Time.parse(self.date_end) random_date = Time.at(self.date_start.to_f + rand * (self.date_end.to_f - self.date_start.to_f)) self.outputs << random_date.strftime("%m/%d/%Y %H:%M:%S") end end -GenerateRandomDate.new.run \ No newline at end of file +GenerateRandomTime.new.run \ No newline at end of file From 00e1f78aa98d595ad9c3b92ce720d2dc9544246e Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sun, 16 Apr 2017 23:36:24 +0100 Subject: [PATCH 12/24] Added scenario files to showcase some forensic modules and a chrome history example scenario file. --- .../chrome_history_example.xml | 39 ++++-- .../multiple_module_example.xml | 115 ++++++++++++++++++ 2 files changed, 141 insertions(+), 13 deletions(-) create mode 100644 scenarios/simple_examples/forensic_examples/multiple_module_example.xml diff --git a/scenarios/simple_examples/forensic_examples/chrome_history_example.xml b/scenarios/simple_examples/forensic_examples/chrome_history_example.xml index e6e98423e..c6287f20d 100644 --- a/scenarios/simple_examples/forensic_examples/chrome_history_example.xml +++ b/scenarios/simple_examples/forensic_examples/chrome_history_example.xml @@ -6,22 +6,35 @@ - - storage_server - - - - - - - - - - - + + + + + + + 100 + + + 3rd july 2013 15:16:20 + + + 5th june 2015 15:16:20 + + + 10 + + + 4th july 2013 12:00:00 + + + 4th july 2013 15:00:00 + + + + diff --git a/scenarios/simple_examples/forensic_examples/multiple_module_example.xml b/scenarios/simple_examples/forensic_examples/multiple_module_example.xml new file mode 100644 index 000000000..07cb9777c --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/multiple_module_example.xml @@ -0,0 +1,115 @@ + + + + + + + storage_server + + + + + + + + + + + + + + 100 + + + 3rd july 2013 15:16:20 + + + 5th june 2015 15:16:20 + + + 10 + + + 4th july 2013 12:00:00 + + + 4th july 2013 15:00:00 + + + + + + + + C:\Users\vagrant\Desktop\Hello.jpg + + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + File contents + + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + + + 4th july 2013 12:00:00 + + + 4th july 2013 15:00:00 + + + + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + + + 4th july 2013 12:00:00 + + + 4th july 2013 15:00:00 + + + + + + + + C:\Users\vagrant\Desktop\Hello.txt + + + + + + + + + C:\Users\vagrant\Desktop\Hello + + + + + + C:\Users\vagrant\Desktop\Hello + + + + + + + From b95c96c3bc6923636e84c0371216def10be8ef1b Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Tue, 18 Apr 2017 21:50:50 +0100 Subject: [PATCH 13/24] Added powershell local requires to SecGen metadata of all modules that require powershell. Also added manifests directory that was not commited to remote branch with initial module commit. --- .../secgen_metadata.xml | 6 +- .../secgen_metadata.xml | 6 +- .../secgen_metadata.xml | 6 +- .../secgen_metadata.xml | 6 +- .../puppetlabs_powershell_local/CHANGELOG.md | 98 +++ .../CONTRIBUTING.md | 219 +++++ .../puppetlabs_powershell_local/Gemfile | 153 ++++ .../puppetlabs_powershell_local/LICENSE | 202 +++++ .../MAINTAINERS.md | 6 + .../shells/puppetlabs_powershell_local/NOTICE | 16 + .../puppetlabs_powershell_local/README.md | 213 +++++ .../puppetlabs_powershell_local/Rakefile | 28 + .../puppetlabs_powershell_local/appveyor.yml | 44 + .../checksums.json | 34 + .../lib/puppet/provider/exec/powershell.rb | 139 ++++ .../compatible_powershell_version.rb | 51 ++ .../powershell/powershell_manager.rb | 328 ++++++++ .../powershell/powershell_version.rb | 53 ++ .../lib/puppet_x/templates/init_ps.ps1 | 787 ++++++++++++++++++ .../puppetlabs_powershell_local/metadata.json | 38 + .../secgen_metadata.xml | 1 + .../spec/acceptance/exec_powershell_spec.rb | 425 ++++++++++ .../spec/acceptance/files/param_script.ps1 | 7 + .../spec/acceptance/files/services.ps1 | 5 + .../acceptance/nodesets/windows-2003-i386.yml | 24 + .../nodesets/windows-2003-x86_64.yml | 24 + .../nodesets/windows-2008-x86_64.yml | 24 + .../nodesets/windows-2008r2-x86_64.yml | 24 + .../nodesets/windows-2012-x86_64.yml | 24 + .../nodesets/windows-2012r2-x86_64.yml | 24 + .../spec/exit-27.ps1 | 1 + .../puppetlabs/powershell_manager_spec.rb | 688 +++++++++++++++ .../spec/spec.opts | 6 + .../spec/spec_helper.rb | 54 ++ .../spec/spec_helper_acceptance.rb | 36 + .../unit/provider/exec/powershell_spec.rb | 240 ++++++ .../compatible_powershell_version_spec.rb | 58 ++ .../powershell/powershell_version_spec.rb | 75 ++ 38 files changed, 4161 insertions(+), 12 deletions(-) create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/CHANGELOG.md create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/CONTRIBUTING.md create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/Gemfile create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/LICENSE create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/MAINTAINERS.md create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/NOTICE create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/README.md create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/Rakefile create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/appveyor.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/checksums.json create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet/provider/exec/powershell.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_version.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/templates/init_ps.ps1 create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/metadata.json create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/exec_powershell_spec.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/param_script.ps1 create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/services.ps1 create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-i386.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-x86_64.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008-x86_64.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008r2-x86_64.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012-x86_64.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012r2-x86_64.yml create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/exit-27.ps1 create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec.opts create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper_acceptance.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/provider/exec/powershell_spec.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb diff --git a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml index dbcbaeaf3..33b2aee41 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_all_main_times/secgen_metadata.xml @@ -25,8 +25,8 @@ - - - + + Powershell install local + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml index 7cc104021..baafdf0f3 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_creation_time/secgen_metadata.xml @@ -25,8 +25,8 @@ - - - + + Powershell install local + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml index a879488aa..1b5eeac0f 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_last_access_time/secgen_metadata.xml @@ -25,8 +25,8 @@ - - - + + Powershell install local + \ No newline at end of file diff --git a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml index bb27159e4..57179a9bf 100644 --- a/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml +++ b/modules/forensics/windows/timestamps/change_timestamp_last_write_time/secgen_metadata.xml @@ -25,8 +25,8 @@ - - - + + Powershell install local + \ No newline at end of file diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/CHANGELOG.md b/modules/utilities/windows/shells/puppetlabs_powershell_local/CHANGELOG.md new file mode 100644 index 000000000..66cc345b5 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/CHANGELOG.md @@ -0,0 +1,98 @@ +##2016-11-17 - Supported Release 2.1.0 +###Summary + +Small release with bugs fixes and another speed improvement. + +###Bug Fixes +- Support Windows 2016/WMF 5.1 using named pipes ([MODULES-3690](https://tickets.puppetlabs.com/browse/MODULES-3690)) + +###Documentation updates +- Document herestring ([DOC-2960](https://tickets.puppetlabs.com/browse/DOC-2960)) + +##2016-10-05 - Supported Release 2.0.3 +###Summary + +Small release with bugs fixes and another speed improvement. + +###Bug Fixes +- Miscellaneous fixes which improve reliability +- Capture exit codes when executing external scripts ([MODULES-3399](https://tickets.puppetlabs.com/browse/MODULES-3399)) +- Add ability to set current working directory ([MODULES-3565](https://tickets.puppetlabs.com/browse/MODULES-3565)) +- Respect user specified timeout ([MODULES-3709](https://tickets.puppetlabs.com/browse/MODULES-3709)) +- Improve handling of user code exceptions ([MODULES-3443](https://tickets.puppetlabs.com/browse/MODULES-3443)) +- Output line and stacktrace of user code exception ([MODULES-3839](https://tickets.puppetlabs.com/browse/MODULES-3839)) +- Improve resilience to failure of PowerShell host ([MODULES-3875](https://tickets.puppetlabs.com/browse/MODULES-3875)) +- Fix race condition in threading with PowerShell host ([MODULES-3144](https://tickets.puppetlabs.com/browse/MODULES-3144)) +- Modify tests to detect differences in PowerShell error text ([MODULES-3442](https://tickets.puppetlabs.com/browse/MODULES-3442)) + +###Documentation updates +- Document how to handle exit codes ([MODULES-3588](https://tickets.puppetlabs.com/browse/MODULES-3588)) + +##2016-07-12 - Supported Release 2.0.2 +###Summary + +Small release with bugs fixes and another speed improvement. + +###Features +- Noticable speed increase by reducing the time start a PowerShell command ([MODULES-3406](https://tickets.puppetlabs.com/browse/MODULES-3406)) + +###Bug Fixes +- Fixed minor bugs in tests ([MODULES-3347](https://tickets.puppetlabs.com/browse/MODULES-3347)) +- Added tests for try/catch ([MODULES-2634](https://tickets.puppetlabs.com/browse/MODULES-2634)) +- Fixed bug with older ruby (1.8) + +##2016-05-24 - Supported Release 2.0.1 +###Bug Fixes + +- Updated the powershell manager in this module in order to not conflict with the Powershell Manager in the Puppet DSC module + +##2016-05-17 - Supported Release 2.0.0 +###Summary + +Major release with performance improvements + +Removed support for Windows Server 2003 + +###Features +- Major performance improvement by sharing a single powershell session instead of creating a new powershell session per command +- Security improvement as scripts are not stored on the filesystem temporarily + +###Bug Fixes +- Updated test suites with later versions +- Documentation cleanup + +##2015-12-08 - Supported Release 1.0.6 +###Summary + +Small release for support of newer PE versions. + +##2015-07-28 - Supported Release 1.0.5 +###Summary + +Add metadata for Puppet 4 and PE 2015.2.0 + +###Bug Fixes +- Minor testing bug fixes +- Readme cleanup + +##2014-11-04 - Supported Release 1.0.4 +###Summary + +Fix Issues URL +Add Future Parser testing support + +##2014-08-25 - Supported Release 1.0.3 +###Summary + +This release updates the tests to verify that powershell continues to function on x64-native ruby. + +##2014-07-15 - Supported Release 1.0.2 +###Summary + +This release merely updates metadata.json so the module can be uninstalled and +upgraded via the puppet module command. + +##2014-07-09 - Release 1.0.1 +###Summary + +Fix issue with metadata and PE version requirement diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/CONTRIBUTING.md b/modules/utilities/windows/shells/puppetlabs_powershell_local/CONTRIBUTING.md new file mode 100644 index 000000000..dd2b5c4ff --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/CONTRIBUTING.md @@ -0,0 +1,219 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](https://tickets.puppet.com/secure/CreateIssue!default.jspa), or [watch the ticket](https://tickets.puppet.com/browse/) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 3. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% rake spec_clean +% rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://puppet.com/community/get-help) + +* [Writing tests](https://docs.puppet.com/guides/module_guides/bgtm.html#step-three-module-testing) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) + + diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/Gemfile b/modules/utilities/windows/shells/puppetlabs_powershell_local/Gemfile new file mode 100644 index 000000000..4d61083ae --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/Gemfile @@ -0,0 +1,153 @@ +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +# Determines what type of gem is requested based on place_or_version. +def gem_type(place_or_version) + if place_or_version =~ /^git:/ + :git + elsif place_or_version =~ /^file:/ + :file + else + :gem + end +end + +# Find a location or specific version for a gem. place_or_version can be a +# version, which is most often used. It can also be git, which is specified as +# `git://somewhere.git#branch`. You can also use a file source location, which +# is specified as `file://some/location/on/disk`. +def location_for(place_or_version, fake_version = nil) + if place_or_version =~ /^(git[:@][^#]*)#(.*)/ + [fake_version, { :git => $1, :branch => $2, :require => false }].compact + elsif place_or_version =~ /^file:\/\/(.*)/ + ['>= 0', { :path => File.expand_path($1), :require => false }] + else + [place_or_version, { :require => false }] + end +end + +# The following gems are not included by default as they require DevKit on Windows. +# You should probably include them in a Gemfile.local or a ~/.gemfile +#gem 'pry' #this may already be included in the gemfile +#gem 'pry-stack_explorer', :require => false +#if RUBY_VERSION =~ /^2/ +# gem 'pry-byebug' +#else +# gem 'pry-debugger' +#end + +group :development do + gem 'rake', :require => false + gem 'rspec', '~>3.0', :require => false + gem 'puppet-lint', :require => false + gem 'puppetlabs_spec_helper', '~>0.10.3', :require => false + gem 'puppet_facts', :require => false + gem 'mocha', '~>0.10.5', :require => false + gem 'pry', :require => false +end + +group :system_tests do + gem 'beaker-rspec', *location_for(ENV['BEAKER_RSPEC_VERSION'] || '~> 5.1') + gem 'beaker', *location_for(ENV['BEAKER_VERSION'] || '~> 2.20') + gem 'beaker-puppet_install_helper', :require => false +end + +# The recommendation is for PROJECT_GEM_VERSION, although there are older ways +# of referencing these. Add them all for compatibility reasons. We'll remove +# later when no issues are known. We'll prefer them in the right order. +puppetversion = ENV['PUPPET_GEM_VERSION'] || ENV['GEM_PUPPET_VERSION'] || ENV['PUPPET_LOCATION'] || '>= 0' +gem 'puppet', *location_for(puppetversion) + +# json_pure 2.0.2 added a requirement on ruby >= 2. We pin to json_pure 2.0.1 +# if using ruby 1.x +gem 'json_pure', '<=2.0.1', :require => false if RUBY_VERSION =~ /^1\./ + +# Only explicitly specify Facter/Hiera if a version has been specified. +# Otherwise it can lead to strange bundler behavior. If you are seeing weird +# gem resolution behavior, try setting `DEBUG_RESOLVER` environment variable +# to `1` and then run bundle install. +facterversion = ENV['FACTER_GEM_VERSION'] || ENV['GEM_FACTER_VERSION'] || ENV['FACTER_LOCATION'] +gem "facter", *location_for(facterversion) if facterversion +hieraversion = ENV['HIERA_GEM_VERSION'] || ENV['GEM_HIERA_VERSION'] || ENV['HIERA_LOCATION'] +gem "hiera", *location_for(hieraversion) if hieraversion + +# For Windows dependencies, these could be required based on the version of +# Puppet you are requiring. Anything greater than v3.5.0 is going to have +# Windows-specific dependencies dictated by the gem itself. The other scenario +# is when you are faking out Puppet to use a local file path / git path. +explicitly_require_windows_gems = false +puppet_gem_location = gem_type(puppetversion) +# This is not a perfect answer to the version check +if puppet_gem_location != :gem || puppetversion < '3.5.0' + if Gem::Platform.local.os == 'mingw32' + explicitly_require_windows_gems = true + end + + if puppet_gem_location == :gem + # If facterversion hasn't been specified and we are + # looking for a Puppet Gem version less than 3.5.0, we + # need to ensure we get a good Facter for specs. + gem "facter",">= 1.6.11","<= 1.7.5",:require => false unless facterversion + # If hieraversion hasn't been specified and we are + # looking for a Puppet Gem version less than 3.5.0, we + # need to ensure we get a good Hiera for specs. + gem "hiera",">= 1.0.0","<= 1.3.0",:require => false unless hieraversion + end +end + +if explicitly_require_windows_gems + # This also means Puppet Gem less than 3.5.0 - this has been tested back + # to 3.0.0. Any further back is likely not supported. + if puppet_gem_location == :gem + gem "ffi", "1.9.0", :require => false + gem "win32-eventlog", "0.5.3","<= 0.6.5", :require => false + gem "win32-process", "0.6.5","<= 0.7.5", :require => false + gem "win32-security", "~> 0.1.2","<= 0.2.5", :require => false + gem "win32-service", "0.7.2","<= 0.8.8", :require => false + gem "minitar", "0.5.4", :require => false + else + gem "ffi", "~> 1.9.0", :require => false + gem "win32-eventlog", "~> 0.5","<= 0.6.5", :require => false + gem "win32-process", "~> 0.6","<= 0.7.5", :require => false + gem "win32-security", "~> 0.1","<= 0.2.5", :require => false + gem "win32-service", "~> 0.7","<= 0.8.8", :require => false + gem "minitar", "~> 0.5.4", :require => false + end + + gem "win32-dir", "~> 0.3","<= 0.4.9", :require => false + gem "win32console", "1.3.2", :require => false if RUBY_VERSION =~ /^1\./ + + # Puppet less than 3.7.0 requires these. + # Puppet 3.5.0+ will control the actual requirements. + # These are listed in formats that work with all versions of + # Puppet from 3.0.0 to 3.6.x. After that, these were no longer used. + # We do not want to allow newer versions than what came out after + # 3.6.x to be used as they constitute some risk in breaking older + # functionality. So we set these to exact versions. + gem "sys-admin", "1.5.6", :require => false + gem "win32-api", "1.4.8", :require => false + gem "win32-taskscheduler", "0.2.2", :require => false + gem "windows-api", "0.4.3", :require => false + gem "windows-pr", "1.2.3", :require => false +else + if Gem::Platform.local.os == 'mingw32' + # If we're using a Puppet gem on windows, which handles its own win32-xxx gem dependencies (Pup 3.5.0 and above), set maximum versions + # Required due to PUP-6445 + gem "win32-dir", "<= 0.4.9", :require => false + gem "win32-eventlog", "<= 0.6.5", :require => false + gem "win32-process", "<= 0.7.5", :require => false + gem "win32-security", "<= 0.2.5", :require => false + gem "win32-service", "<= 0.8.8", :require => false + end +end + +# Evaluate Gemfile.local if it exists +if File.exists? "#{__FILE__}.local" + eval(File.read("#{__FILE__}.local"), binding) +end + +# Evaluate ~/.gemfile if it exists +if File.exists?(File.join(Dir.home, '.gemfile')) + eval(File.read(File.join(Dir.home, '.gemfile')), binding) +end + +# vim:ft=ruby diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/LICENSE b/modules/utilities/windows/shells/puppetlabs_powershell_local/LICENSE new file mode 100644 index 000000000..7a4a3ea24 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/MAINTAINERS.md b/modules/utilities/windows/shells/puppetlabs_powershell_local/MAINTAINERS.md new file mode 100644 index 000000000..19a6f1a0f --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/MAINTAINERS.md @@ -0,0 +1,6 @@ +## Maintenance + +Maintainers: + - Puppet Windows Team `windows |at| puppet |dot| com` + +Tickets: https://tickets.puppet.com/browse/MODULES. Make sure to set component to `powershell`. \ No newline at end of file diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/NOTICE b/modules/utilities/windows/shells/puppetlabs_powershell_local/NOTICE new file mode 100644 index 000000000..c2112aa68 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/NOTICE @@ -0,0 +1,16 @@ +Puppet Module - puppetlabs-powershell + +Copyright 2013 Josh Cooper, original author +Copyright 2013 - 2016 Puppet, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/README.md b/modules/utilities/windows/shells/puppetlabs_powershell_local/README.md new file mode 100644 index 000000000..851e8289c --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/README.md @@ -0,0 +1,213 @@ +# powershell + +#### Table of Contents + +1. [Overview](#overview) +2. [Module Description - What the module does and why it is useful](#module-description) +3. [Setup - The basics of getting started with powershell](#setup) + * [Setup requirements](#setup-requirements) + * [Beginning with powershell](#beginning-with-powershell) +4. [Usage - Configuration options and additional functionality](#usage) + * [External files and exit codes](#external-files-and-exit-codes) + * [Console Error Output](#console-error-output) +5. [Reference - An under-the-hood peek at what the module is doing and how](#reference) +5. [Limitations - OS compatibility, etc.](#limitations) +6. [Development - Guide for contributing to the module](#development) + +## Overview + +This module adds a new exec provider capable of executing PowerShell commands. + +## Module Description + +Puppet provides a built-in `exec` type that is capable of executing commands. This module adds a `powershell` provider to the `exec` type, which enables `exec` parameters, listed below. This module is particularly helpful if you need to run PowerShell commands but don't know the details about how PowerShell is executed, because you can run PowerShell commands in Puppet without the module. + +## Setup + +### Requirements + +This module requires PowerShell to be installed and the `powershell.exe` to be available in the system PATH. + +### Beginning with powershell + +The powershell module adapts the Puppet [exec](http://docs.puppet.com/references/stable/type.html#exec) resource to run PowerShell commands. To get started, install the module and declare 'powershell' in `provider` with the applicable command. + +~~~ puppet +exec { 'RESOURCENAME': + command => '$(SOMECOMMAND)', + provider => powershell, +} +~~~ + +## Usage + +When using `exec` resources with the `powershell` provider, the `command` parameter must be single-quoted to prevent Puppet from interpolating `$(..)`. + +For instance, to rename the Guest account: + +~~~ puppet +exec { 'rename-guest': + command => '$(Get-WMIObject Win32_UserAccount -Filter "Name=\'guest\'").Rename("new-guest")', + unless => 'if (Get-WmiObject Win32_UserAccount -Filter "Name=\'guest\'") { exit 1 }', + provider => powershell, +} +~~~ + +Note that the example uses the `unless` parameter to make the resource idempotent. The `command` is only executed if the Guest account does not exist, as indicated by `unless` returning 0. + +**Note:** PowerShell variables (such as `$_`) must be escaped in Puppet manifests either using backslashes or single quotes. + +Alternately, you can put the PowerShell code for the `command`, `onlyif`, and `unless` parameters into separate files, and then invoke the file function in the resource. You could also use templates and the `template()` function if the PowerShell scripts need access to variables from Puppet. + +~~~ puppet +exec { 'rename-guest': + command => file('guest/rename-guest.ps1'), + onlyif => file('guest/guest-exists.ps1'), + provider => powershell, + logoutput => true, +} +~~~ + +Each file is a PowerShell script that should be in the module's `files/` folder. + +For example, here is the script at: `guest/files/rename-guest.ps1` + +~~~ powershell +$obj = $(Get-WMIObject Win32_UserAccount -Filter "Name='Guest'") +$obj.Rename("OtherGuest") +~~~ + +This has the added benefit of not requiring escaping '$' in the PowerShell code. Note that the files must have DOS linefeeds or they will not work as expected. One tool for converting UNIX linefeeds to DOS linefeeds is [unix2dos](http://freecode.com/projects/dos2unix). + +### External files and exit codes + +If you are calling external files, such as other PowerShell scripts or executables, be aware that the last executed script's exitcode is used by Puppet to determine whether the command was successful. + +For example, if the file `C:\fail.ps1` contains the following PowerShell script: + +~~~ powershell +& cmd /c EXIT 5 +& cmd /c EXIT 1 +~~~ + +and we use the following Puppet manifest: + +~~~ puppet +exec { 'test': + command => '& C:\fail.ps1', + provider => powershell, +} +~~~ + +Then the `exec['test']` resource will always fail, because the last exit code from the external file `C:\fail.ps1` is `1`. This behavior might have unintended consequences if you combine multiple external files. + +To stop this behavior, ensure that you use explicit `Exit` statements in your PowerShell scripts. For example, we changed the Puppet manifest from the above to: + +~~~ puppet +exec { 'test': + command => '& C:\fail.ps1; Exit 0', + provider => powershell, +} +~~~ + +This will always succeed because the `Exit 0` statement overrides the exit code from the `C:\fail.ps1` script. + +### Console Error Output + +The PowerShell module internally captures output sent to the .NET `[System.Console]::Error` stream like: + +~~~ puppet +exec { 'test': + command => '[System.Console]::Error.WriteLine("foo")', + provider => powershell, +} +~~~ + +However, to produce output from a script, use the `Write-` prefixed cmdlets such as `Write-Output`, `Write-Debug` and `Write-Error`. + +## Reference + +#### Provider + +* powershell: Adapts the Puppet `exec` resource to run PowerShell commands. + +#### Parameters + +All parameters are optional. + +##### `creates` + +Specifies the file to look for before running the command. The command runs only if the file doesn't exist. **Note: This parameter does not create a file, it only looks for one.** Valid options: A string of the path to the file. Default: Undefined. + +##### `cwd` + +Sets the directory from which to run the command. Valid options: A string of the directory path. Default: Undefined. + +##### `command` + +Specifies the actual PowerShell command to execute. Must either be fully qualified or a search path for the command must be provided. Valid options: String. Default: Undefined. + +##### `environment` + +Sets additional environment variables to set for a command. Valid options: String, or an array of multiple options. Default: Undefined. + +##### `logoutput` + +Defines whether to log command output in addition to logging the exit code. If you specify 'on_failure', it only logs the output when the command has an exit code that does not match any value specified by the `returns` attribute. Valid options: true, false, and 'on_failure'. Default: 'on_failure'. + +##### `onlyif` + +Runs the exec only if the command returns 0. Valid options: String. Default: Undefined. + +##### `path` + +Specifies the search path used for command execution. Valid options: String of the path, an array, or a semicolon-separated list. Default: Undefined. + +##### `refresh` + +Refreshes the command. Valid options: String. Default: Undefined. + +##### `refreshonly` + +Refreshes the command only when a dependent object is changed. Used with `subscribe` and `notify` [metaparameters](http://docs.puppet.com/references/latest/metaparameter.html). Valid options: true, false. Default: false. + +##### `returns` + +Lists the expected return code(s). If the executed command returns something else, an error is returned. Valid options: An array of acceptable return codes or a single value. Default: 0. + +##### `timeout` + +Sets the maximum time in seconds that the command should take. Valid options: Number or string representation of a number. Default: 300. + +##### `tries` + +Determines the number of times execution of the command should be attempted. Valid options: Number or a string representation of a number. Default: '1'. + +##### `try_sleep` + +Specifies the time to sleep in seconds between `tries`. Valid options: Number or a string representation of a number. Default: Undefined. + +##### `unless` + +Runs the `exec`, unless the command returns 0. Valid options: String. Default: Undefined. + +## Limitations + +* Only supported on Windows Server 2008 and above, and Windows 7 and above. + +* Only supported on Powershell 2.0 and above. + +* When using here-strings in inline or templated scripts executed by this module, you must use the double-quote style syntax that begins with `@"` and ends with `"@`. The single-quote syntax that begins with `@'` and ends with `'@` is not supported. + + Note that any external .ps1 script file loaded or executed with the call operator `&` is not subject to this limitation and can contain any style here-string. For instance, the script file external-code.ps1 can contain any style of here-string: + + ``` + exec { 'external-code': + command => '& C:\external-code.ps1', + provider => powershell, + } + ``` + +## Development + +Puppet modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can’t access the huge number of platforms and myriad hardware, software, and deployment configurations that Puppet is intended to serve. We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. For more information, see our [module contribution guide.](https://docs.puppet.com/forge/contributing.html) diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/Rakefile b/modules/utilities/windows/shells/puppetlabs_powershell_local/Rakefile new file mode 100644 index 000000000..dd489f127 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/Rakefile @@ -0,0 +1,28 @@ +require 'puppetlabs_spec_helper/rake_tasks' +require 'rspec/core/rake_task' + +task :default => :unit + +desc "Unit tests" +RSpec::Core::RakeTask.new(:unit) do |t,args| + t.pattern = 'spec/unit' + t.rspec_opts = '--color' + t.verbose = true +end + +desc "Beaker namespace" +RSpec::Core::RakeTask.new('beaker:rspec:test:pe',:host) do |t,args| + args.with_defaults({:host => 'default'}) + ENV['BEAKER_set'] = args[:host] + t.pattern = 'spec/acceptance' + t.rspec_opts = '--color' + t.verbose = true +end + +RSpec::Core::RakeTask.new('beaker:rspec:test:git',:host) do |t,args| + args.with_defaults({:host => 'default'}) + ENV['BEAKER_set'] = args[:host] + t.pattern = 'spec/acceptance' + t.rspec_opts = '--color' + t.verbose = true +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/appveyor.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/appveyor.yml new file mode 100644 index 000000000..cb3370781 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/appveyor.yml @@ -0,0 +1,44 @@ +version: 1.1.x.{build} +skip_commits: + message: /^\(?doc\)?.*/ +clone_depth: 10 +init: +- SET +- 'mkdir C:\ProgramData\PuppetLabs\code && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\facter && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\hiera && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\puppet\var && exit 0' +environment: + matrix: + - PUPPET_GEM_VERSION: ~> 3.0 + RUBY_VER: 193 + - PUPPET_GEM_VERSION: ~> 3.0 + RUBY_VER: 200-x64 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 21 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 21-x64 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 23 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 23-x64 + - PUPPET_GEM_VERSION: 3.0.0 + RUBY_VER: 193 +matrix: + fast_finish: true +install: +- SET PATH=C:\Ruby%RUBY_VER%\bin;%PATH% +- bundle install --jobs 4 --retry 2 --without system_tests +- type Gemfile.lock +build: off +test_script: +- bundle exec puppet -V +- ruby -v +- bundle exec rspec spec/unit spec/integration -fd -b +notifications: +- provider: Email + to: + - nobody@nowhere.com + on_build_success: false + on_build_failure: false + on_build_status_changed: false diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/checksums.json b/modules/utilities/windows/shells/puppetlabs_powershell_local/checksums.json new file mode 100644 index 000000000..be90cd965 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/checksums.json @@ -0,0 +1,34 @@ +{ + "CHANGELOG.md": "631181084f8f3cb5803f2f7d7d7bb721", + "CONTRIBUTING.md": "2398672bb3e7c2a5ec11a9cea0f809e9", + "Gemfile": "9773686d3171abe4a41875e17244518b", + "LICENSE": "175792518e4ac015ab6696d16c4f607e", + "MAINTAINERS.md": "73b03f2d924a8c904b8ba2aa8034a756", + "NOTICE": "89aabfb0d9b2b991537fe6529d55e8f8", + "README.md": "56a6af068faf00916a87cada55a36c49", + "Rakefile": "b7fa47f72583e517f019ab58f48853f2", + "appveyor.yml": "b4ef9f7c0f183338230ef171465c9622", + "lib/puppet/provider/exec/powershell.rb": "0c7788b88dcdd69156f5ee91e9617f05", + "lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb": "d85ac07f9b23964804f400737c846fb8", + "lib/puppet_x/puppetlabs/powershell/powershell_manager.rb": "2811c13a1954d3cf8eb456c4fa9c80ef", + "lib/puppet_x/puppetlabs/powershell/powershell_version.rb": "0c8298e41ff0216f1f6ad32d2a5ce010", + "lib/puppet_x/templates/init_ps.ps1": "823c389299afb88bfbf56f6a8247bef0", + "metadata.json": "c5c4893b73c5c98ba9a1b9ab5857317c", + "spec/acceptance/exec_powershell_spec.rb": "8a8fa3a2c2435ebf349bedc5be4f004c", + "spec/acceptance/files/param_script.ps1": "942dc0c5aaf3880732b610bdae165cba", + "spec/acceptance/files/services.ps1": "6281d2a9de037d17514ba1618bc8d63f", + "spec/acceptance/nodesets/windows-2003-i386.yml": "ee44e67756ecd0c07e08ae8ec91684cd", + "spec/acceptance/nodesets/windows-2003-x86_64.yml": "33f48b5729235a6764880f18ddfdf976", + "spec/acceptance/nodesets/windows-2008-x86_64.yml": "78c97561cc383cde9e18d612aeafa335", + "spec/acceptance/nodesets/windows-2008r2-x86_64.yml": "bf563a036e054f0f50cc271d6cd819e6", + "spec/acceptance/nodesets/windows-2012-x86_64.yml": "678b95ea73e959c79d8723f35f66bc54", + "spec/acceptance/nodesets/windows-2012r2-x86_64.yml": "f1f44c34a702307974dd1cff2909f402", + "spec/exit-27.ps1": "97c8ee0412556a78aa7ebd42aaa00435", + "spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb": "37e01fcae539ccc83dfe6efbcebc304b", + "spec/spec.opts": "a600ded995d948e393fbe2320ba8e51c", + "spec/spec_helper.rb": "c8dba9cd09f111851024bbe1574b1228", + "spec/spec_helper_acceptance.rb": "8869e99d1b25a73cb075ecf663d68a37", + "spec/unit/provider/exec/powershell_spec.rb": "0225775557ea001e978570fb930bd45b", + "spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb": "023da68379904c6d424193343d227c1d", + "spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb": "4e60382cc58e0466388d71299d9e8d0a" +} \ No newline at end of file diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet/provider/exec/powershell.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet/provider/exec/powershell.rb new file mode 100644 index 000000000..e2f13d9f0 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet/provider/exec/powershell.rb @@ -0,0 +1,139 @@ +require 'puppet/provider/exec' +require File.join(File.dirname(__FILE__), '../../../puppet_x/puppetlabs/powershell/compatible_powershell_version') +require File.join(File.dirname(__FILE__), '../../../puppet_x/puppetlabs/powershell/powershell_manager') + +Puppet::Type.type(:exec).provide :powershell, :parent => Puppet::Provider::Exec do + confine :operatingsystem => :windows + + commands :powershell => + if File.exists?("#{ENV['SYSTEMROOT']}\\sysnative\\WindowsPowershell\\v1.0\\powershell.exe") + "#{ENV['SYSTEMROOT']}\\sysnative\\WindowsPowershell\\v1.0\\powershell.exe" + elsif File.exists?("#{ENV['SYSTEMROOT']}\\system32\\WindowsPowershell\\v1.0\\powershell.exe") + "#{ENV['SYSTEMROOT']}\\system32\\WindowsPowershell\\v1.0\\powershell.exe" + else + 'powershell.exe' + end + + desc <<-EOT + Executes Powershell commands. One of the `onlyif`, `unless`, or `creates` + parameters should be specified to ensure the command is idempotent. + + Example: + # Rename the Guest account + exec { 'rename-guest': + command => '$(Get-WMIObject Win32_UserAccount -Filter "Name=\'guest\'").Rename("new-guest")', + unless => 'if (Get-WmiObject Win32_UserAccount -Filter "Name=\'guest\'") { exit 1 }', + provider => powershell, + } + EOT + + POWERSHELL_UPGRADE_MSG = <<-UPGRADE + Currently, the PowerShell module has reduced v1 functionality on this agent + due to one or more of the following conditions: + + - Puppet 3.x (non-x64 version) + + Puppet 3.x uses a Ruby version that requires a library to support a colored + console. Unfortunately this library prevents the PowerShell module from + using a shared PowerShell process to dramatically improve the performance of + resource application. + + - PowerShell v2 with .NET Framework 2.0 + + PowerShell v2 works with both .NET Framework 2.0 and .NET Framework 3.5. + To be able to use the enhancements, we require at least .NET Framework 3.5. + Typically you will only see this on a base Windows Server 2008 (and R2) + install. + + To enable these improvements, it is suggested to upgrade to any x64 version of + Puppet (including 3.x), or to a Puppet version newer than 3.x and ensure you + have at least .NET Framework 3.5 installed. + UPGRADE + + def self.upgrade_message + Puppet.warning POWERSHELL_UPGRADE_MSG if !@upgrade_warning_issued + @upgrade_warning_issued = true + end + + def self.powershell_args + ps_args = ['-NoProfile', '-NonInteractive', '-NoLogo', '-ExecutionPolicy', 'Bypass'] + ps_args << '-Command' if !PuppetX::PowerShell::PowerShellManager.supported? + + ps_args + end + + def ps_manager + debug_output = Puppet::Util::Log.level == :debug + manager_args = "#{command(:powershell)} #{self.class.powershell_args().join(' ')}" + PuppetX::PowerShell::PowerShellManager.instance(manager_args, debug_output) + end + + def run(command, check = false) + if !PuppetX::PowerShell::PowerShellManager.supported? + self.class.upgrade_message + write_script(command) do |native_path| + # Ideally, we could keep a handle open on the temp file in this + # process (to prevent TOCTOU attacks), and execute powershell + # with -File . But powershell complains that it can't open + # the file for exclusive access. If we close the handle, then an + # attacker could modify the file before we invoke powershell. So + # we redirect powershell's stdin to read from the file. Current + # versions of Windows use per-user temp directories with strong + # permissions, but I'd rather not make (poor) assumptions. + return super("cmd.exe /c \"\"#{native_path(command(:powershell))}\" #{legacy_args} -Command - < \"#{native_path}\"\"", check) + end + else + working_dir = resource[:cwd] + if (!working_dir.nil?) + self.fail "Working directory '#{working_dir}' does not exist" unless File.directory?(working_dir) + end + timeout_ms = resource[:timeout].nil? ? nil : resource[:timeout] * 1000 + + result = ps_manager.execute(command,timeout_ms,working_dir) + + stdout = result[:stdout] + native_out = result[:native_stdout] + stderr = result[:stderr] + exit_code = result[:exitcode] + + unless stderr.nil? + stderr.each { |e| Puppet.debug "STDERR: #{e.chop}" unless e.empty? } + end + + Puppet.debug "STDERR: #{result[:errormessage]}" unless result[:errormessage].nil? + + output = Puppet::Util::Execution::ProcessOutput.new(stdout.to_s + native_out.to_s, exit_code) + + return output, output + end + end + + def checkexe(command) + end + + def validatecmd(command) + true + end + + private + def write_script(content, &block) + Tempfile.open(['puppet-powershell', '.ps1']) do |file| + file.puts(content) + file.puts() + file.flush + yield native_path(file.path) + end + end + + def native_path(path) + if Puppet::Util::Platform.windows? + path.gsub(File::SEPARATOR, File::ALT_SEPARATOR) + else + path + end + end + + def legacy_args + '-NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass' + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb new file mode 100644 index 000000000..b97b06a2f --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/compatible_powershell_version.rb @@ -0,0 +1,51 @@ +require File.join(File.dirname(__FILE__), 'powershell_version') + +module PuppetX + module PuppetLabs + module PowerShell + class CompatiblePowerShellVersion + def self.compatible_version? + value = false + + powershell_version = PuppetX::PuppetLabs::PowerShell::PowerShellVersion.version + + return false if powershell_version.nil? + + # PowerShell v1 - definitely not good to go. Really the entire module + # may not even work but I digress + return false if Gem::Version.new(powershell_version) < Gem::Version.new(2) + + # PowerShell v3+, we are good to go b/c .NET 4+ + # https://msdn.microsoft.com/en-us/powershell/scripting/setup/windows-powershell-system-requirements + # Look at Microsoft .NET Framwork Requirements section. + if Gem::Version.new(powershell_version) >= Gem::Version.new(3) + return true + end + + # If we are using PowerShell v2, we need to see what the latest + # version of .NET is that we have + # https://msdn.microsoft.com/en-us/library/hh925568.aspx + if Puppet::Util::Platform.windows? + require 'win32/registry' + + begin + # At this point in the check, PowerShell is using .NET Framework + # 2.x family, so we only need to verify v3.5 key exists. + # If we were verifying all compatible types we would look for + # any of these keys: v3.5, v4.0, v4 + hive = Win32::Registry::HKEY_LOCAL_MACHINE + # redirection doesn't actually matter here - disable it anyway + hive.open('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100) do |reg| + value = true + end + rescue Win32::Registry::Error => e + value = false + end + end + + value + end + end + end + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb new file mode 100644 index 000000000..38ab51487 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_manager.rb @@ -0,0 +1,328 @@ +require 'rexml/document' +require 'securerandom' +require 'open3' +require 'base64' +require File.join(File.dirname(__FILE__), 'compatible_powershell_version') + +module PuppetX + module PowerShell + class PowerShellManager + @@instances = {} + + def self.instance(cmd, debug = false) + key = cmd + debug.to_s + manager = @@instances[key] + + if manager.nil? || !manager.alive? + # ignore any errors trying to tear down this unusable instance + manager.exit if manager rescue nil + @@instances[key] = PowerShellManager.new(cmd, debug) + end + + @@instances[key] + end + + def self.win32console_enabled? + @win32console_enabled ||= defined?(Win32) && + defined?(Win32::Console) && + Win32::Console.class == Class + end + + def self.compatible_version_of_powershell? + @compatible_powershell_version ||= PuppetX::PuppetLabs::PowerShell::CompatiblePowerShellVersion.compatible_version? + end + + def self.supported? + Puppet::Util::Platform.windows? && + compatible_version_of_powershell? && + !win32console_enabled? + end + + def initialize(cmd, debug) + @usable = true + + named_pipe_name = "#{SecureRandom.uuid}PuppetPsHost" + + ps_args = ['-File', self.class.init_path, "\"#{named_pipe_name}\""] + ps_args << '"-EmitDebugOutput"' if debug + # @stderr should never be written to as PowerShell host redirects output + stdin, @stdout, @stderr, @ps_process = Open3.popen3("#{cmd} #{ps_args.join(' ')}") + stdin.close + + Puppet.debug "#{Time.now} #{cmd} is running as pid: #{@ps_process[:pid]}" + + pipe_path = "\\\\.\\pipe\\#{named_pipe_name}" + # wait for the pipe server to signal ready, and fail if no response in 10 seconds + + # wait up to 10 seconds in 0.2 second intervals to be able to open the pipe + 50.times do + begin + # pipe is opened in binary mode and must always + @pipe = File.open(pipe_path, 'r+b') + break + rescue + sleep 0.2 + end + end + + fail "Failure waiting for PowerShell process #{@ps_process[:pid]} to start pipe server" if @pipe.nil? + + Puppet.debug "#{Time.now} PowerShell initialization complete for pid: #{@ps_process[:pid]}" + + at_exit { exit } + end + + def alive? + # powershell process running + @ps_process.alive? && + # explicitly set during a read / write failure, like broken pipe EPIPE + @usable && + # an explicit failure state might not have been hit, but IO may be closed + self.class.is_stream_valid?(@pipe) && + self.class.is_stream_valid?(@stdout) && + self.class.is_stream_valid?(@stderr) + end + + def execute(powershell_code, timeout_ms = nil, working_dir = nil) + code = make_ps_code(powershell_code, timeout_ms, working_dir) + + + # err is drained stderr pipe (not captured by redirection inside PS) + # or during a failure, a Ruby callstack array + out, native_stdout, err = exec_read_result(code) + + # an error was caught during execution that has invalidated any results + return { :exitcode => -1, :stderr => err } if !@usable && out.nil? + + out[:exitcode] = out[:exitcode].to_i if !out[:exitcode].nil? + # if err contains data it must be "real" stderr output + # which should be appended to what PS has already captured + out[:stderr] = out[:stderr].nil? ? [] : [out[:stderr]] + out[:stderr] += err if !err.nil? + out[:native_stdout] = native_stdout + + out + end + + def exit + @usable = false + + Puppet.debug "PowerShellManager exiting..." + # pipe may still be open, but if stdout / stderr are dead PS process is in trouble + # and will block forever on a write to the pipe + # its safer to close pipe on Ruby side, which gracefully shuts down PS side + @pipe.close if !@pipe.closed? + @stdout.close if !@stdout.closed? + @stderr.close if !@stderr.closed? + + # wait up to 2 seconds for the watcher thread to fully exit + @ps_process.join(2) + end + + def self.init_path + # a PowerShell -File compatible path to bootstrap the instance + path = File.expand_path('../../../templates', __FILE__) + path = File.join(path, 'init_ps.ps1').gsub('/', '\\') + "\"#{path}\"" + end + + def make_ps_code(powershell_code, timeout_ms = nil, working_dir = nil) + begin + timeout_ms = Integer(timeout_ms) + # Lower bound protection. The polling resolution is only 50ms + if (timeout_ms < 50) then timeout_ms = 50 end + rescue + timeout_ms = 300 * 1000 + end + # PS side expects Invoke-PowerShellUserCode is always the return value here + <<-CODE +$params = @{ + Code = @' +#{powershell_code} +'@ + TimeoutMilliseconds = #{timeout_ms} + WorkingDirectory = "#{working_dir}" +} + +Invoke-PowerShellUserCode @params + CODE + end + + private + + def self.is_readable?(stream, timeout = 0.5) + raise Errno::EPIPE if !is_stream_valid?(stream) + read_ready = IO.select([stream], [], [], timeout) + read_ready && stream == read_ready[0][0] + end + + # when a stream has been closed by handle, but Ruby still has a file + # descriptor for it, it can be tricky to determine that it's actually dead + # the .fileno will still return an int, and calling get_osfhandle against + # it returns what the CRT thinks is a valid Windows HANDLE value, but + # that may no longer exist + def self.is_stream_valid?(stream) + # when a stream is closed, its obviously invalid, but Ruby doesn't always know + !stream.closed? && + # so calling stat will yield an EBADF when underlying OS handle is bad + # as this resolves to a HANDLE and then calls the Windows API + !stream.stat.nil? + # any exceptions mean the stream is dead + rescue + false + end + + # copied directly from Puppet 3.7+ to support Puppet 3.5+ + def self.wide_string(str) + # ruby (< 2.1) does not respect multibyte terminators, so it is possible + # for a string to contain a single trailing null byte, followed by garbage + # causing buffer overruns. + # + # See http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?revision=41920&view=revision + newstr = str + "\0".encode(str.encoding) + newstr.encode!('UTF-16LE') + end + + # mutates the given bytes, removing the length prefixed vaule + def self.read_length_prefixed_string(bytes) + # 32-bit integer in Little Endian format + length = bytes.slice!(0, 4).unpack('V').first + return nil if length == 0 + bytes.slice!(0, length).force_encoding(Encoding::UTF_8) + end + + # bytes is a binary string containing a list of length-prefixed + # key / value pairs (of UTF-8 encoded strings) + # this method mutates the incoming value + def self.ps_output_to_hash(bytes) + hash = {} + while !bytes.empty? + hash[read_length_prefixed_string(bytes).to_sym] = read_length_prefixed_string(bytes) + end + + hash + end + + # 1 byte command identifier + # 0 - Exit + # 1 - Execute + def pipe_command(command) + case command + when :exit + "\x00" + when :execute + "\x01" + end + end + + # Data format is: + # 4 bytes - Little Endian encoded 32-bit integer length of string + # Intel CPUs are little endian, hence the .NET Framework typically is + # variable length - UTF8 encoded string bytes + def pipe_data(data) + msg = data.encode(Encoding::UTF_8) + # https://ruby-doc.org/core-1.9.3/Array.html#method-i-pack + [msg.bytes.length].pack('V') + msg.force_encoding(Encoding::BINARY) + end + + def write_pipe(input) + # for compat with Ruby 2.1 and lower, its important to use syswrite and not write + # otherwise the pipe breaks after writing 1024 bytes + written = @pipe.syswrite(input) + @pipe.flush() + + if written != input.length + msg = "Only wrote #{written} out of #{input.length} expected bytes to PowerShell pipe" + raise Errno::EPIPE.new(msg) + end + end + + def read_from_pipe(pipe, timeout = 0.1, &block) + if self.class.is_readable?(pipe, timeout) + l = pipe.readpartial(4096) + Puppet.debug "#{Time.now} PIPE> #{l}" + # since readpartial may return a nil at EOF, skip returning that value + yield l if !l.nil? + end + + nil + end + + def drain_pipe_until_signaled(pipe, signal) + output = [] + + read_from_pipe(pipe) { |s| output << s } until !signal.locked? + + # there's ultimately a bit of a race here + # read one more time after signal is received + read_from_pipe(pipe, 0) { |s| output << s } until !self.class.is_readable?(pipe) + + # string has been binary up to this point, so force UTF-8 now + output == [] ? + [] : + [output.join('').force_encoding(Encoding::UTF_8)] + end + + def read_streams + pipe_done_reading = Mutex.new + pipe_done_reading.lock + start_time = Time.now + + stdout_reader = Thread.new { drain_pipe_until_signaled(@stdout, pipe_done_reading) } + stderr_reader = Thread.new { drain_pipe_until_signaled(@stderr, pipe_done_reading) } + pipe_reader = Thread.new(@pipe) do |pipe| + # read a Little Endian 32-bit integer for length of response + expected_response_length = pipe.sysread(4).unpack('V').first + return nil if expected_response_length == 0 + + # reads the expected bytes as a binary string or fails + pipe.sysread(expected_response_length) + end + + Puppet.debug "Waited #{Time.now - start_time} total seconds." + + # block until sysread has completed or errors + begin + output = pipe_reader.value + output = self.class.ps_output_to_hash(output) if !output.nil? + ensure + # signal stdout / stderr readers via mutex + # so that Ruby doesn't crash waiting on an invalid event + pipe_done_reading.unlock + end + + # given redirection on PowerShell side, this should always be empty + stdout = stdout_reader.value + + [ + output, + stdout == [] ? nil : stdout.join(''), # native stdout + stderr_reader.value # native stderr + ] + ensure + # failsafe if the prior unlock was never reached / Mutex wasn't unlocked + pipe_done_reading.unlock if pipe_done_reading.locked? + # wait for all non-nil threads to see mutex unlocked and finish + [pipe_reader, stdout_reader, stderr_reader].compact.each(&:join) + end + + def exec_read_result(powershell_code) + write_pipe(pipe_command(:execute)) + write_pipe(pipe_data(powershell_code)) + read_streams() + # if any pipes are broken, the manager is totally hosed + # bad file descriptors mean closed stream handles + # EOFError is a closed pipe (could be as a result of tearing down process) + rescue Errno::EPIPE, Errno::EBADF, EOFError => e + @usable = false + return nil, nil, [e.inspect, e.backtrace].flatten + # catch closed stream errors specifically + rescue IOError => ioerror + raise if !ioerror.message.start_with?('closed stream') + @usable = false + return nil, nil, [ioerror.inspect, ioerror.backtrace].flatten + end + + end + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_version.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_version.rb new file mode 100644 index 000000000..7fe9f7b66 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/puppetlabs/powershell/powershell_version.rb @@ -0,0 +1,53 @@ +module PuppetX + module PuppetLabs + module PowerShell + class PowerShellVersion + end + end + end +end + +if Puppet::Util::Platform.windows? + require 'win32/registry' + module PuppetX + module PuppetLabs + module PowerShell + class PowerShellVersion + ACCESS_TYPE = Win32::Registry::KEY_READ | 0x100 + HKLM = Win32::Registry::HKEY_LOCAL_MACHINE + PS_ONE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine' + PS_THREE_REG_PATH = 'SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine' + REG_KEY = 'PowerShellVersion' + + def self.version + powershell_three_version || powershell_one_version + end + + def self.powershell_one_version + version = nil + begin + HKLM.open(PS_ONE_REG_PATH, ACCESS_TYPE) do |reg| + version = reg[REG_KEY] + end + rescue + version = nil + end + version + end + + def self.powershell_three_version + version = nil + begin + HKLM.open(PS_THREE_REG_PATH, ACCESS_TYPE) do |reg| + version = reg[REG_KEY] + end + rescue + version = nil + end + version + end + end + end + end + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/templates/init_ps.ps1 b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/templates/init_ps.ps1 new file mode 100644 index 000000000..5a54284a5 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/lib/puppet_x/templates/init_ps.ps1 @@ -0,0 +1,787 @@ +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [String] + $NamedPipeName, + + [Parameter(Mandatory = $false)] + [Switch] + $EmitDebugOutput = $False, + + [Parameter(Mandatory = $false)] + [System.Text.Encoding] + $Encoding = [System.Text.Encoding]::UTF8 +) + +$script:EmitDebugOutput = $EmitDebugOutput +# Necessary for [System.Console]::Error.WriteLine to roundtrip with UTF-8 +[System.Console]::OutputEncoding = $Encoding + +$hostSource = @" +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Security; +using System.Text; +using System.Threading; + +namespace Puppet +{ + public class PuppetPSHostRawUserInterface : PSHostRawUserInterface + { + public PuppetPSHostRawUserInterface() + { + buffersize = new Size(120, 120); + backgroundcolor = ConsoleColor.Black; + foregroundcolor = ConsoleColor.White; + cursorposition = new Coordinates(0, 0); + cursorsize = 1; + } + + private ConsoleColor backgroundcolor; + public override ConsoleColor BackgroundColor + { + get { return backgroundcolor; } + set { backgroundcolor = value; } + } + + private Size buffersize; + public override Size BufferSize + { + get { return buffersize; } + set { buffersize = value; } + } + + private Coordinates cursorposition; + public override Coordinates CursorPosition + { + get { return cursorposition; } + set { cursorposition = value; } + } + + private int cursorsize; + public override int CursorSize + { + get { return cursorsize; } + set { cursorsize = value; } + } + + private ConsoleColor foregroundcolor; + public override ConsoleColor ForegroundColor + { + get { return foregroundcolor; } + set { foregroundcolor = value; } + } + + private Coordinates windowposition; + public override Coordinates WindowPosition + { + get { return windowposition; } + set { windowposition = value; } + } + + private Size windowsize; + public override Size WindowSize + { + get { return windowsize; } + set { windowsize = value; } + } + + private string windowtitle; + public override string WindowTitle + { + get { return windowtitle; } + set { windowtitle = value; } + } + + public override bool KeyAvailable + { + get { return false; } + } + + public override Size MaxPhysicalWindowSize + { + get { return new Size(165, 66); } + } + + public override Size MaxWindowSize + { + get { return new Size(165, 66); } + } + + public override void FlushInputBuffer() + { + throw new NotImplementedException(); + } + + public override BufferCell[,] GetBufferContents(Rectangle rectangle) + { + throw new NotImplementedException(); + } + + public override KeyInfo ReadKey(ReadKeyOptions options) + { + throw new NotImplementedException(); + } + + public override void ScrollBufferContents(Rectangle source, Coordinates destination, Rectangle clip, BufferCell fill) + { + throw new NotImplementedException(); + } + + public override void SetBufferContents(Rectangle rectangle, BufferCell fill) + { + throw new NotImplementedException(); + } + + public override void SetBufferContents(Coordinates origin, BufferCell[,] contents) + { + throw new NotImplementedException(); + } + } + + public class PuppetPSHostUserInterface : PSHostUserInterface + { + private PuppetPSHostRawUserInterface _rawui; + private StringBuilder _sb; + private StringWriter _errWriter; + private StringWriter _outWriter; + + public PuppetPSHostUserInterface() + { + _sb = new StringBuilder(); + _errWriter = new StringWriter(new StringBuilder()); + // NOTE: StringWriter / StringBuilder are not technically thread-safe + // but PowerShell Write-XXX cmdlets and System.Console.Out.WriteXXX + // should not be executed concurrently within PowerShell, so should be safe + _outWriter = new StringWriter(_sb); + } + + public override PSHostRawUserInterface RawUI + { + get + { + if ( _rawui == null){ + _rawui = new PuppetPSHostRawUserInterface(); + } + return _rawui; + } + } + + public void ResetConsoleStreams() + { + System.Console.SetError(_errWriter); + System.Console.SetOut(_outWriter); + } + + public override void Write(ConsoleColor foregroundColor, ConsoleColor backgroundColor, string value) + { + _sb.Append(value); + } + + public override void Write(string value) + { + _sb.Append(value); + } + + public override void WriteDebugLine(string message) + { + _sb.AppendLine("DEBUG: " + message); + } + + public override void WriteErrorLine(string value) + { + _sb.AppendLine(value); + } + + public override void WriteLine(string value) + { + _sb.AppendLine(value); + } + + public override void WriteVerboseLine(string message) + { + _sb.AppendLine("VERBOSE: " + message); + } + + public override void WriteWarningLine(string message) + { + _sb.AppendLine("WARNING: " + message); + } + + public override void WriteProgress(long sourceId, ProgressRecord record) + { + } + + public string Output + { + get + { + _outWriter.Flush(); + string text = _outWriter.GetStringBuilder().ToString(); + _outWriter.GetStringBuilder().Length = 0; // Only .NET 4+ has .Clear() + return text; + } + } + + public string StdErr + { + get + { + _errWriter.Flush(); + string text = _errWriter.GetStringBuilder().ToString(); + _errWriter.GetStringBuilder().Length = 0; // Only .NET 4+ has .Clear() + return text; + } + } + + public override Dictionary Prompt(string caption, string message, Collection descriptions) + { + throw new NotImplementedException(); + } + + public override int PromptForChoice(string caption, string message, Collection choices, int defaultChoice) + { + throw new NotImplementedException(); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName) + { + throw new NotImplementedException(); + } + + public override PSCredential PromptForCredential(string caption, string message, string userName, string targetName, PSCredentialTypes allowedCredentialTypes, PSCredentialUIOptions options) + { + throw new NotImplementedException(); + } + + public override string ReadLine() + { + throw new NotImplementedException(); + } + + public override SecureString ReadLineAsSecureString() + { + throw new NotImplementedException(); + } + } + + public class PuppetPSHost : PSHost + { + private Guid _hostId = Guid.NewGuid(); + private bool shouldExit; + private int exitCode; + + private readonly PuppetPSHostUserInterface _ui = new PuppetPSHostUserInterface(); + + public PuppetPSHost () {} + + public bool ShouldExit { get { return this.shouldExit; } } + public int ExitCode { get { return this.exitCode; } } + public void ResetExitStatus() + { + this.exitCode = 0; + this.shouldExit = false; + } + public void ResetConsoleStreams() + { + _ui.ResetConsoleStreams(); + } + + public override Guid InstanceId { get { return _hostId; } } + public override string Name { get { return "PuppetPSHost"; } } + public override Version Version { get { return new Version(1, 1); } } + public override PSHostUserInterface UI + { + get { return _ui; } + } + public override CultureInfo CurrentCulture + { + get { return Thread.CurrentThread.CurrentCulture; } + } + public override CultureInfo CurrentUICulture + { + get { return Thread.CurrentThread.CurrentUICulture; } + } + + public override void EnterNestedPrompt() { throw new NotImplementedException(); } + public override void ExitNestedPrompt() { throw new NotImplementedException(); } + public override void NotifyBeginApplication() { return; } + public override void NotifyEndApplication() { return; } + + public override void SetShouldExit(int exitCode) + { + this.shouldExit = true; + this.exitCode = exitCode; + } + } +} +"@ + +Add-Type -TypeDefinition $hostSource -Language CSharp +$global:DefaultWorkingDirectory = (Get-Location -PSProvider FileSystem).Path + +#this is a string so we can import into our dynamic PS instance +$global:ourFunctions = @' +function Get-ProcessEnvironmentVariables +{ + $processVars = [Environment]::GetEnvironmentVariables('Process').Keys | + % -Begin { $h = @{} } -Process { $h.$_ = (Get-Item Env:\$_).Value } -End { $h } + + # eliminate Machine / User vars so that we have only process vars + 'Machine', 'User' | + % { [Environment]::GetEnvironmentVariables($_).GetEnumerator() } | + ? { $processVars.ContainsKey($_.Name) -and ($processVars[$_.Name] -eq $_.Value) } | + % { $processVars.Remove($_.Name) } + + $processVars.GetEnumerator() | Sort-Object Name +} + +function Reset-ProcessEnvironmentVariables +{ + param($processVars) + + # query Machine vars from registry, ensuring expansion EXCEPT for PATH + $vars = [Environment]::GetEnvironmentVariables('Machine').GetEnumerator() | + % -Begin { $h = @{} } -Process { $v = if ($_.Name -eq 'Path') { $_.Value } else { [Environment]::GetEnvironmentVariable($_.Name, 'Machine') }; $h."$($_.Name)" = $v } -End { $h } + + # query User vars from registry, ensuring expansion EXCEPT for PATH + [Environment]::GetEnvironmentVariables('User').GetEnumerator() | % { + if ($_.Name -eq 'Path') { $vars[$_.Name] += ';' + $_.Value } + else + { + $value = [Environment]::GetEnvironmentVariable($_.Name, 'User') + $vars[$_.Name] = $value + } + } + + $processVars.GetEnumerator() | % { $vars[$_.Name] = $_.Value } + + Remove-Item -Path Env:\* -ErrorAction SilentlyContinue -WarningAction SilentlyContinue -Recurse + + $vars.GetEnumerator() | % { Set-Item -Path "Env:\$($_.Name)" -Value $_.Value } +} + +function Reset-ProcessPowerShellVariables +{ + param($psVariables) + $psVariables | %{ + $tempVar = $_ + if(-not(Get-Variable -Name $_.Name -ErrorAction SilentlyContinue)){ + New-Variable -Name $_.Name -Value $_.Value -Description $_.Description -Option $_.Options -Visibility $_.Visibility + } + } +} +'@ + +function Invoke-PowerShellUserCode +{ + [CmdletBinding()] + param( + [String] + $Code, + + [Int] + $TimeoutMilliseconds, + + [String] + $WorkingDirectory + ) + + if ($global:runspace -eq $null){ + # CreateDefault2 requires PS3 + if ([System.Management.Automation.Runspaces.InitialSessionState].GetMethod('CreateDefault2')){ + $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2() + }else{ + $sessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault() + } + + $global:puppetPSHost = New-Object Puppet.PuppetPSHost + $global:runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace($global:puppetPSHost, $sessionState) + $global:runspace.Open() + } + + try + { + $ps = $null + $global:puppetPSHost.ResetExitStatus() + $global:puppetPSHost.ResetConsoleStreams() + + if ($PSVersionTable.PSVersion -ge [Version]'3.0') { + $global:runspace.ResetRunspaceState() + } + + $ps = [System.Management.Automation.PowerShell]::Create() + $ps.Runspace = $global:runspace + [Void]$ps.AddScript($global:ourFunctions) + $ps.Invoke() + + if ([string]::IsNullOrEmpty($WorkingDirectory)) { + [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($global:DefaultWorkingDirectory) + } else { + if (-not (Test-Path -Path $WorkingDirectory)) { Throw "Working directory `"$WorkingDirectory`" does not exist" } + [Void]$ps.Runspace.SessionStateProxy.Path.SetLocation($WorkingDirectory) + } + + if(!$global:environmentVariables){ + $ps.Commands.Clear() + $global:environmentVariables = $ps.AddCommand('Get-ProcessEnvironmentVariables').Invoke() + } + + if($PSVersionTable.PSVersion -le [Version]'2.0'){ + if(!$global:psVariables){ + $global:psVariables = $ps.AddScript('Get-Variable').Invoke() + } + + $ps.Commands.Clear() + [void]$ps.AddScript('Get-Variable -Scope Global | Remove-Variable -Force -ErrorAction SilentlyContinue -WarningAction SilentlyContinue') + $ps.Invoke() + + $ps.Commands.Clear() + [void]$ps.AddCommand('Reset-ProcessPowerShellVariables').AddParameter('psVariables', $global:psVariables) + $ps.Invoke() + } + + $ps.Commands.Clear() + [Void]$ps.AddCommand('Reset-ProcessEnvironmentVariables').AddParameter('processVars', $global:environmentVariables) + $ps.Invoke() + + # we clear the commands before each new command + # to avoid command pollution + $ps.Commands.Clear() + [Void]$ps.AddScript($Code) + + # out-default and MergeMyResults takes all output streams + # and writes it to the PSHost we create + # this needs to be the last thing executed + [void]$ps.AddCommand("out-default"); + + # if the call operator & established an exit code, exit with it + [Void]$ps.AddScript('if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }') + + if($PSVersionTable.PSVersion -le [Version]'2.0'){ + $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::Error, [System.Management.Automation.Runspaces.PipelineResultTypes]::Output); + }else{ + $ps.Commands.Commands[0].MergeMyResults([System.Management.Automation.Runspaces.PipelineResultTypes]::All, [System.Management.Automation.Runspaces.PipelineResultTypes]::Output); + } + $asyncResult = $ps.BeginInvoke() + + if (!$asyncResult.AsyncWaitHandle.WaitOne($TimeoutMilliseconds)){ + throw "Catastrophic failure: PowerShell module timeout ($TimeoutMilliseconds ms) exceeded while executing" + } + + try + { + $ps.EndInvoke($asyncResult) + } catch [System.Management.Automation.IncompleteParseException] { + # https://msdn.microsoft.com/en-us/library/system.management.automation.incompleteparseexception%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 + throw $_.Exception.Message + } catch { + if ($_.Exception.InnerException -ne $null) + { + throw $_.Exception.InnerException + } else { + throw $_.Exception + } + } + + [Puppet.PuppetPSHostUserInterface]$ui = $global:puppetPSHost.UI + return @{ + exitcode = $global:puppetPSHost.Exitcode; + stdout = $ui.Output; + stderr = $ui.StdErr; + errormessage = $null; + } + } + catch + { + try + { + if ($global:runspace) { $global:runspace.Dispose() } + } + finally + { + $global:runspace = $null + } + if(($global:puppetPSHost -ne $null) -and $global:puppetPSHost.ExitCode){ + $ec = $global:puppetPSHost.ExitCode + }else{ + # This is technically not true at this point as we do not + # know what exitcode we should return as an unexpected exception + # happened and the user did not set an exitcode. Our best guess + # is to return 1 so that we ensure Puppet reports this run as an error. + $ec = 1 + } + + if ($_.Exception.ErrorRecord.InvocationInfo -ne $null) + { + $output = $_.Exception.Message + "`n`r" + $_.Exception.ErrorRecord.InvocationInfo.PositionMessage + } else { + $output = $_.Exception.Message | Out-String + } + + # make an attempt to read StdErr as it may contain info about failures + try { $err = $global:puppetPSHost.UI.StdErr } catch { $err = $null } + return @{ + exitcode = $ec; + stdout = $null; + stderr = $err; + errormessage = $output; + } + } + finally + { + if ($ps -ne $null) { [Void]$ps.Dispose() } + } +} + +function Write-SystemDebugMessage +{ + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [String] + $Message + ) + + if ($script:EmitDebugOutput -or ($DebugPreference -ne 'SilentlyContinue')) + { + [System.Diagnostics.Debug]::WriteLine($Message) + } +} + +function Signal-Event +{ + [CmdletBinding()] + param( + [String] + $EventName + ) + + $event = [System.Threading.EventWaitHandle]::OpenExisting($EventName) + + [Void]$event.Set() + [Void]$event.Close() + if ($PSVersionTable.CLRVersion.Major -ge 3) { + [Void]$event.Dispose() + } + + Write-SystemDebugMessage -Message "Signaled event $EventName" +} + +function ConvertTo-LittleEndianBytes +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Int32] + $Value + ) + + $bytes = [BitConverter]::GetBytes($Value) + if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($bytes) } + + return $bytes +} + +function ConvertTo-ByteArray +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [Hashtable] + $Hash, + + [Parameter(Mandatory = $true)] + [System.Text.Encoding] + $Encoding + ) + + # Initialize empty byte array that can be appended to + $result = [Byte[]]@() + # and add length / name / length / value from Hashtable + $Hash.GetEnumerator() | + % { + $name = $Encoding.GetBytes($_.Name) + $result += (ConvertTo-LittleEndianBytes $name.Length) + $name + + $value = @() + if ($_.Value -ne $null) { $value = $Encoding.GetBytes($_.Value.ToString()) } + $result += (ConvertTo-LittleEndianBytes $value.Length) + $value + } + + return $result +} + +function Write-StreamResponse +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.IO.Pipes.PipeStream] + $Stream, + + [Parameter(Mandatory = $true)] + [Byte[]] + $Bytes + ) + + $length = ConvertTo-LittleEndianBytes -Value $Bytes.Length + $Stream.Write($length, 0, 4) + $Stream.Flush() + + Write-SystemDebugMessage -Message "Wrote Int32 $($bytes.Length) as Byte[] $length to Stream" + + $Stream.Write($bytes, 0, $bytes.Length) + $Stream.Flush() + + Write-SystemDebugMessage -Message "Wrote $($bytes.Length) bytes of data to Stream" +} + +function Read-Int32FromStream +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.IO.Pipes.PipeStream] + $Stream + ) + + $length = New-Object Byte[] 4 + # Read blocks until all 4 bytes available + $Stream.Read($length, 0, 4) | Out-Null + # value is sent in Little Endian, but if the CPU is not, in-place reverse the array + if (![BitConverter]::IsLittleEndian) { [Array]::Reverse($length) } + $value = [BitConverter]::ToInt32($length, 0) + + Write-SystemDebugMessage -Message "Read Byte[] $length from stream as Int32 $value" + + return $value +} + +# Message format is: +# 1 byte - command identifier +# 0 - Exit +# 1 - Execute +# -1 - Exit - automatically returned when ReadByte encounters a closed pipe +# [optional] 4 bytes - Little Endian encoded 32-bit code block length for execute +# Intel CPUs are little endian, hence the .NET Framework typically is +# [optional] variable length - code block +function ConvertTo-PipeCommand +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [System.IO.Pipes.PipeStream] + $Stream, + + [Parameter(Mandatory = $true)] + [System.Text.Encoding] + $Encoding, + + [Parameter(Mandatory = $false)] + [Int32] + $BufferChunkSize = 4096 + ) + + # command identifier is a single value - ReadByte blocks until byte is ready / pipe closes + $command = $Stream.ReadByte() + + Write-SystemDebugMessage -Message "Command id $command read from pipe" + + switch ($command) + { + # Exit + # ReadByte returns a -1 when the pipe is closed on the other end + { @(0, -1) -contains $_ } { return @{ Command = 'Exit' }} + + # Execute + 1 { $parsed = @{ Command = 'Execute' } } + + default { throw "Catastrophic failure: Unexpected Command $command received" } + } + + # read size of incoming byte buffer + $parsed.Length = Read-Int32FromStream -Stream $Stream + Write-SystemDebugMessage -Message "Expecting $($parsed.Length) raw bytes of $($Encoding.EncodingName) characters" + + # Read blocks until all bytes are read or EOF / broken pipe hit - tested with 5MB and worked fine + $parsed.RawData = New-Object Byte[] $parsed.Length + $read = $Stream.Read($parsed.RawData, 0, $parsed.Length) + if ($read -lt $parsed.Length) + { + throw "Catastrophic failure: Expected $($parsed.Length) raw bytes, only received $read" + } + + # turn the raw bytes into the expected encoded string! + $parsed.Code = $Encoding.GetString($parsed.RawData) + + return $parsed +} + +function Start-PipeServer +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [String] + $CommandChannelPipeName, + + [Parameter(Mandatory = $true)] + [System.Text.Encoding] + $Encoding + ) + + # this does not require versioning in the payload as client / server are tightly coupled + $server = New-Object System.IO.Pipes.NamedPipeServerStream($CommandChannelPipeName, + [System.IO.Pipes.PipeDirection]::InOut) + + try + { + # block until Ruby process connects + $server.WaitForConnection() + + Write-SystemDebugMessage -Message "Incoming Connection to $CommandChannelPipeName Received - Expecting Strings as $($Encoding.EncodingName)" + + # Infinite Loop to process commands until EXIT received + $running = $true + while ($running) + { + # throws if an unxpected command id is read from pipe + $response = ConvertTo-PipeCommand -Stream $server -Encoding $Encoding + + Write-SystemDebugMessage -Message "Received $($response.Command) command from client" + + switch ($response.Command) + { + 'Execute' { + Write-SystemDebugMessage -Message "[Execute] Invoking user code:`n`n $($response.Code)" + + # assuming that the Ruby code always calls Invoked-PowerShellUserCode, + # result should already be returned as a hash + $result = Invoke-Expression $response.Code + + $bytes = ConvertTo-ByteArray -Hash $result -Encoding $Encoding + + Write-StreamResponse -Stream $server -Bytes $bytes + } + 'Exit' { $running = $false } + } + } + } + catch [Exception] + { + Write-SystemDebugMessage -Message "PowerShell Pipe Server Failed!`n`n$_" + throw + } + finally + { + if ($server -ne $null) { $server.Dispose() } + } +} + +Start-PipeServer -CommandChannelPipeName $NamedPipeName -Encoding $Encoding diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/metadata.json b/modules/utilities/windows/shells/puppetlabs_powershell_local/metadata.json new file mode 100644 index 000000000..80b14ec03 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/metadata.json @@ -0,0 +1,38 @@ +{ + "name": "puppetlabs-powershell", + "version": "2.1.0", + "author": "Puppet Inc", + "summary": "Adds a new exec provider for executing PowerShell commands.", + "license": "Apache-2.0", + "source": "https://github.com/puppetlabs/puppetlabs-powershell", + "project_page": "https://github.com/puppetlabs/puppetlabs-powershell", + "issues_url": "https://tickets.puppet.com/browse/MODULES/component/12015", + "dependencies": [ + + ], + "data_provider": null, + "operatingsystem_support": [ + { + "operatingsystem": "Windows", + "operatingsystemrelease": [ + "Server 2008", + "Server 2008 R2", + "Server 2012", + "Server 2012 R2", + "7", + "8", + "10" + ] + } + ], + "requirements": [ + { + "name": "pe", + "version_requirement": ">= 3.0.0 < 2016.4.0" + }, + { + "name": "puppet", + "version_requirement": ">= 3.0.0 < 5.0.0" + } + ] +} diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml b/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml index cf3d8d860..f329b9c06 100644 --- a/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/secgen_metadata.xml @@ -5,6 +5,7 @@ xsi:schemaLocation="http://www.github/cliffe/SecGen/utility"> Powershell install local Jason Keighley + Puppetlabs Apache v2 A local version of the powershell shell provisioner diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/exec_powershell_spec.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/exec_powershell_spec.rb new file mode 100644 index 000000000..5df116ebe --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/exec_powershell_spec.rb @@ -0,0 +1,425 @@ +require 'spec_helper_acceptance' + +describe 'powershell provider:' do #, :unless => UNSUPPORTED_PLATFORMS.include?(fact('osfamily')) do + windows_agents = agents.select { |a| a.platform =~ /windows/ } + + shared_examples 'should fail' do |manifest, error_check| + it 'should throw an error' do + expect { apply_manifest_on(windows_agents, manifest, :catch_failures => true, :future_parser => FUTURE_PARSER) }.to raise_error(error_check) + end + end + + shared_examples 'apply success' do |manifest| + it 'should succeed' do + apply_manifest_on(windows_agents, manifest, :catch_failures => true, :future_parser => FUTURE_PARSER) + end + end + + describe 'should run successfully' do + + p1 = <<-MANIFEST + exec{'TestPowershell': + command => 'Get-Process > c:/process.txt', + unless => 'if(!(test-path "c:/process.txt")){exit 1}', + provider => powershell, + } + MANIFEST + + it 'should not error on first run' do + # Run it twice and test for idempotency + apply_manifest_on(windows_agents, p1, :catch_failures => true, :future_parser => FUTURE_PARSER) + end + + it 'should be idempotent' do + apply_manifest_on(windows_agents, p1, :catch_failures => true, :future_parser => FUTURE_PARSER, :acceptable_exit_codes => [0]) + end + + end + + describe 'should handle a try/catch successfully' do + + it 'should demonstrably execute PowerShell code inside a try block' do + tryoutfile = 'C:\try_success.txt' + try_content = 'try_executed' + catchoutfile = 'c:\catch_shouldntexist.txt' + + powershell_cmd = <<-CMD + try { + $foo = @(1, 2, 3).count + "#{try_content}" | Out-File -FilePath "#{tryoutfile}" + } catch { + "catch_executed" | Out-File -FilePath "#{catchoutfile}" + } + CMD + + p1 = <<-MANIFEST + exec{'TestPowershell': + command => '#{powershell_cmd}', + provider => powershell, + } + MANIFEST + + apply_manifest_on(windows_agents, p1, :catch_failures => true, :future_parser => FUTURE_PARSER) + + windows_agents.each do |agent| + on(agent, "cmd.exe /c \"type #{tryoutfile}\"") do |result| + assert_match(/#{try_content}/, result.stdout, "Unexpected result for host '#{agent}'") + end + + on(agent, "cmd.exe /c \"type #{catchoutfile}\"", :acceptable_exit_codes => [1]) do |result| + assert_match(/^The system cannot find the file specified\./, result.stderr, "Unexpected file content #{result.stdout} on host '#{agent}'") + end + end + end + + it 'should demonstrably execute PowerShell code inside a catch block' do + + tryoutfile = 'C:\try_shouldntexist.txt' + catchoutfile = 'c:\catch_success.txt' + catch_content = 'catch_executed' + + powershell_cmd = <<-CMD + try { + throw "execute catch!" + "try_executed" | Out-File -FilePath "#{tryoutfile}" + } catch { + "#{catch_content}" | Out-File -FilePath "#{catchoutfile}" + } + CMD + + p1 = <<-MANIFEST + exec{'TestPowershell': + command => '#{powershell_cmd}', + provider => powershell, + } + MANIFEST + + apply_manifest_on(windows_agents, p1, :catch_failures => true, :future_parser => FUTURE_PARSER) + + windows_agents.each do |agent| + on(agent, "cmd.exe /c \"type #{tryoutfile}\"", :acceptable_exit_codes => [1]) do |result| + assert_match(/^The system cannot find the file specified\./, result.stderr, "Unexpected file content #{result.stdout} on host '#{agent}'") + end + + on(agent, "cmd.exe /c \"type #{catchoutfile}\"") do |result| + assert_match(/#{catch_content}/, result.stdout, "Unexpected result for host '#{agent}'") + end + end + end + + end + + describe 'should run commands that exit session' do + + exit_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'exit 0', + provider => powershell, + } + MANIFEST + + it 'should not error on first run' do + apply_manifest_on(windows_agents, exit_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + it 'should run a second time' do + apply_manifest_on(windows_agents, exit_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + end + + describe 'should run commands that break session' do + + break_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'Break', + provider => powershell, + } + MANIFEST + + it 'should not error on first run' do + apply_manifest_on(windows_agents, break_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + it 'should run a second time' do + apply_manifest_on(windows_agents, break_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + end + + describe 'should run commands that return from session' do + + return_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'return 0', + provider => powershell, + } + MANIFEST + + it 'should not error on first run' do + apply_manifest_on(windows_agents, return_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + it 'should run a second time' do + apply_manifest_on(windows_agents, return_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + end + + describe 'should not leak variables across calls to single session' do + + var_leak_setup_pp = <<-MANIFEST + exec{'TestPowershell': + command => '$special=1', + provider => powershell, + } + MANIFEST + + var_leak_test_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'if ( $special -eq 1 ) { exit 1 } else { exit 0 }', + provider => powershell, + } + MANIFEST + + it 'should not see variable from previous run' do + # Setup the variable + apply_manifest_on(windows_agents, var_leak_setup_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + + # Test to see if subsequent call sees the variable + apply_manifest_on(windows_agents, var_leak_test_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + end + + describe 'should not leak environment variables across calls to single session' do + + envar_leak_setup_pp = <<-MANIFEST + exec{'TestPowershell': + command => "\\$env:superspecial='1'", + provider => powershell, + } + MANIFEST + + envar_leak_test_pp = <<-MANIFEST + exec{'TestPowershell': + command => "if ( \\$env:superspecial -eq '1' ) { exit 1 } else { exit 0 }", + provider => powershell, + } + MANIFEST + + envar_ext_test_pp = <<-MANIFEST + exec{'TestPowershell': + command => "if ( \\$env:outside -eq '1' ) { exit 0 } else { exit 1 }", + provider => powershell, + } + MANIFEST + + after(:each) do + on(default, powershell("'Remove-Item Env:\\superspecial -ErrorAction Ignore;exit 0'")) + on(default, powershell("'Remove-Item Env:\\outside -ErrorAction Ignore;exit 0'")) + end + + it 'should not see environment variable from previous run' do + # Setup the environment variable + apply_manifest_on(windows_agents, envar_leak_setup_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + + # Test to see if subsequent call sees the environment variable + apply_manifest_on(windows_agents, envar_leak_test_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + it 'should see environment variables set outside of session' do + # Setup the environment variable outside of Puppet + on(default, powershell("\\$env:outside='1'")) + + # Test to see if initial run sees the environment variable + apply_manifest_on(windows_agents, envar_leak_test_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + + # Test to see if subsequent call sees the environment variable and environment purge + apply_manifest_on(windows_agents, envar_leak_test_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + end + + describe 'should allow exit from unless' do + + unless_not_triggered_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'exit 0', + unless => 'exit 1', + provider => powershell, + } + MANIFEST + + unless_triggered_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'exit 0', + unless => 'exit 0', + provider => powershell, + } + MANIFEST + + it 'should RUN command if unless is NOT triggered' do + apply_manifest_on(windows_agents, unless_not_triggered_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + it 'should NOT run command if unless IS triggered' do + apply_manifest_on(windows_agents, unless_triggered_pp, :catch_changes => true, :future_parser => FUTURE_PARSER) + end + + end + + describe 'should allow exit from onlyif' do + + onlyif_not_triggered_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'exit 0', + onlyif => 'exit 1', + provider => powershell, + } + MANIFEST + + onlyif_triggered_pp = <<-MANIFEST + exec{'TestPowershell': + command => 'exit 0', + onlyif => 'exit 0', + provider => powershell, + } + MANIFEST + + it 'should NOT run command if onlyif is NOT triggered' do + apply_manifest_on(windows_agents, onlyif_not_triggered_pp, :catch_changes => true, :future_parser => FUTURE_PARSER) + end + + it 'should RUN command if onlyif IS triggered' do + apply_manifest_on(windows_agents, onlyif_triggered_pp, :expect_changes => true, :future_parser => FUTURE_PARSER) + end + + end + + describe 'should be able to access the files after execution' do + + p2 = <<-MANIFEST + exec{"TestPowershell": + command => 'Get-Service *puppet* | Out-File -FilePath C:/services.txt -Encoding UTF8', + provider => powershell + } + MANIFEST + + describe file('c:/services.txt') do + apply_manifest_on(windows_agents, p2, :catch_failures => true, :future_parser => FUTURE_PARSER) + it { should be_file } + its(:content) { should match /puppet/ } + end + end + + describe 'should catch and rethrow exceptions up to puppet' do + pexception = <<-MANIFEST + exec{'PowershellException': + provider => powershell, + command => 'throw "We are writing an error"', + } + MANIFEST + it_should_behave_like 'should fail', pexception, /We are writing an error/i + end + + describe 'should error if timeout is exceeded' do + ptimeoutexception = <<-MANIFEST + exec{'PowershellException': + command => 'Write-Host "Going to sleep now..."; Start-Sleep 5', + timeout => 2, + provider => powershell, + } + MANIFEST + it_should_behave_like 'should fail', ptimeoutexception + end + + describe 'should be able to execute a ps1 file provided' do + p2 = <<-MANIFEST + file{'c:/services.ps1': + content => '#{File.open(File.join(File.dirname(__FILE__), 'files/services.ps1')).read()}' + } + exec{"TestPowershellPS1": + command => 'c:/services.ps1', + provider => powershell, + require => File['c:/services.ps1'] + } + MANIFEST + describe file('c:/temp/services.csv') do + apply_manifest_on(windows_agents, p2, :catch_failures => true, :future_parser => FUTURE_PARSER) + it { should be_file } + its(:content) { should match /puppet/ } + end + end + + describe 'passing parameters to the ps1 file' do + outfile = 'C:/temp/svchostprocess.txt' + processName = 'svchost' + pp = <<-MANIFEST + $process = '#{processName}' + $outFile = '#{outfile}' + file{'c:/param_script.ps1': + content => '#{File.open(File.join(File.dirname(__FILE__), 'files/param_script.ps1')).read()}' + } + exec{'run this with param': + provider => powershell, + command => "c:/param_script.ps1 -ProcessName '$process' -FileOut '$outFile'", + require => File['c:/param_script.ps1'], + } + MANIFEST + describe file(outfile) do + apply_manifest_on(windows_agents, pp, :catch_failures => true, :future_parser => FUTURE_PARSER) + it { should be_file } + its(:content) { should match /svchost/ } + end + end + + describe 'should execute using 64 bit powershell' do + p3 = <<-MANIFEST + $maxArchNumber = $::architecture? { + /(?i)(i386|i686|x86)$/ => 4, + /(?i)(x64|x86_64)/=> 8, + default => 0 + } + exec{'Test64bit': + command => "if([IntPtr]::Size -eq $maxArchNumber) { exit 0 } else { Write-Error 'Architecture mismatch' }", + provider => powershell + } + MANIFEST + it_should_behave_like 'apply success', p3 + end + + shared_examples 'standard exec' do |powershell_cmd| + padmin = <<-MANIFEST + exec{'no fail test': + command => '#{powershell_cmd}', + provider => powershell, + } + MANIFEST + it 'should not fail' do + apply_manifest_on(windows_agents, padmin, :catch_failures => true, :future_parser => FUTURE_PARSER) + end + end + + describe 'test admin rights' do + ps1 = <<-PS1 + $id = [Security.Principal.WindowsIdentity]::GetCurrent() + $pr = New-Object Security.Principal.WindowsPrincipal $id + if(!($pr.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))){Write-Error "Not in admin"} + PS1 + it_should_behave_like 'standard exec', ps1 + end + + describe 'test import-module' do + pimport = <<-PS1 + $mods = Get-Module -ListAvailable | Sort + if($mods.Length -lt 1) { + Write-Error "Expected to get at least one module, but none were listed" + } + Import-Module $mods[0].Name + if(-not (Get-Module $mods[0].Name)){ + Write-Error "Failed to import module ${mods[0].Name}" + } + PS1 + it_should_behave_like 'standard exec', pimport + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/param_script.ps1 b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/param_script.ps1 new file mode 100644 index 000000000..495f59b94 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/param_script.ps1 @@ -0,0 +1,7 @@ +Param( + [String] $ProcessName, + [String] $fileOut +) +$processes = Get-Process $ProcessName +New-Item $fileOut -ItemType File +$processes | Out-File $fileOut -Encoding UTF8 diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/services.ps1 b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/services.ps1 new file mode 100644 index 000000000..01ab8a67b --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/files/services.ps1 @@ -0,0 +1,5 @@ +$temp = "C:/temp" +if(!(Test-Path $temp)){ + mkdir $temp +} +Get-Service *puppet* | Export-Csv "$temp\services.csv" diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-i386.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-i386.yml new file mode 100644 index 000000000..eb571eea1 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-i386.yml @@ -0,0 +1,24 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + win2003_i386: + roles: + - agent + - default + platform: windows-2003-i386 + template: win-2003-i386 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-x86_64.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-x86_64.yml new file mode 100644 index 000000000..5c3e83522 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2003-x86_64.yml @@ -0,0 +1,24 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + win2003_x86_64: + roles: + - agent + - default + platform: windows-2003-x86_64 + template: win-2003-x86_64 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008-x86_64.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008-x86_64.yml new file mode 100644 index 000000000..e9d6d4dca --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008-x86_64.yml @@ -0,0 +1,24 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + win2008_x86_64: + roles: + - agent + - default + platform: windows-2008-x86_64 + template: win-2008-x86_64 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008r2-x86_64.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008r2-x86_64.yml new file mode 100644 index 000000000..ab9c70f78 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2008r2-x86_64.yml @@ -0,0 +1,24 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + win2008r2: + roles: + - agent + - default + platform: windows-2008r2-x86_64 + template: win-2008r2-x86_64 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012-x86_64.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012-x86_64.yml new file mode 100644 index 000000000..058665450 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012-x86_64.yml @@ -0,0 +1,24 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + win2012: + roles: + - agent + - default + platform: windows-2012-x86_64 + template: win-2012-x86_64 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012r2-x86_64.yml b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012r2-x86_64.yml new file mode 100644 index 000000000..ef06e40c5 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/acceptance/nodesets/windows-2012r2-x86_64.yml @@ -0,0 +1,24 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + win2012r2: + roles: + - agent + - default + platform: windows-2012r2-x86_64 + template: win-2012r2-x86_64 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/Enterprise/Dynamic + resourcepool: delivery/Quality Assurance/Enterprise/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/exit-27.ps1 b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/exit-27.ps1 new file mode 100644 index 000000000..c3605d3f5 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/exit-27.ps1 @@ -0,0 +1 @@ +exit 27 diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb new file mode 100644 index 000000000..472659eb6 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/integration/puppet_x/puppetlabs/powershell_manager_spec.rb @@ -0,0 +1,688 @@ +require 'spec_helper' +require 'puppet/type' +require 'puppet_x/puppetlabs/powershell/powershell_manager' + +module PuppetX + module PowerShell + class PowerShellManager; end + if Puppet::Util::Platform.windows? + module WindowsAPI + require 'ffi' + extend FFI::Library + + ffi_convention :stdcall + + # https://msdn.microsoft.com/en-us/library/ks2530z6%28v=VS.100%29.aspx + # intptr_t _get_osfhandle( + # int fd + # ); + ffi_lib [FFI::CURRENT_PROCESS, 'msvcrt'] + attach_function :get_osfhandle, :_get_osfhandle, [:int], :uintptr_t + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211(v=vs.85).aspx + # BOOL WINAPI CloseHandle( + # _In_ HANDLE hObject + # ); + ffi_lib :kernel32 + attach_function :CloseHandle, [:uintptr_t], :int32 + end + end + end +end + +describe PuppetX::PowerShell::PowerShellManager, + :if => Puppet::Util::Platform.windows? && PuppetX::PowerShell::PowerShellManager.supported? do + + let (:manager_args) { + provider = Puppet::Type.type(:exec).provider(:powershell) + powershell = provider.command(:powershell) + cli_args = provider.powershell_args + "#{powershell} #{cli_args.join(' ')}" + } + + def create_manager + PuppetX::PowerShell::PowerShellManager.instance(manager_args, true) + end + + let (:manager) { create_manager() } + + describe "when managing the powershell process" do + describe "the PowerShellManager::instance method" do + it "should return the same manager instance / process given the same cmd line" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + manager_2 = create_manager() + second_pid = manager_2.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + expect(manager_2).to eq(manager) + expect(first_pid).to eq(second_pid) + end + + def bad_file_descriptor_regex + # Ruby can do something like: + # + # + @bad_file_descriptor_regex ||= ( + ebadf = Errno::EBADF.new() + '^' + Regexp.escape("\#<#{ebadf.class}: #{ebadf.message}") + ) + end + + def pipe_error_regex + @pipe_error_regex ||= ( + epipe = Errno::EPIPE.new() + '^' + Regexp.escape("\#<#{epipe.class}: #{epipe.message}") + ) + end + # reason should be a string for an exact match + # else an array of regex matches + def expect_dead_manager(manager, reason, style = :exact) + # additional attempts to use the manager will fail for the given reason + result = manager.execute('Write-Host "hi"') + expect(result[:exitcode]).to eq(-1) + + if reason.is_a?(String) + expect(result[:stderr][0]).to eq(reason) if style == :exact + expect(result[:stderr][0]).to match(reason) if style == :regex + elsif reason.is_a?(Array) + expect(reason).to include(result[:stderr][0]) if style == :exact + if style == :regex + expect(result[:stderr][0]).to satisfy("should match expected error(s): #{reason}") do |msg| + reason.any? { |m| msg.match m } + end + end + end + + # and the manager no longer considers itself alive + expect(manager.alive?).to eq(false) + end + + def expect_different_manager_returned_than(manager, pid) + # acquire another manager instance + new_manager = create_manager() + + # which should be different than the one passed in + expect(new_manager).to_not eq(manager) + + # with a different PID + second_pid = new_manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + expect(pid).to_not eq(second_pid) + end + + def close_stream(stream, style = :inprocess) + if style == :inprocess + stream.close + else style == :viahandle + handle = PuppetX::PowerShell::WindowsAPI.get_osfhandle(stream.fileno) + PuppetX::PowerShell::WindowsAPI.CloseHandle(handle) + end + end + + it "should create a new PowerShell manager host if user code exits the first process" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + exitcode = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Kill()')[:exitcode] + + # when a process gets torn down out from under manager before reading stdout + # it catches the error and returns a -1 exitcode + expect(exitcode).to eq(-1) + + expect_dead_manager(manager, pipe_error_regex, :regex) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the underlying PowerShell process is killed" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # kill the PID from Ruby + process = manager.instance_variable_get(:@ps_process) + Process.kill('KILL', process.pid) + + expect_dead_manager(manager, pipe_error_regex, :regex) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the input stream is closed" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # closing pipe from the Ruby side tears down the process + close_stream(manager.instance_variable_get(:@pipe), :inprocess) + + expect_dead_manager(manager, IOError.new('closed stream').inspect, :exact) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the input stream handle is closed" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # call CloseHandle against pipe, therby tearing down the PowerShell process + close_stream(manager.instance_variable_get(:@pipe), :viahandle) + + expect_dead_manager(manager, bad_file_descriptor_regex, :regex) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the output stream is closed" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # closing stdout from the Ruby side allows process to run + close_stream(manager.instance_variable_get(:@stdout), :inprocess) + + # fails with vanilla EPIPE or closed stream IOError depening on timing / Ruby version + msgs = [ Errno::EPIPE.new().inspect, IOError.new('closed stream').inspect ] + expect_dead_manager(manager, msgs, :exact) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the output stream handle is closed" do + # currently skipped as it can trigger an internal Ruby thread clean-up race + # its unknown why this test fails, but not the identical test against @stderr + skip('This test can cause intermittent segfaults in Ruby with w32_reset_event invalid handle') + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # call CloseHandle against stdout, which leaves PowerShell process running + close_stream(manager.instance_variable_get(:@stdout), :viahandle) + + # fails with vanilla EPIPE or various EBADF depening on timing / Ruby version + msgs = [ + '^' + Regexp.escape(Errno::EPIPE.new().inspect), + bad_file_descriptor_regex + ] + expect_dead_manager(manager, msgs, :regex) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the error stream is closed" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # closing stderr from the Ruby side allows process to run + close_stream(manager.instance_variable_get(:@stderr), :inprocess) + + # fails with vanilla EPIPE or closed stream IOError depening on timing / Ruby version + msgs = [ Errno::EPIPE.new().inspect, IOError.new('closed stream').inspect ] + expect_dead_manager(manager, msgs, :exact) + + expect_different_manager_returned_than(manager, first_pid) + end + + it "should create a new PowerShell manager host if the error stream handle is closed" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + # call CloseHandle against stderr, which leaves PowerShell process running + close_stream(manager.instance_variable_get(:@stderr), :viahandle) + + # fails with vanilla EPIPE or various EBADF depening on timing / Ruby version + msgs = [ + '^' + Regexp.escape(Errno::EPIPE.new().inspect), + bad_file_descriptor_regex + ] + expect_dead_manager(manager, msgs, :regex) + + expect_different_manager_returned_than(manager, first_pid) + end + end + end + + let(:powershell_runtime_error) { '$ErrorActionPreference = "Stop";$test = 1/0' } + let(:powershell_parseexception_error) { '$ErrorActionPreference = "Stop";if (1 -badoperator 2) { Exit 1 }' } + let(:powershell_incompleteparseexception_error) { '$ErrorActionPreference = "Stop";if (1 -eq 2) { ' } + + describe "when provided powershell commands" do + it "shows ps version" do + result = manager.execute('$psversiontable') + puts result[:stdout] + end + + it "should return simple output" do + result = manager.execute('write-output foo') + + expect(result[:stdout]).to eq("foo\r\n") + expect(result[:exitcode]).to eq(0) + end + + it "should return the exitcode specified" do + result = manager.execute('write-output foo; exit 55') + + expect(result[:stdout]).to eq("foo\r\n") + expect(result[:exitcode]).to eq(55) + end + + it "should return the exitcode 1 when exception is thrown" do + result = manager.execute('throw "foo"') + + expect(result[:stdout]).to eq(nil) + expect(result[:exitcode]).to eq(1) + end + + it "should return the exitcode of the last command to set an exit code" do + result = manager.execute("$LASTEXITCODE = 0; write-output 'foo'; cmd.exe /c 'exit 99'; write-output 'bar'") + + expect(result[:stdout]).to eq("foo\r\nbar\r\n") + expect(result[:exitcode]).to eq(99) + end + + it "should return the exitcode of a script invoked with the call operator &" do + fixture_path = File.expand_path(File.dirname(__FILE__) + '../../../../exit-27.ps1') + result = manager.execute("& #{fixture_path}") + + expect(result[:stdout]).to eq(nil) + expect(result[:exitcode]).to eq(27) + end + + it "should collect anything written to stderr" do + result = manager.execute('[System.Console]::Error.WriteLine("foo")') + + expect(result[:stderr]).to eq(["foo\r\n"]) + expect(result[:exitcode]).to eq(0) + end + + it "should collect multiline output written to stderr" do + # induce a failure in cmd.exe that emits a multi-iline error message + result = manager.execute('cmd.exe /c foo.exe') + + expect(result[:stdout]).to eq(nil) + expect(result[:stderr]).to eq(["'foo.exe' is not recognized as an internal or external command,\r\noperable program or batch file.\r\n"]) + expect(result[:exitcode]).to eq(1) + end + + it "should handle writting to stdout and stderr" do + result = manager.execute('ps;[System.Console]::Error.WriteLine("foo")') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:stderr]).to eq(["foo\r\n"]) + expect(result[:exitcode]).to eq(0) + end + + it "should handle writing to stdout natively" do + result = manager.execute('[System.Console]::Out.WriteLine("foo")') + + expect(result[:stdout]).to eq("foo\r\n") + expect(result[:native_stdout]).to eq(nil) + expect(result[:stderr]).to eq([]) + expect(result[:exitcode]).to eq(0) + end + + it "should properly interleave output written natively to stdout and via Write-XXX cmdlets" do + result = manager.execute('Write-Output "bar"; [System.Console]::Out.WriteLine("foo"); Write-Warning "baz";') + + expect(result[:stdout]).to eq("bar\r\nfoo\r\nWARNING: baz\r\n") + expect(result[:stderr]).to eq([]) + expect(result[:exitcode]).to eq(0) + end + + it "should handle writing to regularly captured output AND stdout natively" do + result = manager.execute('ps;[System.Console]::Out.WriteLine("foo")') + + expect(result[:stdout]).not_to eq("foo\r\n") + expect(result[:native_stdout]).to eq(nil) + expect(result[:stderr]).to eq([]) + expect(result[:exitcode]).to eq(0) + end + + it "should handle writing to regularly captured output, stderr AND stdout natively" do + result = manager.execute('ps;[System.Console]::Out.WriteLine("foo");[System.Console]::Error.WriteLine("bar")') + + expect(result[:stdout]).not_to eq("foo\r\n") + expect(result[:native_stdout]).to eq(nil) + expect(result[:stderr]).to eq(["bar\r\n"]) + expect(result[:exitcode]).to eq(0) + end + + context "it should handle UTF-8" do + # different UTF-8 widths + # 1-byte A + # 2-byte Û¿ - http://www.fileformat.info/info/unicode/char/06ff/index.htm - 0xDB 0xBF / 219 191 + # 3-byte áš  - http://www.fileformat.info/info/unicode/char/16A0/index.htm - 0xE1 0x9A 0xA0 / 225 154 160 + # 4-byte 𠜎 - http://www.fileformat.info/info/unicode/char/2070E/index.htm - 0xF0 0xA0 0x9C 0x8E / 240 160 156 142 + let (:mixed_utf8) { "A\u06FF\u16A0\u{2070E}" } # Aۿᚠ𠜎 + + it "when writing basic text" do + code = "Write-Output '#{mixed_utf8}'" + result = manager.execute(code) + + expect(result[:stdout]).to eq("#{mixed_utf8}\r\n") + expect(result[:exitcode]).to eq(0) + end + + it "when writing basic text to stderr" do + code = "[System.Console]::Error.WriteLine('#{mixed_utf8}')" + result = manager.execute(code) + + expect(result[:stderr]).to eq(["#{mixed_utf8}\r\n"]) + expect(result[:exitcode]).to eq(0) + end + end + + it "should execute cmdlets" do + result = manager.execute('ls') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should execute cmdlets with pipes" do + result = manager.execute('Get-Process | ? { $_.PID -ne $PID }') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should execute multi-line" do + result = manager.execute(<<-CODE +$foo = ls +$count = $foo.count +$count + CODE + ) + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should execute code with a try/catch, receiving the output of Write-Error" do + result = manager.execute(<<-CODE +try{ + $foo = ls + $count = $foo.count + $count +}catch{ + Write-Error "foo" +} + CODE + ) + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should be able to execute the code in a try block when using try/catch" do + result = manager.execute(<<-CODE + try { + $foo = @(1, 2, 3).count + exit 400 + } catch { + exit 1 + } + CODE + ) + + expect(result[:stdout]).to eq(nil) + # using an explicit exit code ensures we've really executed correct block + expect(result[:exitcode]).to eq(400) + end + + it "should be able to execute the code in a catch block when using try/catch" do + result = manager.execute(<<-CODE +try { + throw "Error!" + exit 0 +} catch { + exit 500 +} + CODE + ) + + expect(result[:stdout]).to eq(nil) + # using an explicit exit code ensures we've really executed correct block + expect(result[:exitcode]).to eq(500) + end + + + it "should reuse the same PowerShell process for multiple calls" do + first_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + second_pid = manager.execute('[Diagnostics.Process]::GetCurrentProcess().Id')[:stdout] + + expect(first_pid).to eq(second_pid) + end + + it "should remove psvariables between runs" do + manager.execute('$foo = "bar"') + result = manager.execute('$foo') + + expect(result[:stdout]).to eq(nil) + end + + it "should remove env variables between runs" do + manager.execute('[Environment]::SetEnvironmentVariable("foo", "bar", "process")') + result = manager.execute('Test-Path env:\foo') + + expect(result[:stdout]).to eq("False\r\n") + end + + def current_powershell_major_version + provider = Puppet::Type.type(:exec).provider(:powershell) + powershell = provider.command(:powershell) + + begin + version = `#{powershell} -NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -Command \"$PSVersionTable.PSVersion.Major.ToString()\"`.chomp!.to_i + rescue + puts "Unable to determine PowerShell version" + version = -1 + end + + version + end + + def output_cmdlet + # Write-Output is the default behavior, except on older PS2 where the + # behavior of Write-Output introduces newlines after every width number + # of characters as specified in the BufferSize of the custom console UI + # Write-Host should usually be avoided, but works for this test in old PS2 + current_powershell_major_version >= 3 ? + 'Write-Output' : + 'Write-Host' + end + + it "should be able to write more than the 64k default buffer size to the managers pipe without deadlocking the Ruby parent process or breaking the pipe" do + # this was tested successfully up to 5MB of text + buffer_string_96k = 'a' * ((1024 * 96) + 1) + result = manager.execute(<<-CODE +'#{buffer_string_96k}' | #{output_cmdlet} + CODE + ) + + expect(result[:errormessage]).to eq(nil) + expect(result[:exitcode]).to eq(0) + terminator = output_cmdlet == 'Write-Output' ? "\r\n" : "\n" + expect(result[:stdout]).to eq("#{buffer_string_96k}#{terminator}") + end + + it "should be able to write more than the 64k default buffer size to child process stdout without deadlocking the Ruby parent process" do + result = manager.execute(<<-CODE +$bytes_in_k = (1024 * 64) + 1 +[Text.Encoding]::UTF8.GetString((New-Object Byte[] ($bytes_in_k))) | #{output_cmdlet} + CODE + ) + + expect(result[:errormessage]).to eq(nil) + expect(result[:exitcode]).to eq(0) + terminator = output_cmdlet == 'Write-Output' ? "\r\n" : "\n" + expected = "\x0" * (1024 * 64 + 1) + terminator + expect(result[:stdout]).to eq(expected) + end + + it "should return a response with a timeout error if the execution timeout is exceeded" do + timeout_ms = 100 + result = manager.execute('sleep 1', timeout_ms) + # TODO What is the real message now? + msg = /Catastrophic failure\: PowerShell module timeout \(#{timeout_ms} ms\) exceeded while executing\r\n/ + expect(result[:errormessage]).to match(msg) + end + + it "should not deadlock and return a valid response given invalid unparseable PowerShell code" do + result = manager.execute(<<-CODE + { + + CODE + ) + + expect(result[:errormessage]).not_to be_empty + end + + it "should error if working directory does not exist" do + work_dir = 'C:/some/directory/that/does/not/exist' + + result = manager.execute('(Get-Location).Path',nil,work_dir) + + expect(result[:exitcode]).to_not eq(0) + expect(result[:errormessage]).to match(/Working directory .+ does not exist/) + end + + it "should allow forward slashes in working directory" do + work_dir = ENV["WINDIR"] + forward_work_dir = work_dir.gsub('\\','/') + + result = manager.execute('(Get-Location).Path',nil,work_dir)[:stdout] + + expect(result).to eq("#{work_dir}\r\n") + end + + it "should use a specific working directory if set" do + work_dir = ENV["WINDIR"] + + result = manager.execute('(Get-Location).Path',nil,work_dir)[:stdout] + + expect(result).to eq("#{work_dir}\r\n") + end + + it "should not reuse the same working directory between runs" do + work_dir = ENV["WINDIR"] + current_work_dir = Dir.getwd + + first_cwd = manager.execute('(Get-Location).Path',nil,work_dir)[:stdout] + second_cwd = manager.execute('(Get-Location).Path')[:stdout] + + # Paths should be case insensitive + expect(first_cwd.downcase).to eq("#{work_dir}\r\n".downcase) + expect(second_cwd.downcase).to eq("#{current_work_dir}\r\n".downcase) + end + + context "with runtime error" do + it "should not refer to 'EndInvoke' or 'throw' for a runtime error" do + result = manager.execute(powershell_runtime_error) + + expect(result[:exitcode]).to eq(1) + expect(result[:errormessage]).not_to match(/EndInvoke/) + expect(result[:errormessage]).not_to match(/throw/) + end + + it "should display line and char information for a runtime error" do + result = manager.execute(powershell_runtime_error) + + expect(result[:exitcode]).to eq(1) + expect(result[:errormessage]).to match(/At line\:\d+ char\:\d+/) + end + end + + context "with ParseException error" do + it "should not refer to 'EndInvoke' or 'throw' for a ParseException error" do + result = manager.execute(powershell_parseexception_error) + + expect(result[:exitcode]).to eq(1) + expect(result[:errormessage]).not_to match(/EndInvoke/) + expect(result[:errormessage]).not_to match(/throw/) + end + + it "should display line and char information for a ParseException error" do + result = manager.execute(powershell_parseexception_error) + + expect(result[:exitcode]).to eq(1) + expect(result[:errormessage]).to match(/At line\:\d+ char\:\d+/) + end + end + + context "with IncompleteParseException error" do + it "should not refer to 'EndInvoke' or 'throw' for an IncompleteParseException error" do + result = manager.execute(powershell_incompleteparseexception_error) + + expect(result[:exitcode]).to eq(1) + expect(result[:errormessage]).not_to match(/EndInvoke/) + expect(result[:errormessage]).not_to match(/throw/) + end + + it "should not display line and char information for an IncompleteParseException error" do + result = manager.execute(powershell_incompleteparseexception_error) + + expect(result[:exitcode]).to eq(1) + expect(result[:errormessage]).not_to match(/At line\:\d+ char\:\d+/) + end + end + end + + describe "when output is written to a PowerShell Stream" do + it "should collect anything written to verbose stream" do + msg = SecureRandom.uuid.to_s.gsub('-', '') + result = manager.execute("$VerbosePreference = 'Continue';Write-Verbose '#{msg}'") + + expect(result[:stdout]).to match(/^VERBOSE\: #{msg}/) + expect(result[:exitcode]).to eq(0) + end + + it "should collect anything written to debug stream" do + msg = SecureRandom.uuid.to_s.gsub('-', '') + result = manager.execute("$debugPreference = 'Continue';Write-debug '#{msg}'") + + expect(result[:stdout]).to match(/^DEBUG: #{msg}/) + expect(result[:exitcode]).to eq(0) + end + + it "should collect anything written to Warning stream" do + msg = SecureRandom.uuid.to_s.gsub('-', '') + result = manager.execute("Write-Warning '#{msg}'") + + expect(result[:stdout]).to match(/^WARNING: #{msg}/) + expect(result[:exitcode]).to eq(0) + end + + it "should collect anything written to Error stream" do + msg = SecureRandom.uuid.to_s.gsub('-', '') + result = manager.execute("Write-Error '#{msg}'") + + expect(result[:stdout]).to eq("Write-Error '#{msg}' : #{msg}\r\n + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException\r\n + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException\r\n \r\n") + expect(result[:exitcode]).to eq(0) + end + + it "should handle a Write-Error in the middle of code" do + result = manager.execute('ls;Write-Error "Hello";ps') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should handle a Out-Default in the user code" do + result = manager.execute('\'foo\' | Out-Default') + + expect(result[:stdout]).to eq("foo\r\n") + expect(result[:exitcode]).to eq(0) + end + + it "should handle lots of output from user code" do + result = manager.execute('1..1000 | %{ (65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_} }') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should handle a larger return of output from user code" do + result = manager.execute('1..1000 | %{ (65..90) + (97..122) | Get-Random -Count 5 | % {[char]$_} } | %{ $f="" } { $f+=$_ } {$f }') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + + it "should handle shell redirection" do + # the test here is to ensure that this doesn't break. because we merge the streams regardless + # the opposite of this test shows the same thing + result = manager.execute('function test-error{ ps;write-error \'foo\' }; test-error 2>&1') + + expect(result[:stdout]).not_to eq(nil) + expect(result[:exitcode]).to eq(0) + end + end + +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec.opts b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec.opts new file mode 100644 index 000000000..91cd6427e --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec.opts @@ -0,0 +1,6 @@ +--format +s +--colour +--loadby +mtime +--backtrace diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper.rb new file mode 100644 index 000000000..92082847c --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper.rb @@ -0,0 +1,54 @@ +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +require 'puppet' +require 'puppetlabs_spec_helper/module_spec_helper' +require 'pathname' +require 'rspec' + +require 'tmpdir' +require 'fileutils' + +if Puppet.features.microsoft_windows? + require 'puppet/util/windows/security' + + def take_ownership(path) + path = path.gsub('/', '\\') + output = %x(takeown.exe /F #{path} /R /A /D Y 2>&1) + if $? != 0 #check if the child process exited cleanly. + puts "#{path} got error #{output}" + end + end +end + +RSpec.configure do |config| + tmpdir = Dir.mktmpdir("rspecrun_powershell") + oldtmpdir = Dir.tmpdir() + ENV['TMPDIR'] = tmpdir + + if Puppet::Util::Platform.windows? + config.output_stream = $stdout + config.error_stream = $stderr + config.formatters.each { |f| f.instance_variable_set(:@output, $stdout) } + end + + config.expect_with :rspec do |c| + c.syntax = [:should, :expect] + end + + config.after :suite do + # return to original tmpdir + ENV['TMPDIR'] = oldtmpdir + if Puppet::Util::Platform.windows? + take_ownership(tmpdir) + end + FileUtils.rm_rf(tmpdir) + end +end + +# We need this because the RAL uses 'should' as a method. This +# allows us the same behavior but with a different method name. +class Object + alias :must :should + alias :must_not :should_not +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper_acceptance.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper_acceptance.rb new file mode 100644 index 000000000..be6dd592f --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/spec_helper_acceptance.rb @@ -0,0 +1,36 @@ +require 'beaker-rspec/spec_helper' +require 'beaker-rspec/helpers/serverspec' +require 'beaker/puppet_install_helper' + +UNSUPPORTED_PLATFORMS = ['debian', 'ubuntu', 'Solaris'] +FUTURE_PARSER = ENV['FUTURE_PARSER'] == 'true' || false + +run_puppet_install_helper + +unless ENV['MODULE_provision'] == 'no' + + on default, "mkdir -p #{default['distmoduledir']}/powershell" + result = on default, "echo #{default['distmoduledir']}/powershell" + target = result.raw_output.chomp + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + %w(lib metadata.json).each do |file| + scp_to default, "#{proj_root}/#{file}", target + end +end + +RSpec.configure do |c| + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '..')) + + # Readable test descriptions + c.formatter = :documentation + + # Configure all nodes in nodeset + c.before :suite do + shell("/bin/touch #{default['puppetpath']}/hiera.yaml") + end + c.after :suite do + absent_files = 'file{["c:/services.txt","c:/process.txt","c:/try_success.txt","c:/catch_shouldntexist.txt","c:/try_shouldntexist.txt","c:/catch_success.txt"]: ensure => absent }' + apply_manifest(absent_files) + end +end + diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/provider/exec/powershell_spec.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/provider/exec/powershell_spec.rb new file mode 100644 index 000000000..532367fc0 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/provider/exec/powershell_spec.rb @@ -0,0 +1,240 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/util' +require 'puppet_x/puppetlabs/powershell/powershell_manager' +require 'fileutils' + +describe Puppet::Type.type(:exec).provider(:powershell) do + + # Override the run value so we can test the super call + # There is no real good way to do this otherwise, previously we were + # testing Puppet internals that changed in 3.4.0 and made the specs + # no longer work the way they were originally specified. + Puppet::Type::Exec::ProviderPowershell.instance_eval do + alias_method :run_spec_override, :run + end + + let(:command) { '$(Get-WMIObject Win32_Account -Filter "SID=\'S-1-5-18\'") | Format-List' } + let(:args) { '-NoProfile -NonInteractive -NoLogo -ExecutionPolicy Bypass -Command -' } + let(:resource) { Puppet::Type.type(:exec).new(:command => command, :provider => :powershell) } + let(:provider) { described_class.new(resource) } + + let(:powershell) { + if File.exists?("#{ENV['SYSTEMROOT']}\\sysnative\\WindowsPowershell\\v1.0\\powershell.exe") + "#{ENV['SYSTEMROOT']}\\sysnative\\WindowsPowershell\\v1.0\\powershell.exe" + elsif File.exists?("#{ENV['SYSTEMROOT']}\\system32\\WindowsPowershell\\v1.0\\powershell.exe") + "#{ENV['SYSTEMROOT']}\\system32\\WindowsPowershell\\v1.0\\powershell.exe" + else + 'powershell.exe' + end + } + + describe "#run" do + context "stubbed calls" do + before :each do + PuppetX::PowerShell::PowerShellManager.stubs(:supported?).returns(false) + Puppet::Provider::Exec.any_instance.stubs(:run) + end + + it "should call exec run" do + Puppet::Type::Exec::ProviderPowershell.any_instance.expects(:run) + + provider.run_spec_override(command) + end + + it "should call cmd.exe /c" do + Puppet::Type::Exec::ProviderPowershell.any_instance.expects(:run) + .with(regexp_matches(/^cmd.exe \/c/), anything) + + provider.run_spec_override(command) + end + + it "should quote powershell.exe path", :if => Puppet.features.microsoft_windows? do + Puppet::Type::Exec::ProviderPowershell.any_instance.expects(:run). + with(regexp_matches(/"#{Regexp.escape(powershell)}"/), false) + + provider.run_spec_override(command) + end + + it "should quote the path to the temp file" do + path = 'C:\Users\albert\AppData\Local\Temp\puppet-powershell20130715-788-1n66f2j.ps1' + + provider.expects(:write_script).with(command).yields(path) + Puppet::Type::Exec::ProviderPowershell.any_instance.expects(:run). + with(regexp_matches(/^cmd.exe \/c ".* < "#{Regexp.escape(path)}""/), false) + + provider.run_spec_override(command) + end + + it "should supply default arguments to supress user interaction" do + Puppet::Type::Exec::ProviderPowershell.any_instance.expects(:run). + with(regexp_matches(/^cmd.exe \/c ".* #{args} < .*"/), false) + + provider.run_spec_override(command) + end + end + + context "actual runs", :if => Puppet.features.microsoft_windows? do + it "returns the output and status" do + output, status = provider.run(command) + + expect(output).to match(/SID\s+:\s+S-1-5-18/) + expect(status.exitstatus).to eq(0) + end + + it "returns true if the `onlyif` check command succeeds" do + resource[:onlyif] = command + + expect(resource.parameter(:onlyif).check(command)).to eq(true) + end + + it "returns false if the `unless` check command succeeds" do + resource[:unless] = command + + expect(resource.parameter(:unless).check(command)).to eq(false) + end + + it "runs commands properly that output to multiple streams" do + command = 'echo "foo"; [System.Console]::Error.WriteLine("bar"); cmd.exe /c foo.exe' + output, status = provider.run(command) + + if PuppetX::PowerShell::PowerShellManager.supported? + expected = "foo\r\n" + else + # when PowerShellManager is not used, the v1 style module collected + # all streams inside of a single output string + expected = [ + "foo\n", + "bar\n'", + "foo.exe' is not recognized as an internal or external command,\n", + "operable program or batch file.\n" + ].join('') + end + + expect(output).to eq(expected) + expect(status.exitstatus).to eq(1) + end + end + end + + describe "#checkexe" do + it "should skip checking the exe" do + expect(provider.checkexe(command)).to be_nil + end + end + + describe "#validatecmd" do + it "should always successfully validate the command to execute" do + expect(provider.validatecmd(command)).to eq(true) + end + end + + describe 'when specifying a working directory' do + describe 'that does not exist' do + let(:work_dir) { + if Puppet.features.microsoft_windows? + "#{ENV['SYSTEMROOT']}\\some\\directory\\that\\does\\not\\exist" + else + '/some/directory/that/does/not/exist' + end + } + let(:command) { 'exit 0' } + let(:resource) { Puppet::Type.type(:exec).new(:command => command, :provider => :powershell, :cwd => work_dir) } + let(:provider) { described_class.new(resource) } + + it 'emits an error when working directory does not exist' do + expect { provider.run(command) }.to raise_error(/Working directory .+ does not exist/) + end + end + end + + describe 'when applying a catalog' do + let(:manifests) { <<-MANIFEST + exec { 'PS': + command => 'exit 0', + provider => powershell, + } + MANIFEST + } + let(:tmpdir) { Dir.mktmpdir('statetmp').encode!(Encoding::UTF_8) } + + before :each do + # a statedir setting must now exist per the new transactionstore code + # introduced in Puppet 4.6 for corrective changes, as a new YAML file + # called transactionstore.yaml will be written under this path + # which defaults to c:\dev\null when not set on Windows + Puppet[:statedir] = tmpdir + end + + after :each do + FileUtils.rm_rf(tmpdir) + end + + def compile_to_catalog(string, node = Puppet::Node.new('foonode')) + Puppet[:code] = string + + # see lib/puppet/indirector/catalog/compiler.rb#filter + Puppet::Parser::Compiler.compile(node).filter { |r| r.virtual? } + end + + def compile_to_ral(manifest) + catalog = compile_to_catalog(manifest) + ral = catalog.to_ral + ral.finalize + ral + end + + def apply_compiled_manifest(manifest) + catalog = compile_to_ral(manifest) + + # ensure compilation works from Puppet 3.0.0 forward + args = [catalog, Puppet::Transaction::Report.new('apply')] + args << Puppet::Graph::SequentialPrioritizer.new if defined?(Puppet::Graph) + transaction = Puppet::Transaction.new(*args) + transaction.evaluate + transaction.report.finalize_report + + transaction + end + + it 'does not emit an irrelevant upgrade message when in a non-Windows environment', + :if => !Puppet.features.microsoft_windows? do + + expect(PuppetX::PowerShell::PowerShellManager.supported?).to eq(false) + + # run should never be called on an unsuitable provider + Puppet::Type::Exec::ProviderPowershell.any_instance.expects(:run).never + # and therefore neither should our upgrade message + Puppet::Type::Exec::ProviderPowershell.expects(:upgrade_message).never + + apply_compiled_manifest(manifest) + end + + it 'does not emit a warning message when PowerShellManager is usable in a Windows environment', + :if => Puppet.features.microsoft_windows? do + + PuppetX::PowerShell::PowerShellManager.stubs(:win32console_enabled?).returns(false) + + expect(PuppetX::PowerShell::PowerShellManager.supported?).to eq(true) + + # given PowerShellManager is supported, never emit an upgrade message + Puppet::Type::Exec::ProviderPowershell.expects(:upgrade_message).never + + apply_compiled_manifest(manifest) + end + + it 'emits a warning message when PowerShellManager cannot be used in a Windows environment', + :if => Puppet.features.microsoft_windows? do + + # pretend we're Ruby 1.9.3 / Puppet 3.x x86 + PuppetX::PowerShell::PowerShellManager.stubs(:win32console_enabled?).returns(true) + + expect(PuppetX::PowerShell::PowerShellManager.supported?).to eq(false) + + # given PowerShellManager is NOT supported, emit an upgrade message + Puppet::Type::Exec::ProviderPowershell.expects(:upgrade_message).once + + apply_compiled_manifest(manifest) + end + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb new file mode 100644 index 000000000..cac9d3d8c --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/compatible_powershell_version_spec.rb @@ -0,0 +1,58 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/type' +require 'puppet_x/puppetlabs/powershell/powershell_version' +require 'puppet_x/puppetlabs/powershell/compatible_powershell_version' + +describe PuppetX::PuppetLabs::PowerShell::CompatiblePowerShellVersion, :if => Puppet::Util::Platform.windows? do + before(:each) do + @compat = PuppetX::PuppetLabs::PowerShell::CompatiblePowerShellVersion + end + + describe "when a newer version of PowerShell is installed" do + it "should return true when PowerShell v3 is installed" do + PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('3.0') + + expect(@compat.compatible_version?).to eq(true) + end + + it "should return true when PowerShell v5.0 is installed" do + PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('5.0.201001.1') + + expect(@compat.compatible_version?).to eq(true) + end + end + + describe "when PowerShell v2 is installed" do + before(:each) do + PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('2.0') + end + + it "should return true when .NET 3.5 is installed" do + reg_key = mock('bob') + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100).yields(reg_key) + + expect(@compat.compatible_version?).to eq(true) + end + + it "should return false when .NET 3.5 is not installed" do + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once + + expect(@compat.compatible_version?).to eq(false) + end + end + + describe "when PowerShell is not installed or not compatible" do + it "should return false when PowerShell is not installed" do + PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns(nil) + + expect(@compat.compatible_version?).to eq(false) + end + + it "should return false when PowerShell is v1" do + PuppetX::PuppetLabs::PowerShell::PowerShellVersion.expects(:version).returns('1.0') + + expect(@compat.compatible_version?).to eq(false) + end + end +end diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb new file mode 100644 index 000000000..a182cc0d1 --- /dev/null +++ b/modules/utilities/windows/shells/puppetlabs_powershell_local/spec/unit/puppet_x/puppetlabs/powershell/powershell_version_spec.rb @@ -0,0 +1,75 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/type' +require 'puppet_x/puppetlabs/powershell/powershell_version' + +describe PuppetX::PuppetLabs::PowerShell::PowerShellVersion, :if => Puppet::Util::Platform.windows? do + before(:each) do + @ps = PuppetX::PuppetLabs::PowerShell::PowerShellVersion + end + + describe "when powershell is installed" do + + describe "when powershell version is greater than three" do + + it "should detect a powershell version" do + Win32::Registry.any_instance.expects(:[]).with('PowerShellVersion').returns('5.0.10514.6') + + version = @ps.version + + expect(version).to eq '5.0.10514.6' + end + + it "should call the powershell three registry path" do + reg_key = mock('bob') + reg_key.expects(:[]).with('PowerShellVersion').returns('5.0.10514.6') + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).yields(reg_key).once + + @ps.version + end + + it "should not call powershell one registry path" do + reg_key = mock('bob') + reg_key.expects(:[]).with('PowerShellVersion').returns('5.0.10514.6') + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).yields(reg_key) + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).times(0) + + @ps.version + end + end + + describe "when powershell version is less than three" do + + it "should detect a powershell version" do + Win32::Registry.any_instance.expects(:[]).with('PowerShellVersion').returns('2.0') + + version = @ps.version + + expect(version).to eq '2.0' + end + + it "should call powershell one registry path" do + reg_key = mock('bob') + reg_key.expects(:[]).with('PowerShellVersion').returns('2.0') + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).yields(reg_key).once + + version = @ps.version + + expect(version).to eq '2.0' + end + end + end + + describe "when powershell is not installed" do + + it "should return nil and not throw" do + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\3\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once + Win32::Registry.any_instance.expects(:open).with('SOFTWARE\Microsoft\PowerShell\1\PowerShellEngine', Win32::Registry::KEY_READ | 0x100).raises(Win32::Registry::Error.new(2), 'nope').once + + version = @ps.version + + expect(version).to eq nil + end + end +end From 004c740ccdc22229845a76ace8dc7137ad42069b Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Tue, 18 Apr 2017 21:54:33 +0100 Subject: [PATCH 14/24] Added .no_puppet module to ensure manifests directory is uploaded to GitHub to avoid error. --- .../shells/puppetlabs_powershell_local/manifests/.no_puppet | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 modules/utilities/windows/shells/puppetlabs_powershell_local/manifests/.no_puppet diff --git a/modules/utilities/windows/shells/puppetlabs_powershell_local/manifests/.no_puppet b/modules/utilities/windows/shells/puppetlabs_powershell_local/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb From aba2e2b028a2af438e41a05ec417d9dbc79f61fa Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Tue, 18 Apr 2017 23:13:07 +0100 Subject: [PATCH 15/24] Fixed error caused by total_memory option whereby the wrong type was not changed to the right type for a calculation in the packerfile (string -> int) --- modules/bases/windows_server_2008_r2_amd64/Packerfile.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bases/windows_server_2008_r2_amd64/Packerfile.erb b/modules/bases/windows_server_2008_r2_amd64/Packerfile.erb index d01d790b8..686bc1b2a 100644 --- a/modules/bases/windows_server_2008_r2_amd64/Packerfile.erb +++ b/modules/bases/windows_server_2008_r2_amd64/Packerfile.erb @@ -6,7 +6,7 @@ <%= if (@options.has_key? :memory_per_vm) "[ \"modifyvm\", \"{{.Name}}\", \"--memory\", \"#{@options[:memory_per_vm]}\" ]," elsif (@options.has_key? :total_memory) - "[ \"modifyvm\", \"{{.Name}}\", \"--memory\", \"#{@options[:total_memory]/@systems.length}\" ]," + "[ \"modifyvm\", \"{{.Name}}\", \"--memory\", \"#{@options[:total_memory].to_i/@systems.length}\" ]," end %> <%= if (@options.has_key? :max_cpu_cores) "[ \"modifyvm\", \"{{.Name}}\", \"--cpus\", \"#{@options[:max_cpu_cores]}\" ]," From 7c2e7f6be4c9694a14d6de83436e6f075adfdc27 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Thu, 20 Apr 2017 23:37:53 +0100 Subject: [PATCH 16/24] no_files file added to ensure makeshift puppet module files directory is synced to GitHub --- .../file_transfer_storage_module/files/.no_files | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/files/.no_files diff --git a/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/files/.no_files b/modules/forensics/windows/file_transfer_storage/file_transfer_storage_module/files/.no_files new file mode 100644 index 000000000..e69de29bb From d33b4f84307257b396a4b5981d03430869b3d7da Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Thu, 20 Apr 2017 23:48:04 +0100 Subject: [PATCH 17/24] Added registry module to add registry keys and registry key values. Also added access data registry viewer install module to view registry, although regedit.exe can be used instead. --- .../add_registry_keys/add_registry_keys.pp | 7 + .../add_registry_keys/manifests/init.pp | 7 + .../add_registry_keys/secgen_metadata.xml | 27 ++ .../add_registry_values.pp | 11 + .../add_registry_values/manifests/init.pp | 14 + .../add_registry_values/secgen_metadata.xml | 37 ++ .../access_data_registry_viewer.pp | 1 + .../manifests/install.pp | 8 + .../secgen_metadata.xml | 20 ++ .../puppetlabs_registry_library/CHANGELOG.md | 267 +++++++++++++++ .../CONTRIBUTING.md | 217 ++++++++++++ .../puppetlabs_registry_library/Gemfile | 167 +++++++++ .../puppetlabs_registry_library/LICENSE | 202 +++++++++++ .../MAINTAINERS.md | 6 + .../puppetlabs_registry_library/NOTICE | 15 + .../README.markdown | 251 ++++++++++++++ .../puppetlabs_registry_library/Rakefile | 38 +++ .../acceptance/.beaker-git.cfg | 11 + .../acceptance/.beaker-pe.cfg | 11 + .../config/masterless-windows-2008-x86_64.cfg | 17 + .../masterless-windows-2008r2-x86_64.cfg | 17 + .../config/masterless-windows-2012-x86_64.cfg | 17 + .../masterless-windows-2012r2-x86_64.cfg | 17 + .../acceptance/config/windows-2003r2-i386.cfg | 23 ++ .../config/windows-2003r2-x86_64.cfg | 23 ++ .../config/windows-2008r2-x86_64.cfg | 23 ++ .../acceptance/config/windows-2012-x86_64.cfg | 23 ++ .../acceptance/lib/systest.rb | 2 + .../acceptance/lib/systest/util.rb | 4 + .../acceptance/lib/systest/util/registry.rb | 232 +++++++++++++ .../acceptance/setup/install_puppet.rb | 19 ++ .../resource/registry/should_create_key.rb | 161 +++++++++ .../registry/should_have_defined_type.rb | 89 +++++ .../resource/registry/should_manage_values.rb | 323 ++++++++++++++++++ .../registry/should_tolerate_mixed_case.rb | 158 +++++++++ .../puppetlabs_registry_library/appveyor.yml | 42 +++ .../checksums.json | 52 +++ .../examples/compliance_example.pp | 111 ++++++ .../examples/purge_example.pp | 119 +++++++ .../examples/registry_examples.pp | 87 +++++ .../examples/service_example.pp | 36 ++ .../puppet/provider/registry_key/registry.rb | 65 ++++ .../provider/registry_value/registry.rb | 219 ++++++++++++ .../lib/puppet/type/registry_key.rb | 119 +++++++ .../lib/puppet/type/registry_value.rb | 138 ++++++++ .../lib/puppet_x/puppetlabs/registry.rb | 162 +++++++++ .../puppetlabs/registry/provider_base.rb | 229 +++++++++++++ .../manifests/service.pp | 135 ++++++++ .../manifests/value.pp | 83 +++++ .../puppetlabs_registry_library/metadata.json | 38 +++ .../puppetlabs_registry_library.pp | 0 .../secgen_metadata.xml | 18 + .../spec/acceptance/nodesets/centos-7-x64.yml | 10 + .../spec/acceptance/nodesets/debian-8-x64.yml | 10 + .../spec/acceptance/nodesets/default.yml | 10 + .../acceptance/nodesets/docker/centos-7.yml | 12 + .../acceptance/nodesets/docker/debian-8.yml | 11 + .../nodesets/docker/ubuntu-14.04.yml | 12 + .../spec/spec_helper.rb | 30 ++ .../unit/puppet/provider/registry_key_spec.rb | 116 +++++++ .../puppet/provider/registry_value_spec.rb | 223 ++++++++++++ .../unit/puppet/type/registry_key_spec.rb | 151 ++++++++ .../unit/puppet/type/registry_value_spec.rb | 161 +++++++++ .../spec/watchr.rb | 85 +++++ .../simple_registry_example.xml | 37 ++ 65 files changed, 4986 insertions(+) create mode 100644 modules/forensics/windows/registry/add_registry_keys/add_registry_keys.pp create mode 100644 modules/forensics/windows/registry/add_registry_keys/manifests/init.pp create mode 100644 modules/forensics/windows/registry/add_registry_keys/secgen_metadata.xml create mode 100644 modules/forensics/windows/registry/add_registry_values/add_registry_values.pp create mode 100644 modules/forensics/windows/registry/add_registry_values/manifests/init.pp create mode 100644 modules/forensics/windows/registry/add_registry_values/secgen_metadata.xml create mode 100644 modules/utilities/windows/registry/access_data_registry_viewer/access_data_registry_viewer.pp create mode 100644 modules/utilities/windows/registry/access_data_registry_viewer/manifests/install.pp create mode 100644 modules/utilities/windows/registry/access_data_registry_viewer/secgen_metadata.xml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/CHANGELOG.md create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/CONTRIBUTING.md create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/Gemfile create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/LICENSE create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/MAINTAINERS.md create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/NOTICE create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/README.markdown create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/Rakefile create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-git.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-pe.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008r2-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012r2-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-i386.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2008r2-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2012-x86_64.cfg create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util/registry.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/setup/install_puppet.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_create_key.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_have_defined_type.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_manage_values.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_tolerate_mixed_case.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/appveyor.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/checksums.json create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/examples/compliance_example.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/examples/purge_example.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/examples/registry_examples.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/examples/service_example.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_key/registry.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_value/registry.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_key.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_value.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry/provider_base.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/manifests/service.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/manifests/value.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/metadata.json create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/puppetlabs_registry_library.pp create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/secgen_metadata.xml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/centos-7-x64.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/debian-8-x64.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/default.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/centos-7.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/debian-8.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/ubuntu-14.04.yml create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/spec_helper.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_key_spec.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_value_spec.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_key_spec.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_value_spec.rb create mode 100644 modules/utilities/windows/registry/puppetlabs_registry_library/spec/watchr.rb create mode 100644 scenarios/simple_examples/forensic_examples/simple_registry_example.xml diff --git a/modules/forensics/windows/registry/add_registry_keys/add_registry_keys.pp b/modules/forensics/windows/registry/add_registry_keys/add_registry_keys.pp new file mode 100644 index 000000000..b05d24169 --- /dev/null +++ b/modules/forensics/windows/registry/add_registry_keys/add_registry_keys.pp @@ -0,0 +1,7 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$key_locations=$secgen_parameters['key_locations'] + +class { 'add_registry_keys': + key_locations => $key_locations[0], +} \ No newline at end of file diff --git a/modules/forensics/windows/registry/add_registry_keys/manifests/init.pp b/modules/forensics/windows/registry/add_registry_keys/manifests/init.pp new file mode 100644 index 000000000..8f92b3a04 --- /dev/null +++ b/modules/forensics/windows/registry/add_registry_keys/manifests/init.pp @@ -0,0 +1,7 @@ +class add_registry_keys ($key_locations) { + # $key_locations.each | Integer $index, String $key_location | { + registry_key { $key_locations: + ensure => present, + } + # } +} \ No newline at end of file diff --git a/modules/forensics/windows/registry/add_registry_keys/secgen_metadata.xml b/modules/forensics/windows/registry/add_registry_keys/secgen_metadata.xml new file mode 100644 index 000000000..f97537ddd --- /dev/null +++ b/modules/forensics/windows/registry/add_registry_keys/secgen_metadata.xml @@ -0,0 +1,27 @@ + + + + Add registry keys + Jason Keighley + Apache v2 + Add registry keys to Windows registry + + registry + windows + + + + + key_locations + + + HKLM\System\CurrentControlSet\Services\Puppet + + + + Registry library + + + \ No newline at end of file diff --git a/modules/forensics/windows/registry/add_registry_values/add_registry_values.pp b/modules/forensics/windows/registry/add_registry_values/add_registry_values.pp new file mode 100644 index 000000000..8871fd80d --- /dev/null +++ b/modules/forensics/windows/registry/add_registry_values/add_registry_values.pp @@ -0,0 +1,11 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$key_locations=$secgen_parameters['key_locations'] +$key_value_type=$secgen_parameters['key_value_type'] +$key_value=$secgen_parameters['key_value'] + +class { 'add_registry_values': + key_location => $key_locations[0], + key_value_type => $key_value_type[0], + key_value => $key_value[0], +} \ No newline at end of file diff --git a/modules/forensics/windows/registry/add_registry_values/manifests/init.pp b/modules/forensics/windows/registry/add_registry_values/manifests/init.pp new file mode 100644 index 000000000..14ec1d9eb --- /dev/null +++ b/modules/forensics/windows/registry/add_registry_values/manifests/init.pp @@ -0,0 +1,14 @@ +class add_registry_values($key_location, $key_value_type, $key_value) { + + # $key_locations.each | Integer $index, String $key_location | { + registry_key { $key_location: + ensure => present, + } + + registry_value { $key_location: + ensure => present, + type => $key_value_type, + data => $key_value, + } + # } +} \ No newline at end of file diff --git a/modules/forensics/windows/registry/add_registry_values/secgen_metadata.xml b/modules/forensics/windows/registry/add_registry_values/secgen_metadata.xml new file mode 100644 index 000000000..823669ce2 --- /dev/null +++ b/modules/forensics/windows/registry/add_registry_values/secgen_metadata.xml @@ -0,0 +1,37 @@ + + + + Add registry values + Jason Keighley + Apache v2 + Add registry values to Windows registry + + registry + windows + + + + + key_locations + key_value_type + key_value + + + HKLM\System\CurrentControlSet\Services\Puppet + + + + string + + + + String to demonstrate the module + + + + Registry library + + + \ No newline at end of file diff --git a/modules/utilities/windows/registry/access_data_registry_viewer/access_data_registry_viewer.pp b/modules/utilities/windows/registry/access_data_registry_viewer/access_data_registry_viewer.pp new file mode 100644 index 000000000..0e68c21d3 --- /dev/null +++ b/modules/utilities/windows/registry/access_data_registry_viewer/access_data_registry_viewer.pp @@ -0,0 +1 @@ +include access_data_registry_viewer::install \ No newline at end of file diff --git a/modules/utilities/windows/registry/access_data_registry_viewer/manifests/install.pp b/modules/utilities/windows/registry/access_data_registry_viewer/manifests/install.pp new file mode 100644 index 000000000..edf1f5cea --- /dev/null +++ b/modules/utilities/windows/registry/access_data_registry_viewer/manifests/install.pp @@ -0,0 +1,8 @@ +class access_data_registry_viewer::install { + include chocolatey + + package { 'access-data-registry-viewer': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/registry/access_data_registry_viewer/secgen_metadata.xml b/modules/utilities/windows/registry/access_data_registry_viewer/secgen_metadata.xml new file mode 100644 index 000000000..61a6c6192 --- /dev/null +++ b/modules/utilities/windows/registry/access_data_registry_viewer/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Access data registry viewer install + Jason Keighley + Apache v2 + An installation of the access data registry viewer + + registry + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/CHANGELOG.md b/modules/utilities/windows/registry/puppetlabs_registry_library/CHANGELOG.md new file mode 100644 index 000000000..55c0122d4 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/CHANGELOG.md @@ -0,0 +1,267 @@ +## 2017-03-06 - Supported Release 1.1.4 +### Summary + +This release allows `HKEY_USER` registry keys to be managed, removes Windows Server 2003 support and applies a few minor bugfixes. + +#### Features +- Allow keys and values in `HKEY_USERS` (`hku`) to be managed ([MODULES-3865](https://tickets.puppetlabs.com/browse/MODULES-3865)) +- Remove Windows Server 2003 as a supported Operating System + +#### Bugfixes +- Use double quotes so $key is interpolated ([FM-5236](https://tickets.puppetlabs.com/browse/FM-5236)) +- Fix WOW64 Constant Definition ([MODULES-3195](https://tickets.puppetlabs.com/browse/MODULES-3195)) +- Fix UNSET no longer available as a bareword ([MODULES-4331](https://tickets.puppetlabs.com/browse/MODULES-4331)) + +## 2015-12-08 - Supported Release 1.1.3 +### Summary + +Small release for support of newer PE versions. + +## 2015-08-13 - Supported Release 1.1.2 +### Summary + +Fix critical bug when writing dword and qword values. + +#### Bugfixes +- Fix the way we write dword and qword values [MODULES-2409](https://tickets.puppet.com/browse/MODULES-2409) +- changed byte conversion to use pack instead +- Added tests to catch scenario + +## ~~2015-08-12 - Supported Release 1.1.1~~ - Deleted +### Summary + +This release adds Puppet Enterprise 2015.2.0 to metadata + +#### Features +- Testcase fixes +- Gemfile updates +- Updated the logic used to convert to byte arrays +- [MODULES-1921](https://tickets.puppet.com/browse/MODULES-1921) Fixes for: +-- Ruby registry writes corrupt string [PR # 93](https://github.com/puppetlabs/puppetlabs-registry/commit/0b99718bc7f2d48752aa976d1ba30e49803e97f1) + +## 2015-03-24 - Supported Release 1.1.0 +### Summary + +This release adds support for Ruby 2.1.5 and issues with how Ruby reads back the registry in certain scenarios, see MODULES-1723 for more details. + +#### Bugfixes +- Additional tests for purge_values +- Use wide character registry APIs +- Test Ruby Registry methods uncalled +- Introduce Ruby 2.1.5 failing tests + + +## 2014-08-25 - Supported Release 1.0.3 +### Summary + +This release adds support for native x64 ruby and puppet 3.7, and bugfixes issues with non-leading-zero binary values in registry keys. + +## 2014-07-15 - Supported Release 1.0.2 +### Summary + +This release merely updates metadata.json so the module can be uninstalled and +upgraded via the puppet module command. + +## 2014-05-20 - Supported Release 1.0.1 +#### Bugfixes +- Add zero padding to binary single character inputs + +## 2014-03-04 - Supported Release 1.0.0 +### Summary +This is a supported release. + +#### Bugfixes +- Documentation updates +- Add license file + +#### Known Bugs + +* This module does not work if run as non-root. Please see [PE-2772](https://tickets.puppet.com/browse/PE-2772) + +--- + +## 2013-08-01 - Release 0.1.2 +### Summary: +This is a bugfix release that allows the module to work more reliably on x64 +systems and on older systems such as 2003. Also fixes compilation errors due +to windows library loading on *nix masters. + +#### Bugfixes: +- Refactored code into PuppetX namespace +- Fixed unhandled exception when loading windows code on *nix +- Updated README and manifest documentation +- Only manage redirected keys on 64 bit systems +- Only use /sysnative filesystem when available +- Use class accessor method instead of class instance variable +- Add geppetto project file + +--- + +##### 2012-05-21 - 0.1.1 - Jeff McCune + + * (#14517) Improve error handling when writing values (27223db) + * (#14572) Fix management of the default value (f29bdc5) + +##### 2012-05-16 - 0.1.0 - Jeff McCune + + * (#14529) Add registry::value defined type (bf44208) + +##### 2012-05-16 - Josh Cooper + + * Update README.markdown (2e9e45e) + +##### 2012-05-16 - Josh Cooper + + * Update README.markdown (3904838) + +##### 2012-05-15 - Josh Cooper + + * (Maint) Add type documentation (82205ad) + +##### 2012-05-15 - Josh Cooper + + * Remove note about case-sensitivity, as that is no longer an issue (5440a0e) + +##### 2012-05-15 - Jeff McCune + + * (#14501) Fix autorequire case sensitivity (d5c12f0) + +##### 2012-05-15 - Jeff McCune + + * (maint) Remove RegistryKeyPath#{valuename,default?} methods (29db478) + +##### 2012-05-14 - Jeff McCune + + * Add acceptance tests for registry_value provider (6285f4a) + +##### 2012-05-14 - Jeff McCune + + * Eliminate RegistryPathBas#(default?,valuename) from base class (2234f96) + +##### 2012-05-14 - Jeff McCune + + * Memoize the filter_path method for performance (6139b7d) + +##### 2012-05-11 - Jeff McCune + + * Add Registry_key ensure => absent and purge_values coverage (cfd3789) + +##### 2012-05-11 - Jeff McCune + + * Fix cannot alias error when managing 32 and 64 bit versions of a key (3a2f260) + +##### 2012-05-11 - Jeff McCune + + * Add registry_key creation acceptance test (0e68654) + +##### 2012-05-09 - Jeff McCune + + * Add acceptance tests for the registry type (0a01b11) + +##### 2012-05-08 - Jeff McCune + + * Update type description strings (c69bf2d) + +##### 2012-05-05 - Jeff McCune + + * Separate the implementation of the type and provider (4e06ae5) + +##### 2012-05-04 - Jeff McCune + + * Add watchr script to automatically run tests (d5bce2d) + +##### 2012-05-04 - Jeff McCune + + * Add registry::compliance_example class to test compliance (0aa8a68) + +##### 2012-05-03 - Jeff McCune + + * Allow values associated with a registry key to be purged (27eaee3) + +##### 2012-05-01 - Jeff McCune + + * Update README with info about the types provided (b9b2d11) + +##### 2012-04-30 - Jeff McCune + + * Add registry::service defined resource example (57c5b59) + +##### 2012-04-25 - Jeff McCune + + * Add REG_MULTI_SZ (type => array) implementation (1b17c6f) + +##### 2012-04-26 - Jeff McCune + + * Work around #3947, #4248, #14073; load our utility code (a8d9402) + +##### 2012-04-24 - Josh Cooper + + * Handle binary registry values (4353642) + +##### 2012-04-24 - Josh Cooper + + * Fix puppet resource registry_key (f736cff) + +##### 2012-04-23 - Josh Cooper + + * Registry keys and values were autorequiring all ancestors (0de7a0a) + +##### 2012-04-24 - Jeff McCune + + * Add examples of current registry key and value types (bb7e4f4) + +##### 2012-04-23 - Josh Cooper + + * Add the ability to manage 32 and 64-bit keys/values (9a16a9b) + +##### 2012-04-23 - Josh Cooper + + * Remove rspec deprecation warning (94063d5) + +##### 2012-04-23 - Josh Cooper + + * Rename registry-specific util code (cd2aaa1) + +##### 2012-04-20 - Josh Cooper + + * Fix autorequiring when using different root key forms (b7a1c39) + +##### 2012-04-19 - Josh Cooper + + * Refactor key and value paths (74ebc80) + +##### 2012-04-19 - Josh Cooper + + * Encode default-ness in the registry path (64bba67) + +##### 2012-04-19 - Josh Cooper + + * Better validation and testing of key paths (d05d1e6) + +##### 2012-04-19 - Josh Cooper + + * Maint: Remove more crlf line endings (e9f00c1) + +##### 2012-04-19 - Josh Cooper + + * Maint: remove windows cr line endings (0138a1d) + +##### 2012-04-18 - Josh Cooper + + * Rename `default` parameter (f45af86) + +##### 2012-04-18 - Josh Cooper + + * Fix modifying existing registry values (d06be98) + +##### 2012-04-18 - Josh Cooper + + * Remove debugging (8601e92) + +##### 2012-04-18 - Josh Cooper + + * Always split the path (de66832) + +##### 2012-04-18 - Josh Cooper + + * Initial registry key and value types and providers (065d43d) diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/CONTRIBUTING.md b/modules/utilities/windows/registry/puppetlabs_registry_library/CONTRIBUTING.md new file mode 100644 index 000000000..990edba7e --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/CONTRIBUTING.md @@ -0,0 +1,217 @@ +Checklist (and a short version for the impatient) +================================================= + + * Commits: + + - Make commits of logical units. + + - Check for unnecessary whitespace with "git diff --check" before + committing. + + - Commit using Unix line endings (check the settings around "crlf" in + git-config(1)). + + - Do not check in commented out code or unneeded files. + + - The first line of the commit message should be a short + description (50 characters is the soft limit, excluding ticket + number(s)), and should skip the full stop. + + - Associate the issue in the message. The first line should include + the issue number in the form "(#XXXX) Rest of message". + + - The body should provide a meaningful commit message, which: + + - uses the imperative, present tense: "change", not "changed" or + "changes". + + - includes motivation for the change, and contrasts its + implementation with the previous behavior. + + - Make sure that you have tests for the bug you are fixing, or + feature you are adding. + + - Make sure the test suites passes after your commit: + `bundle exec rspec spec/acceptance` More information on [testing](#Testing) below + + - When introducing a new feature, make sure it is properly + documented in the README.md + + * Submission: + + * Pre-requisites: + + - Make sure you have a [GitHub account](https://github.com/join) + + - [Create a ticket](https://tickets.puppet.com/secure/CreateIssue!default.jspa), or [watch the ticket](https://tickets.puppet.com/browse/) you are patching for. + + * Preferred method: + + - Fork the repository on GitHub. + + - Push your changes to a topic branch in your fork of the + repository. (the format ticket/1234-short_description_of_change is + usually preferred for this project). + + - Submit a pull request to the repository in the puppetlabs + organization. + +The long version +================ + + 1. Make separate commits for logically separate changes. + + Please break your commits down into logically consistent units + which include new or changed tests relevant to the rest of the + change. The goal of doing this is to make the diff easier to + read for whoever is reviewing your code. In general, the easier + your diff is to read, the more likely someone will be happy to + review it and get it into the code base. + + If you are going to refactor a piece of code, please do so as a + separate commit from your feature or bug fix changes. + + We also really appreciate changes that include tests to make + sure the bug is not re-introduced, and that the feature is not + accidentally broken. + + Describe the technical detail of the change(s). If your + description starts to get too long, that is a good sign that you + probably need to split up your commit into more finely grained + pieces. + + Commits which plainly describe the things which help + reviewers check the patch and future developers understand the + code are much more likely to be merged in with a minimum of + bike-shedding or requested changes. Ideally, the commit message + would include information, and be in a form suitable for + inclusion in the release notes for the version of Puppet that + includes them. + + Please also check that you are not introducing any trailing + whitespace or other "whitespace errors". You can do this by + running "git diff --check" on your changes before you commit. + + 2. Sending your patches + + To submit your changes via a GitHub pull request, we _highly_ + recommend that you have them on a topic branch, instead of + directly on "master". + It makes things much easier to keep track of, especially if + you decide to work on another thing before your first change + is merged in. + + GitHub has some pretty good + [general documentation](http://help.github.com/) on using + their site. They also have documentation on + [creating pull requests](http://help.github.com/send-pull-requests/). + + In general, after pushing your topic branch up to your + repository on GitHub, you can switch to the branch in the + GitHub UI and click "Pull Request" towards the top of the page + in order to open a pull request. + + + 3. Update the related GitHub issue. + + If there is a GitHub issue associated with the change you + submitted, then you should update the ticket to include the + location of your branch, along with any other commentary you + may wish to make. + +Testing +======= + +Getting Started +--------------- + +Our puppet modules provide [`Gemfile`](./Gemfile)s which can tell a ruby +package manager such as [bundler](http://bundler.io/) what Ruby packages, +or Gems, are required to build, develop, and test this software. + +Please make sure you have [bundler installed](http://bundler.io/#getting-started) +on your system, then use it to install all dependencies needed for this project, +by running + +```shell +% bundle install +Fetching gem metadata from https://rubygems.org/........ +Fetching gem metadata from https://rubygems.org/.. +Using rake (10.1.0) +Using builder (3.2.2) +-- 8><-- many more --><8 -- +Using rspec-system-puppet (2.2.0) +Using serverspec (0.6.3) +Using rspec-system-serverspec (1.0.0) +Using bundler (1.3.5) +Your bundle is complete! +Use `bundle show [gemname]` to see where a bundled gem is installed. +``` + +NOTE some systems may require you to run this command with sudo. + +If you already have those gems installed, make sure they are up-to-date: + +```shell +% bundle update +``` + +With all dependencies in place and up-to-date we can now run the tests: + +```shell +% bundle exec rake spec +``` + +This will execute all the [rspec tests](http://rspec-puppet.com/) tests +under [spec/defines](./spec/defines), [spec/classes](./spec/classes), +and so on. rspec tests may have the same kind of dependencies as the +module they are testing. While the module defines in its [Modulefile](./Modulefile), +rspec tests define them in [.fixtures.yml](./fixtures.yml). + +Some puppet modules also come with [beaker](https://github.com/puppetlabs/beaker) +tests. These tests spin up a virtual machine under +[VirtualBox](https://www.virtualbox.org/)) with, controlling it with +[Vagrant](http://www.vagrantup.com/) to actually simulate scripted test +scenarios. In order to run these, you will need both of those tools +installed on your system. + +You can run them by issuing the following command + +```shell +% bundle exec rake spec_clean +% bundle exec rspec spec/acceptance +``` + +This will now download a pre-fabricated image configured in the [default node-set](./spec/acceptance/nodesets/default.yml), +install puppet, copy this module and install its dependencies per [spec/spec_helper_acceptance.rb](./spec/spec_helper_acceptance.rb) +and then run all the tests under [spec/acceptance](./spec/acceptance). + +Writing Tests +------------- + +XXX getting started writing tests. + +If you have commit access to the repository +=========================================== + +Even if you have commit access to the repository, you will still need to +go through the process above, and have someone else review and merge +in your changes. The rule is that all changes must be reviewed by a +developer on the project (that did not write the code) to ensure that +all changes go through a code review process. + +Having someone other than the author of the topic branch recorded as +performing the merge is the record that they performed the code +review. + + +Additional Resources +==================== + +* [Getting additional help](http://puppet.com/community/get-help) + +* [Writing tests](https://docs.puppet.com/guides/module_guides/bgtm.html#step-three-module-testing) + +* [General GitHub documentation](http://help.github.com/) + +* [GitHub pull request documentation](http://help.github.com/send-pull-requests/) diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/Gemfile b/modules/utilities/windows/registry/puppetlabs_registry_library/Gemfile new file mode 100644 index 000000000..d16218590 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/Gemfile @@ -0,0 +1,167 @@ +#This file is generated by ModuleSync, do not edit. + +source ENV['GEM_SOURCE'] || "https://rubygems.org" + +# Determines what type of gem is requested based on place_or_version. +def gem_type(place_or_version) + if place_or_version =~ /^git:/ + :git + elsif place_or_version =~ /^file:/ + :file + else + :gem + end +end + +# Find a location or specific version for a gem. place_or_version can be a +# version, which is most often used. It can also be git, which is specified as +# `git://somewhere.git#branch`. You can also use a file source location, which +# is specified as `file://some/location/on/disk`. +def location_for(place_or_version, fake_version = nil) + if place_or_version =~ /^(git[:@][^#]*)#(.*)/ + [fake_version, { :git => $1, :branch => $2, :require => false }].compact + elsif place_or_version =~ /^file:\/\/(.*)/ + ['>= 0', { :path => File.expand_path($1), :require => false }] + else + [place_or_version, { :require => false }] + end +end + +# Used for gem conditionals +supports_windows = true + +# The following gems are not included by default as they require DevKit on Windows. +# You should probably include them in a Gemfile.local or a ~/.gemfile +#gem 'pry' #this may already be included in the gemfile +#gem 'pry-stack_explorer', :require => false +#if RUBY_VERSION =~ /^2/ +# gem 'pry-byebug' +#else +# gem 'pry-debugger' +#end + +group :development do + gem 'puppet-lint', :require => false + gem 'metadata-json-lint', :require => false, :platforms => 'ruby' + gem 'puppet_facts', :require => false + gem 'puppet-blacksmith', '>= 3.4.0', :require => false, :platforms => 'ruby' + gem 'puppetlabs_spec_helper', '>= 1.2.1', :require => false + gem 'rspec-puppet', '>= 2.3.2', :require => false + gem 'rspec-puppet-facts', :require => false, :platforms => 'ruby' + gem 'mocha', '< 1.2.0', :require => false + gem 'simplecov', :require => false, :platforms => 'ruby' + gem 'parallel_tests', '< 2.10.0', :require => false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') + gem 'parallel_tests', :require => false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.0.0') + gem 'rubocop', '0.41.2', :require => false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') + gem 'rubocop', :require => false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.0.0') + gem 'rubocop-rspec', '~> 1.6', :require => false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.3.0') + gem 'pry', :require => false + gem 'json_pure', '<= 2.0.1', :require => false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.0.0') + gem 'fast_gettext', '1.1.0', :require => false if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.1.0') + gem 'fast_gettext', :require => false if Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('2.1.0') + gem 'rainbow', '< 2.2.0', :require => false +end + +group :system_tests do + gem 'beaker', *location_for(ENV['BEAKER_VERSION'] || '>= 3') + gem 'beaker-pe', :require => false + gem 'beaker-rspec', *location_for(ENV['BEAKER_RSPEC_VERSION']) + gem 'beaker-puppet_install_helper', :require => false + gem 'beaker-module_install_helper', :require => false + gem 'master_manipulator', :require => false + gem 'beaker-hostgenerator', *location_for(ENV['BEAKER_HOSTGENERATOR_VERSION']) + gem 'beaker-abs', *location_for(ENV['BEAKER_ABS_VERSION'] || '~> 0.1') +end + +gem 'puppet', *location_for(ENV['PUPPET_GEM_VERSION']) + +# Only explicitly specify Facter/Hiera if a version has been specified. +# Otherwise it can lead to strange bundler behavior. If you are seeing weird +# gem resolution behavior, try setting `DEBUG_RESOLVER` environment variable +# to `1` and then run bundle install. +gem 'facter', *location_for(ENV['FACTER_GEM_VERSION']) if ENV['FACTER_GEM_VERSION'] +gem 'hiera', *location_for(ENV['HIERA_GEM_VERSION']) if ENV['HIERA_GEM_VERSION'] + +# For Windows dependencies, these could be required based on the version of +# Puppet you are requiring. Anything greater than v3.5.0 is going to have +# Windows-specific dependencies dictated by the gem itself. The other scenario +# is when you are faking out Puppet to use a local file path / git path. +explicitly_require_windows_gems = false +puppet_gem_location = gem_type(ENV['PUPPET_GEM_VERSION']) +# This is not a perfect answer to the version check +if puppet_gem_location != :gem || (ENV['PUPPET_GEM_VERSION'] && Gem::Version.correct?(ENV['PUPPET_GEM_VERSION']) && Gem::Requirement.new('< 3.5.0').satisfied_by?(Gem::Version.new(ENV['PUPPET_GEM_VERSION'].dup))) + if Gem::Platform.local.os == 'mingw32' + explicitly_require_windows_gems = true + end + if puppet_gem_location == :gem + # If facterversion hasn't been specified and we are + # looking for a Puppet Gem version less than 3.5.0, we + # need to ensure we get a good Facter for specs. + gem "facter",">= 1.6.11","<= 1.7.5",:require => false unless ENV['FACTER_GEM_VERSION'] + # If hieraversion hasn't been specified and we are + # looking for a Puppet Gem version less than 3.5.0, we + # need to ensure we get a good Hiera for specs. + gem "hiera",">= 1.0.0","<= 1.3.0",:require => false unless ENV['HIERA_GEM_VERSION'] + end +end + +if explicitly_require_windows_gems + # This also means Puppet Gem less than 3.5.0 - this has been tested back + # to 3.0.0. Any further back is likely not supported. + if puppet_gem_location == :gem + gem "ffi", "1.9.0", :require => false + gem "win32-eventlog", "0.5.3","<= 0.6.5", :require => false + gem "win32-process", "0.6.5","<= 0.7.5", :require => false + gem "win32-security", "~> 0.1.2","<= 0.2.5", :require => false + gem "win32-service", "0.7.2","<= 0.8.8", :require => false + gem "minitar", "0.5.4", :require => false + else + gem "ffi", "~> 1.9.0", :require => false + gem "win32-eventlog", "~> 0.5","<= 0.6.5", :require => false + gem "win32-process", "~> 0.6","<= 0.7.5", :require => false + gem "win32-security", "~> 0.1","<= 0.2.5", :require => false + gem "win32-service", "~> 0.7","<= 0.8.8", :require => false + gem "minitar", "~> 0.5.4", :require => false + end + + gem "win32-dir", "~> 0.3","<= 0.4.9", :require => false + gem "win32console", "1.3.2", :require => false if RUBY_VERSION =~ /^1\./ + + # sys-admin was removed in Puppet 3.7.0+, and doesn't compile + # under Ruby 2.3 - so restrict it to Ruby 1.x + gem "sys-admin", "1.5.6", :require => false if RUBY_VERSION =~ /^1\./ + + # Puppet less than 3.7.0 requires these. + # Puppet 3.5.0+ will control the actual requirements. + # These are listed in formats that work with all versions of + # Puppet from 3.0.0 to 3.6.x. After that, these were no longer used. + # We do not want to allow newer versions than what came out after + # 3.6.x to be used as they constitute some risk in breaking older + # functionality. So we set these to exact versions. + gem "win32-api", "1.4.8", :require => false + gem "win32-taskscheduler", "0.2.2", :require => false + gem "windows-api", "0.4.3", :require => false + gem "windows-pr", "1.2.3", :require => false +else + if Gem::Platform.local.os == 'mingw32' + # If we're using a Puppet gem on windows, which handles its own win32-xxx gem dependencies (Pup 3.5.0 and above), set maximum versions + # Required due to PUP-6445 + gem "win32-dir", "<= 0.4.9", :require => false + gem "win32-eventlog", "<= 0.6.5", :require => false + gem "win32-process", "<= 0.7.5", :require => false + gem "win32-security", "<= 0.2.5", :require => false + gem "win32-service", "<= 0.8.8", :require => false + end +end + +# Evaluate Gemfile.local if it exists +if File.exists? "#{__FILE__}.local" + eval(File.read("#{__FILE__}.local"), binding) +end + +# Evaluate ~/.gemfile if it exists +if File.exists?(File.join(Dir.home, '.gemfile')) + eval(File.read(File.join(Dir.home, '.gemfile')), binding) +end + +# vim:ft=ruby diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/LICENSE b/modules/utilities/windows/registry/puppetlabs_registry_library/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/MAINTAINERS.md b/modules/utilities/windows/registry/puppetlabs_registry_library/MAINTAINERS.md new file mode 100644 index 000000000..6ef3c581b --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/MAINTAINERS.md @@ -0,0 +1,6 @@ +## Maintenance + +Maintainers: + - Puppet Windows Team `windows |at| puppet |dot| com` + +Tickets: https://tickets.puppet.com/browse/MODULES. Make sure to set component to `registry`. diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/NOTICE b/modules/utilities/windows/registry/puppetlabs_registry_library/NOTICE new file mode 100644 index 000000000..324523d7b --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/NOTICE @@ -0,0 +1,15 @@ +Puppet Module - puppetlabs-registry + +Copyright 2012 - 2017 Puppet, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/README.markdown b/modules/utilities/windows/registry/puppetlabs_registry_library/README.markdown new file mode 100644 index 000000000..dc4fb1cb0 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/README.markdown @@ -0,0 +1,251 @@ +# registry +[![Build Status](https://travis-ci.org/puppetlabs/puppetlabs-registry.png?branch=master)](https://travis-ci.org/puppetlabs/puppetlabs-registry) + +#### Table of Contents + +1. [Overview - What is the registry module?](#overview) +2. [Module Description - What registry does and why it is useful](#module-description) +3. [Setup - The basics of getting started with registry](#setup) + * [Beginning with registry](#beginning-with-registry) +4. [Usage - Configuration options and additional functionality](#usage) +5. [Reference](#reference) + * [Public Defines](#public-defines) + * [Public Types](#public-types) + * [Parameters](#parameters) +6. [Limitations](#limitations) +7. [Development - Guide for contributing to registry](#development) + +## Overview + +This module supplies the types and providers you'll need to manage the Registry on your Windows nodes. + +## Module Description + +The Registry is a hierarchical database built into Microsoft Windows. It stores settings and other information for the operating system and a wide range of applications. This module lets Puppet manage individual Registry keys and values, and provides a simplified way to manage Windows services. + +## Setup + +This module must be installed on your Puppet master. We've tested it with Puppet agents running on Windows Server 2008 R2, 2012, and 2012 R2. + +### Beginning with registry + +Use the `registry_key` type to manage a single registry key: + +``` puppet +registry_key { 'HKLM\System\CurrentControlSet\Services\Puppet': + ensure => present, +} +``` + +## Usage + +The registry module works mainly through two types: `registry_key` and `registry_value`. These types combine to let you specify a Registry container and its intended contents. + +### Manage a single Registry value + +``` puppet +registry_value { 'HKLM\System\CurrentControlSet\Services\Puppet\Description': + ensure => present, + type => string, + data => "The Puppet Agent service periodically manages your configuration", +} +``` + +### Manage a Registry value and its parent key in one declaration + +``` puppet +class myapp { + registry::value { 'puppetmaster': + key => 'HKLM\Software\Vendor\PuppetLabs', + data => 'puppet.puppet.com', + } +} +``` + +Puppet looks up the key 'HKLM\Software\Vendor\PuppetLabs' and makes sure it contains a value named 'puppetmaster' containing the string 'puppet.puppet.com'. + +**Note:** the `registry::value` define only manages keys and values in the system-native architecture. In other words, 32-bit keys applied in a 64-bit OS aren't managed by this define; instead, you must use the types, [`registry_key`](#type-registry_key) and [`registry_value`](#type-registry_value) individually. + +Within this define, you can specify multiple Registry values for one Registry key and manage them all at once. + +### Set the default value for a key + +``` puppet +registry::value { 'Setting0': + key  => 'HKLM\System\CurrentControlSet\Services\Puppet', + value => '(default)', + data  => "Hello World!", +} +``` + +You can still add values in a string (or array) beyond the default, but you can only set one default value per key. + + +### Purge existing values + +By default, if a key includes additional values besides the ones you specify through this module, Puppet leaves those extra values in place. To change that, use the `purge_values => true` parameter of the `registry_key` resource. **Enabling this feature deletes any values in the key that are not managed by Puppet.** + +The `registry::purge_example` class provides a quick and easy way to see a demonstration of how this works. This example class has two modes of operation determined by the Facter fact `PURGE_EXAMPLE_MODE`: 'setup' and 'purge'. + +To run the demonstration, make sure the `registry::purge_example` class is included in your node catalog, then set an environment variable in PowerShell. This sets up a Registry key that contains six values. + +``` powershell + PS C:\> $env:FACTER_PURGE_EXAMPLE_MODE = 'setup' + PS C:\> puppet agent --test + + notice: /Stage[main]/Registry::Purge_example/Registry_key[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value3]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value2]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_key[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\SubKey]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value5]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value6]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\SubKey\Value1]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value1]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\SubKey\Value2]/ensure: created + notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value4]/ensure: created + notice: Finished catalog run in 0.14 seconds +``` + +Switching the mode to 'purge' causes the class to only manage three of the six `registry_value` resources. The other three are purged because they are not specifically declared in the manifest. +Notice how `Value4`, `Value5` and `Value6` are being removed. + +``` powershell +PS C:\> $env:FACTER_PURGE_EXAMPLE_MODE = 'purge' +PS C:\> puppet agent --test + +notice: /Registry_value[hklm\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value4]/ensure: removed +notice: /Registry_value[hklm\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value6]/ensure: removed +notice: /Registry_value[hklm\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value5]/ensure: removed +notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value3]/data: data changed 'key3' to 'should not be purged' +notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value2]/data: data changed '2' to '0' +notice: /Stage[main]/Registry::Purge_example/Registry_value[HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge\Value1]/data: data changed '1' to '0' +notice: Finished catalog run in 0.16 seconds +``` + +### Manage Windows services + +The `registry::service` define manages entries in the Microsoft service control framework by automatically manipulating values in the key `HKLM\System\CurrentControlSet\Services\$name\`. + +This is an alternative approach to using INSTSRV.EXE [1](http://support.microsoft.com/kb/137890). + +``` powershell +registry::service { puppet: + ensure => present, + display_name => "Puppet Agent", + description => "Periodically fetches and applies configurations from a Puppet master server.", + command => 'C:\PuppetLabs\Puppet\service\daemon.bat', +} +``` + +## Reference + +### Public Defines +* `registry::value`: Manages the parent key for a particular value. If the parent key doesn't exist, Puppet automatically creates it. +* `registry::service`: Manages entries in the Microsoft service control framework by manipulating values in the key `HKLM\System\CurrentControlSet\Services\$name\`. + +### Public Types +* `registry_key`: Manages individual Registry keys. +* `registry_value`: Manages individual Registry values. + +### Parameters + +#### `registry::value`: + +All parameters are required unless otherwise stated. + +##### `key` + +Specifies a Registry key for Puppet to manage. Note that if any of the parent keys in the path do not exist, the resource raises an error. Use the `registry_key` to create the parent key prior to setting a registry value. Valid options: a string containing a Registry path. + +##### `data` + +Provides the contents of the specified value. Valid options: a string by default; an array if specified through the `type` parameter. + +##### `type` + +*Optional.* Sets the data type of the specified value. Valid options: 'string', 'array', 'dword', 'qword', 'binary' and 'expand'. Default value: 'string'. + +##### `value` + +*Optional.* Determines what Registry value(s) to manage within the specified key. To set a Registry value as the default value for its parent key, name the value '(default)'. Valid options: a string. Default value: the title of your declared resource. + +#### `registry_key` + +##### `ensure` + +Tells Puppet whether the key should or shouldn't exist. Valid options: 'present' and 'absent'. Default value: 'present'. + +##### `path` + +Specifies a Registry key for Puppet to manage. If any of the parent keys in the path don't exist, Puppet creates them automatically. Valid options: a string containing a Registry path. For example: `HKLM\Software` or `HKEY_LOCAL_MACHINE\Software\Vendor`. + +If Puppet is running on a 64-bit system, the 32-bit Registry key can be explicitly managed using a prefix. For example: `32:HKLM\Software`. + +##### `purge_values` + +*Optional.* Specifies whether to delete any values in the specified key that are not managed by Puppet. Valid options: `true` and `false`. Default value: `false`. + +For more on this parameter, see the [Purge existing values section](#purge-existing-values) under Usage. + +#### `registry_value` + +##### `path` + +*Optional.* Specifies a Registry value for Puppet to manage. Valid options: a string containing a Registry path. If any of the parent keys in the path don't exist, Puppet creates them automatically. For example: `HKLM\Software` or `HKEY_LOCAL_MACHINE\Software\Vendor`. Default value: the title of your declared resource. + +If Puppet is running on a 64-bit system, the 32-bit Registry key can be explicitly managed using a prefix. For example: `32:HKLM\Software\Value3`. + +##### `ensure` + +Tells Puppet whether the value should or shouldn't exist. Valid options: 'present' and 'absent'. Default value: 'present'. + +##### `type` + +*Optional.* Sets the data type of the specified value. Valid options: 'string', 'array', 'dword', 'qword', 'binary' and 'expand'. Default value: 'string'. + +##### `data` + +Provides the contents of the specified value. Valid options: a string by default; an array if specified through the `type` parameter. + +#### `registry::service` + +##### `ensure` + +Tells Puppet whether the service should or shouldn't exist. Valid options: 'present' and 'absent'. Default value: 'present'. + +##### `display_name` + +*Optional.* Provides a Display Name for the service. Valid options: a string. Default value: the title of your declared resource. + +##### `description` + +*Optional.* Provides a description of the service. Valid options: a string. Default value: blank. + +##### `command` + +Specifies the command to execute when starting the service. Valid options: a string containing the absolute path to an executable file. + +##### `start` + +Specifies the starting mode of the service. Valid options: 'automatic', 'manual' and 'disabled'. + +Puppet's [native service resource](http://docs.puppet.com/references/latest/type.html#service) can also be used to manage this setting. + +## Limitations + +* Keys within `HKEY_LOCAL_MACHINE` (`hklm`), `HKEY_CLASSES_ROOT` (`hkcr`) or `HKEY_USERS` (`hku`) are supported. Other predefined root keys (e.g., `HKEY_CURRENT_USER`) are not currently supported. +* Puppet doesn't recursively delete Registry keys. + +Please report any issues through our [Module Issue Tracker](https://tickets.puppet.com/browse/MODULES). + +## Development + +Puppet Inc modules on the Puppet Forge are open projects, and community contributions are essential for keeping them great. We can't access the huge number of platforms and myriad of hardware, software, and deployment configurations that Puppet is intended to serve. + +We want to keep it as easy as possible to contribute changes so that our modules work in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. + +For more information, see our [module contribution guide.](https://docs.puppet.com/forge/contributing.html) + +### Contributors + +To see who's already involved, see the [list of contributors.](https://github.com/puppetlabs/puppetlabs-registry/graphs/contributors) diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/Rakefile b/modules/utilities/windows/registry/puppetlabs_registry_library/Rakefile new file mode 100644 index 000000000..24644f7fe --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/Rakefile @@ -0,0 +1,38 @@ + require 'puppetlabs_spec_helper/rake_tasks' + require 'puppet-lint/tasks/puppet-lint' + require 'puppet_blacksmith/rake_tasks' if Bundler.rubygems.find_name('puppet-blacksmith').any? +begin + require 'beaker/tasks/test' +rescue LoadError + #Do nothing, rescue for Windows as beaker does not work and will not be installed +end + +#Due to puppet-lint not ignoring tests folder or the ignore paths attribute +#we have to ignore many things +# #Due to bug in puppet-lint we have to clear and redo the lint tasks to achieve ignore paths + Rake::Task[:lint].clear + PuppetLint::RakeTask.new(:lint) do |config| + config.pattern = 'manifests/**/*.pp' + config.fail_on_warnings = true + config.disable_checks = [ + '80chars', + 'class_inherits_from_params_class', + 'class_parameter_defaults', + 'documentation', + 'single_quote_string_with_variables'] + config.ignore_paths = ["tests/*.pp", "spec/**/*.pp", "pkg/**/*.pp"] + end + +task :default => [:test] + +desc 'Run RSpec' +RSpec::Core::RakeTask.new(:test) do |t| + t.pattern = 'spec/{unit}/**/*.rb' + #t.rspec_opts = ['--color'] +end + +desc 'Generate code coverage' +RSpec::Core::RakeTask.new(:coverage) do |t| + t.rcov = true + t.rcov_opts = ['--exclude', 'spec'] +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-git.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-git.cfg new file mode 100644 index 000000000..50c628773 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-git.cfg @@ -0,0 +1,11 @@ +{ + :load_path => "./acceptance/lib/", + :hosts_file => './acceptance/config/windows-2012-x86_64.cfg', + :type => "foss", + :pre_suite => ["./acceptance/setup/install_puppet.rb"], + :tests => "./acceptance/tests", + :log_level => "debug", + :timeout => 6000, + :ntp => true, + :keyfile => "~/.ssh/id_rsa-acceptance" +} diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-pe.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-pe.cfg new file mode 100644 index 000000000..b0eaa19d9 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/.beaker-pe.cfg @@ -0,0 +1,11 @@ +{ + :load_path => "./acceptance/lib/", + :hosts_file => './acceptance/config/windows-2012-x86_64.cfg', + :type => "pe", + :pre_suite => ['./acceptance/setup/install_puppet.rb'], + :tests => "./acceptance/tests/", + :log_level => "debug", + :timeout => 6000, + :ntp => true, + :keyfile => "~/.ssh/id_rsa-acceptance" +} diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008-x86_64.cfg new file mode 100644 index 000000000..74ddd3ac6 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008-x86_64.cfg @@ -0,0 +1,17 @@ +HOSTS: + w2008: + roles: + - agent + - default + platform: windows-2008-x86_64 + template: win-2008-x86_64 + hypervisor: vcloud +CONFIG: + type: foss + keyfile: ~/.ssh/id_rsa-acceptance + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008r2-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008r2-x86_64.cfg new file mode 100644 index 000000000..86c6fe073 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2008r2-x86_64.cfg @@ -0,0 +1,17 @@ +HOSTS: + w2008r2: + roles: + - agent + - default + platform: windows-2008r2-x86_64 + template: win-2008r2-x86_64 + hypervisor: vcloud +CONFIG: + type: foss + keyfile: ~/.ssh/id_rsa-acceptance + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012-x86_64.cfg new file mode 100644 index 000000000..37a273d70 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012-x86_64.cfg @@ -0,0 +1,17 @@ +HOSTS: + w2012: + roles: + - agent + - default + platform: windows-2012-x86_64 + template: win-2012-x86_64 + hypervisor: vcloud +CONFIG: + type: foss + keyfile: ~/.ssh/id_rsa-acceptance + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012r2-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012r2-x86_64.cfg new file mode 100644 index 000000000..fbad38cbe --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/masterless-windows-2012r2-x86_64.cfg @@ -0,0 +1,17 @@ +HOSTS: + w2012r2: + roles: + - agent + - default + platform: windows-2012r2-x86_64 + template: win-2012r2-x86_64 + hypervisor: vcloud +CONFIG: + type: foss + keyfile: ~/.ssh/id_rsa-acceptance + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-i386.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-i386.cfg new file mode 100644 index 000000000..3879e144b --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-i386.cfg @@ -0,0 +1,23 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + w2k3r2: + roles: + - agent + platform: windows-2003r2-i386 + template: win-2003r2-i386 + hypervisor: vcloud +CONFIG: + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-x86_64.cfg new file mode 100644 index 000000000..51b621005 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2003r2-x86_64.cfg @@ -0,0 +1,23 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + w2k3r2: + roles: + - agent + platform: windows-2003r2-x86_64 + template: win-2003r2-x86_64 + hypervisor: vcloud +CONFIG: + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2008r2-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2008r2-x86_64.cfg new file mode 100644 index 000000000..af8788263 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2008r2-x86_64.cfg @@ -0,0 +1,23 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + w2k8r2: + roles: + - agent + platform: windows-2008r2-x86_64 + template: win-2008r2-x86_64 + hypervisor: vcloud + pe_dir: http://neptune.puppetlabs.lan/3.2/ci-ready/ +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2012-x86_64.cfg b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2012-x86_64.cfg new file mode 100644 index 000000000..1fdc6a043 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/config/windows-2012-x86_64.cfg @@ -0,0 +1,23 @@ +HOSTS: + ubuntu1204: + roles: + - master + - database + - dashboard + platform: ubuntu-12.04-amd64 + template: ubuntu-1204-x86_64 + hypervisor: vcloud + w2012: + roles: + - agent + - default + platform: windows-2012-x86_64 + template: win-2012-x86_64 + hypervisor: vcloud +CONFIG: + nfs_server: none + consoleport: 443 + datastore: instance0 + folder: Delivery/Quality Assurance/FOSS/Dynamic + resourcepool: delivery/Quality Assurance/FOSS/Dynamic + pooling_api: http://vcloud.delivery.puppetlabs.net/ diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest.rb new file mode 100644 index 000000000..d7513b8b0 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest.rb @@ -0,0 +1,2 @@ +module Systest +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util.rb new file mode 100644 index 000000000..61da9151e --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util.rb @@ -0,0 +1,4 @@ +require 'pathname' +require Pathname.new(__FILE__).dirname +module Systest::Util +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util/registry.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util/registry.rb new file mode 100644 index 000000000..ecf86604a --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/lib/systest/util/registry.rb @@ -0,0 +1,232 @@ +require 'pathname' + +require Pathname.new(__FILE__).dirname +# This module is meant to be mixed into the individual test cases for the +# registry module. +module Systest::Util::Registry + FUTURE_PARSER = ENV['FUTURE_PARSER'] == 'true' + + # Given a relative path, returns an absolute path for a test file. + # Basically, this just prepends the a unique temp dir path (specific to the + # current test execution) to your relative path. + def get_test_file_path(host, file_rel_path) + File.join(host_test_tmp_dirs[host.name], file_rel_path) + end + + def cur_test_file + @path + end + + def cur_test_file_shortname + File.basename(cur_test_file, File.extname(cur_test_file)) + end + + def tmpdir(host, basename) + host_tmpdir = host.tmpdir(basename) + # we need to make sure that the puppet user can traverse this directory... + chmod(host, "755", host_tmpdir) + host_tmpdir + end + + def mkdirs(host, dir_path) + on(host, "mkdir -p #{dir_path}") + end + + def chown(host, owner, group, path) + on(host, "chown #{owner}:#{group} #{path}") + end + + def chmod(host, mode, path) + on(host, "chmod #{mode} #{path}") + end + + def all_hosts + # we need one list of all of the hosts, to assist in managing temp dirs. It's possible + # that the master is also an agent, so this will consolidate them into a unique set + hosts + end + + def host_test_tmp_dirs + # now we can create a hash of temp dirs--one per host, and unique to this test--without worrying about + # doing it twice on any individual host + @host_test_tmp_dirs ||= Hash[all_hosts.map do |host| + [host.name, tmpdir(host, cur_test_file_shortname)] + end] + end + + def master_manifest_dir + @master_manifest_dir ||= "master_manifest" + end + + def master_module_dir + @master_module_dir ||= "master_modules" + end + + def master_manifest_file + @master_manifest_file ||= "#{master_manifest_dir}/site.pp" + end + + def agent_args + @agent_args ||= "--trace --libdir=\"%s\" --pluginsync --no-daemonize --verbose --onetime --test --server #{master}" + end + + def agent_lib_dir + @agent_lib_dir ||= "agent_lib" + end + + def masters + @masters ||= hosts.select { |host| host['roles'].include? 'master' } || [] + end + + def windows_agents + agents.select { |agent| agent['platform'].include?('windows') } + end + + def master_options + @master_options ||= "--manifest=\"#{get_test_file_path(master, master_manifest_file)}\" " + + "--modulepath=\"#{get_test_file_path(master, master_module_dir)}\" " + + "--autosign true --pluginsync" + end + + def master_options_hash + @master_options_hash ||= { + :manifest => "#{get_test_file_path(master, master_manifest_file)}", + :modulepath => "#{get_test_file_path(master, master_module_dir)} ", + :autosign => true, + :pluginsync => true + } + end + + def agent_exit_codes + # legal exit codes whenever we run the agent + # we need to allow exit code 2, which means "changes were applied" on the agent + @agent_exit_codes ||= [0, 2] + end + + def x64?(agent) + on(agent, facter('architecture')).stdout.chomp == 'x64' + end + + def native_sysdir(agent) + if x64?(agent) + if on(agent, 'ls /cygdrive/c/windows/sysnative', :acceptable_exit_codes => (0..255)).exit_code == 0 + '`cygpath -W`/sysnative' + else + nil + end + else + '`cygpath -S`' + end + end + + def randomstring(length) + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + str = "" + 1.upto(length) { |i| str << chars[rand(chars.size-1)] } + return str + end + + # Create a file on the host. + # Parameters: + # [host] the host to create the file on + # [file_path] the path to the file to be created + # [file_content] a string containing the contents to be written to the file + # [options] a hash containing additional behavior options. Currently supported: + # * :mkdirs (default false) if true, attempt to create the parent directories on the remote host before writing + # the file + # * :owner (default 'root') the username of the user that the file should be owned by + # * :group (default 'puppet') the name of the group that the file should be owned by + # * :mode (default '644') the mode (file permissions) that the file should be created with + def create_test_file(host, file_rel_path, file_content, options) + + # set default options + options[:mkdirs] ||= false + options[:owner] ||= (host['user'] || "root") + options[:group] ||= (host['group'] || "puppet") + options[:mode] ||= "755" + + file_path = get_test_file_path(host, file_rel_path) + + mkdirs(host, File.dirname(file_path)) if (options[:mkdirs] == true) + create_remote_file(host, file_path, file_content) + # + # NOTE: we need these `chown/chmod calls because the acceptance framework connects to the nodes as "root", but + # puppet 'master' runs as user 'puppet'. Therefore, in order for puppet master to be able to read any files + # that we've created, we have to carefully set their permissions + # + chown(host, options[:owner], options[:group], file_path) + chmod(host, options[:mode], file_path) + end + + def puppet_module_install(host = nil, source = nil, module_name = nil, module_path = '/etc/puppet/modules') + opts = {:source => source, :module_name => module_name, :target_module_path => module_path} + copy_root_module_to(host, opts) + end + + def setup_master(master_manifest_content="# Intentionally Blank\n") + step "Setup Puppet Master Manifest" do + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '../../../')) + if any_hosts_as?('master') do + masters.each do |host| + puppet_module_install(host, proj_root, 'registry', File.join(host['puppetpath'], "modules")) + create_test_file(host, master_manifest_file, master_manifest_content, :mkdirs => true) + puppet_conf_update_ini = <<-MANIFEST + ini_setting{'Update Puppet.Conf': + ensure => present, + section => 'main', + key_val_separator => '=', + path => '#{host['puppetpath']}/puppet.conf', + setting => 'manifestdir', + value => '#{host_test_tmp_dirs[host.name]}/master_manifest/' } + MANIFEST + on host, puppet('apply', '--debug'), :stdin => puppet_conf_update_ini + end + end + end + end + step "Symlink the module(s) into the master modulepath" do + if any_hosts_as?('master') do + masters.each do |host| + moddir = get_test_file_path(host, master_module_dir) + mkdirs(host, moddir) + #on host, "ln -s /opt/puppet-git-repos/stdlib \"#{moddir}/stdlib\"; ln -s /opt/puppet-git-repos/registry \"#{moddir}/registry\"" + end + end + end + end + end + + def clean_up + step "Clean Up" do + if any_hosts_as?(:master) + masters.each do |host| + puppet_conf_update_ini = <<-MANIFEST + ini_setting{'Revert Puppet.Conf': + ensure => absent, + section => 'main', + key_val_separator => '=', + path => '#{host['puppetpath']}/puppet.conf', + setting => 'manifestdir' } + MANIFEST + on host, puppet('apply', '--debug'), :stdin => puppet_conf_update_ini + on host, "rm -rf \"%s\"" % get_test_file_path(host, '') + end + end + agents.each do |host| + on host, "rm -rf \"%s\"" % get_test_file_path(host, '') + end + end + end + + def get_apply_opts(environment_hash = nil, acceptable_exit_codes = agent_exit_codes) + opts = { + :catch_failures => true, + :future_parser => FUTURE_PARSER, + :acceptable_exit_codes => agent_exit_codes, + } + opts.merge!(:environment => environment_hash) if environment_hash + opts + end + +end + diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/setup/install_puppet.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/setup/install_puppet.rb new file mode 100644 index 000000000..8ec9483d4 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/setup/install_puppet.rb @@ -0,0 +1,19 @@ +require 'beaker/puppet_install_helper' + +run_puppet_install_helper + +test_name "Installing Puppet Modules" do + proj_root = File.expand_path(File.join(File.dirname(__FILE__), '../..')) + hosts.each do |host| + if host['platform'] =~ /windows/ + on host, "mkdir -p #{host['distmoduledir']}/registry" + target = (on host, "echo #{host['distmoduledir']}/registry").raw_output.chomp + + %w(lib manifests metadata.json).each do |file| + scp_to host, "#{proj_root}/#{file}", target + end + on host, 'curl -k -o c:/puppetlabs-stdlib-4.6.0.tar.gz https://forgeapi.puppetlabs.com/v3/files/puppetlabs-stdlib-4.6.0.tar.gz' + on host, puppet('module install c:/puppetlabs-stdlib-4.6.0.tar.gz --force --ignore-dependencies'), {:acceptable_exit_codes => [0, 1]} + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_create_key.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_create_key.rb new file mode 100644 index 000000000..c9c9b9417 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_create_key.rb @@ -0,0 +1,161 @@ +require 'tempfile' +require 'pathname' +require 'systest/util/registry' +# Include our utility methods in the singleton class of the test case instance. +class << self + include Systest::Util::Registry +end + +test_name "Registry Key Management" + +# Generate a unique key name +keyname = "PuppetLabsTest_#{randomstring(8)}" +# This is the keypath we'll use for this entire test. We will actually create this key and delete it. +keypath = "HKLM\\Software\\Vendor\\#{keyname}" + +master_manifest_content = < "fact_phase: $fact_phase" } + +registry_key { 'HKLM\\Software\\Vendor': ensure => present } +case $fact_phase { + '2': { include phase2 } + '3': { include phase3 } + default: { include phase1 } +} +HERE + +# Setup keys to purge +phase1 = < present } + Registry_key { ensure => present } + Registry_value { ensure => present, data => 'Puppet Default Data' } + + registry_key { '#{keypath}': } + registry_key { '#{keypath}\\SubKey1': } + + if $architecture == 'x64' { + registry_key { '32:#{keypath}': } + registry_key { '32:#{keypath}\\SubKey1': } + } + + registry_key { '#{keypath}\\SubKeyToPurge': } + registry_value { '#{keypath}\\SubKeyToPurge\\Value1': } + registry_value { '#{keypath}\\SubKeyToPurge\\Value2': } + registry_value { '#{keypath}\\SubKeyToPurge\\Value3': } + + if $architecture == 'x64' { + registry_key { '32:#{keypath}\\SubKeyToPurge': } + registry_value { '32:#{keypath}\\SubKeyToPurge\\Value1': } + registry_value { '32:#{keypath}\\SubKeyToPurge\\Value2': } + registry_value { '32:#{keypath}\\SubKeyToPurge\\Value3': } + } +PHASE1 + +# Purge the keys in a subsequent run +phase2 = < present } + Registry_key { ensure => present, purge_values => true } + + registry_key { '#{keypath}\\SubKeyToPurge': } + if $architecture == 'x64' { + registry_key { '32:#{keypath}\\SubKeyToPurge': } + } +PHASE2 + +# Delete our keys +phase3 = < present } + Registry_key { ensure => absent } + + # These have relationships because autorequire break things when + # ensure is absent. REVISIT: Make this not a requirement. + # REVISIT: This appears to work with explicit relationships but not with -> + # notation. + registry_key { '#{keypath}\\SubKey1': } + registry_key { '#{keypath}\\SubKeyToPurge': } + registry_key { '#{keypath}': + require => Registry_key['#{keypath}\\SubKeyToPurge', '#{keypath}\\SubKey1'], + } + + if $architecture == 'x64' { + registry_key { '32:#{keypath}\\SubKey1': } + registry_key { '32:#{keypath}\\SubKeyToPurge': } + registry_key { '32:#{keypath}': + require => Registry_key['32:#{keypath}\\SubKeyToPurge', '32:#{keypath}\\SubKey1'], + } + } +PHASE3 + + +# Setup the master to use the modules specified in the --modules option + + +step "Start should_create_key test" do + # setup_master master_manifest_content + # with_puppet_running_on master, :__commandline_args__ => master_options do + # A set of keys we expect Puppet to create + keys_created_native = [ + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest\w+\].ensure: created/, + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\].ensure: created/ + ] + + keys_created_wow = [ + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest\w+\].ensure: created/, + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\].ensure: created/ + ] + + # A set of regular expression of values to be purged in phase 2. + values_purged_native = [ + /Registry_value\[hklm.Software.Vendor.PuppetLabsTest\w+.SubKeyToPurge.Value1\].ensure: removed/, + /Registry_value\[hklm.Software.Vendor.PuppetLabsTest\w+.SubKeyToPurge.Value2\].ensure: removed/, + /Registry_value\[hklm.Software.Vendor.PuppetLabsTest\w+.SubKeyToPurge.Value3\].ensure: removed/ + ] + + values_purged_wow = [ + /Registry_value\[32:hklm.Software.Vendor.PuppetLabsTest\w+.SubKeyToPurge.Value1\].ensure: removed/, + /Registry_value\[32:hklm.Software.Vendor.PuppetLabsTest\w+.SubKeyToPurge.Value2\].ensure: removed/, + /Registry_value\[32:hklm.Software.Vendor.PuppetLabsTest\w+.SubKeyToPurge.Value3\].ensure: removed/ + ] + + windows_agents.each do |agent| + is_x64 = x64?(agent) + + keys_created = keys_created_native + (is_x64 ? keys_created_wow : []) + values_purged = values_purged_native + (is_x64 ? values_purged_wow : []) + + # Do the first run and make sure the key gets created. + step "Registry - Phase 1.a - Create some keys" + apply_manifest_on(agent, phase1, get_apply_opts) do + keys_created.each do |key_re| + assert_match(key_re, result.stdout, + "Expected #{key_re.inspect} to match the output. (First Run)") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Registry - Phase 1.b - Make sure Puppet is idempotent" + # Do a second run and make sure the key isn't created a second time. + apply_manifest_on(agent, phase1, get_apply_opts) do + keys_created.each do |key_re| + assert_no_match(key_re, result.stdout, + "Expected #{key_re.inspect} NOT to match the output. (First Run)") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Registry - Phase 2 - Make sure purge_values works" + apply_manifest_on(agent, phase2, get_apply_opts({'FACTER_FACT_PHASE' => '2'})) do + values_purged.each do |val_re| + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Registry - Phase 3 - Clean up" + apply_manifest_on(agent, phase3, get_apply_opts({'FACTER_FACT_PHASE' => '3'})) do + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + end +end + +# clean_up diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_have_defined_type.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_have_defined_type.rb new file mode 100644 index 000000000..183cb0b2d --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_have_defined_type.rb @@ -0,0 +1,89 @@ +require 'pathname' +require 'systest/util/registry' +# Include our utility methods in the singleton class of the test case instance. +class << self + include Systest::Util::Registry +end + +test_name "registry::value defined type" + +# Generate a unique key name +keyname = "PuppetLabsTest_MixedCase_#{randomstring(8)}" +# This is the keypath we'll use for this entire test. We will actually create this key and delete it. +vendor_path = "HKLM\\Software\\Vendor" +keypath = "#{vendor_path}\\#{keyname}" + +manifest = < present } + + registry::value { 'Setting1': + key => '#{keypath}', + value => 'Setting1', + data => "fact_phase=${fact_phase}", + } + registry::value { 'Setting2': + key => '#{keypath}', + data => "fact_phase=${fact_phase}", + } + registry::value { 'Setting3': + key => '#{keypath}', + value => 'Setting3', + data => "fact_phase=${fact_phase}", + } + registry::value { 'Setting0': + key => '#{keypath}', + value => '(default)', + data => "fact_phase=${fact_phase}", + } +HERE + +step "Start testing should_have_defined_type" do + # A set of keys we expect Puppet to create + phase1_resources_created = [ + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest\w+\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\Setting1\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\Setting2\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\Setting3\].ensure: created/, + ] + + phase2_resources_changed = [ + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\\].data: data changed 'fact_phase=1' to 'fact_phase=2'/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\Setting1\].data: data changed 'fact_phase=1' to 'fact_phase=2'/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\Setting2\].data: data changed 'fact_phase=1' to 'fact_phase=2'/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest\w+\\Setting3\].data: data changed 'fact_phase=1' to 'fact_phase=2'/, + ] + windows_agents.each do |agent| + step "Phase 1.a - Create some values" + apply_manifest_on agent, manifest, get_apply_opts({'FACTER_FACT_PHASE' => '1'}, agent_exit_codes) do + phase1_resources_created.each do |val_re| + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Phase 1.b - Make sure Puppet is idempotent" + apply_manifest_on agent, manifest, get_apply_opts({'FACTER_FACT_PHASE' => '1'}, agent_exit_codes) do + phase1_resources_created.each do |val_re| + assert_no_match(val_re, result.stdout, "Expected output not to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Phase 2.a - Change some values" + apply_manifest_on agent, manifest, get_apply_opts({'FACTER_FACT_PHASE' => '2'}, agent_exit_codes) do + phase2_resources_changed.each do |val_re| + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Phase 2.b - Make sure Puppet is idempotent" + apply_manifest_on agent, manifest, get_apply_opts({'FACTER_FACT_PHASE' => '2'}, agent_exit_codes) do + (phase1_resources_created + phase2_resources_changed).each do |val_re| + assert_no_match(val_re, result.stdout, "Expected output not to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_manage_values.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_manage_values.rb new file mode 100644 index 000000000..d99faa381 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_manage_values.rb @@ -0,0 +1,323 @@ +require 'pathname' +require 'systest/util/registry' +# Include our utility methods in the singleton class of the test case instance. +class << self + include Systest::Util::Registry +end + +test_name "Registry Value Management" + +# Generate a unique key name +keyname = "PuppetLabsTest_Value_#{randomstring(8)}" +# This is the keypath we'll use for this entire test. We will actually create this key and delete it. +vendor_path = "HKLM\\Software\\Vendor" +keypath = "#{vendor_path}\\#{keyname}" + +def getManifest(keypath, vendor_path, phase) + manifest = < "fact_phase: #{phase}" } + registry_key { '#{vendor_path}': ensure => present } + if $architecture == 'x64' { + registry_key { '32:#{vendor_path}': ensure => present } + } + Registry_key { ensure => present } + registry_key { '#{keypath}': } + registry_key { '#{keypath}\\SubKey1': } + registry_key { '#{keypath}\\SubKey2': } + if $architecture == 'x64' { + registry_key { '32:#{keypath}': } + registry_key { '32:#{keypath}\\SubKey1': } + registry_key { '32:#{keypath}\\SubKey2': } + } + + # The Default Value + registry_value { '#{keypath}\\SubKey1\\\\': + data => "Default Data phase=#{phase}", + } + registry_value { '#{keypath}\\SubKey2\\\\': + type => array, + data => [ "Default Data L1 phase=#{phase}", "Default Data L2 phase=#{phase}" ], + } + + # String Values + registry_value { '#{keypath}\\SubKey1\\ValueString1': + data => "Should be a string phase=#{phase}", + } + registry_value { '#{keypath}\\SubKey1\\ValueString2': + type => string, + data => "Should be a string phase=#{phase}", + } + registry_value { '#{keypath}\\SubKey1\\ValueString3': + ensure => present, + type => string, + data => "Should be a string phase=#{phase}", + } + registry_value { '#{keypath}\\SubKey1\\ValueString4': + data => "Should be a string phase=#{phase}", + type => string, + ensure => present, + } + + if $architecture == 'x64' { + # String Values + registry_value { '32:#{keypath}\\SubKey1\\ValueString1': + data => "Should be a string phase=#{phase}", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueString2': + type => string, + data => "Should be a string phase=#{phase}", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueString3': + ensure => present, + type => string, + data => "Should be a string phase=#{phase}", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueString4': + data => "Should be a string phase=#{phase}", + type => string, + ensure => present, + } + } + + # Array Values + registry_value { '#{keypath}\\SubKey1\\ValueArray1': + type => array, + data => "Should be an array L1 phase=#{phase}", + } + registry_value { '#{keypath}\\SubKey1\\ValueArray2': + type => array, + data => [ "Should be an array L1 phase=#{phase}" ], + } + registry_value { '#{keypath}\\SubKey1\\ValueArray3': + type => array, + data => [ "Should be an array L1 phase=#{phase}", + "Should be an array L2 phase=#{phase}" ], + } + registry_value { '#{keypath}\\SubKey1\\ValueArray4': + ensure => present, + type => array, + data => [ "Should be an array L1 phase=#{phase}", + "Should be an array L2 phase=#{phase}" ], + } + registry_value { '#{keypath}\\SubKey1\\ValueArray5': + data => [ "Should be an array L1 phase=#{phase}", + "Should be an array L2 phase=#{phase}" ], + type => array, + ensure => present, + } + if $architecture == 'x64' { + registry_value { '32:#{keypath}\\SubKey1\\ValueArray1': + type => array, + data => "Should be an array L1 phase=#{phase}", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueArray2': + type => array, + data => [ "Should be an array L1 phase=#{phase}" ], + } + registry_value { '32:#{keypath}\\SubKey1\\ValueArray3': + type => array, + data => [ "Should be an array L1 phase=#{phase}", + "Should be an array L2 phase=#{phase}" ], + } + registry_value { '32:#{keypath}\\SubKey1\\ValueArray4': + ensure => present, + type => array, + data => [ "Should be an array L1 phase=#{phase}", + "Should be an array L2 phase=#{phase}" ], + } + registry_value { '32:#{keypath}\\SubKey1\\ValueArray5': + data => [ "Should be an array L1 phase=#{phase}", + "Should be an array L2 phase=#{phase}" ], + type => array, + ensure => present, + } + } + + # Expand Values + registry_value { '#{keypath}\\SubKey1\\ValueExpand1': + type => expand, + data => "%SystemRoot% - Should be a REG_EXPAND_SZ phase=#{phase}", + } + registry_value { '#{keypath}\\SubKey1\\ValueExpand2': + type => expand, + data => "%SystemRoot% - Should be a REG_EXPAND_SZ phase=#{phase}", + ensure => present, + } + if $architecture == 'x64' { + registry_value { '32:#{keypath}\\SubKey1\\ValueExpand1': + type => expand, + data => "%SystemRoot% - Should be a REG_EXPAND_SZ phase=#{phase}", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueExpand2': + type => expand, + data => "%SystemRoot% - Should be a REG_EXPAND_SZ phase=#{phase}", + ensure => present, + } + } + + # DWORD Values + registry_value { '#{keypath}\\SubKey1\\ValueDword1': + type => dword, + data => #{phase}, + } + if $architecture == 'x64' { + registry_value { '32:#{keypath}\\SubKey1\\ValueDword1': + type => dword, + data => #{phase}, + } + } + + # QWORD Values + registry_value { '#{keypath}\\SubKey1\\ValueQword1': + type => qword, + data => #{phase}, + } + if $architecture == 'x64' { + registry_value { '32:#{keypath}\\SubKey1\\ValueQword1': + type => qword, + data => #{phase}, + } + } + + # Binary Values + registry_value { '#{keypath}\\SubKey1\\ValueBinary1': + type => binary, + data => "#{phase}", + } + registry_value { '#{keypath}\\SubKey1\\ValueBinary2': + type => binary, + data => "DE AD BE EF CA F#{phase}" + } + if $architecture == 'x64' { + registry_value { '32:#{keypath}\\SubKey1\\ValueBinary1': + type => binary, + data => "0#{phase}", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueBinary2': + type => binary, + data => "DEAD BEEF CAF#{phase}" + } + } +P1 +end + +step "Start testing should_manage_values" do + windows_agents.each do |agent| + x64 = x64?(agent) + + # A set of keys we expect Puppet to create + phase1_resources_created = [ + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest\w+\].ensure: created/, + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\].ensure: created/, + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey2\].ensure: created/, + ] + + if x64 + phase1_resources_created += [ + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest\w+\].ensure: created/, + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\].ensure: created/, + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey2\].ensure: created/, + ] + end + + # A set of values we expect Puppet to change in Phase 2 + phase2_resources_changed = Array.new + + prefixes = [''] + prefixes << '32:' if x64 + + # This is just to save a whole bunch of copy / paste + prefixes.each do |prefix| + # We should have created 4 REG_SZ values + 1.upto(4).each do |idx| + phase1_resources_created << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueString#{idx}\].ensure: created/ + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueString#{idx}\].data: data changed 'Should be a string phase=1' to 'Should be a string phase=2'/ + end + # We should have created 5 REG_MULTI_SZ values + 1.upto(5).each do |idx| + phase1_resources_created << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueArray#{idx}\].ensure: created/ + end + + # The first two array items are an exception + 1.upto(2).each do |idx| + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueArray#{idx}\].data: data changed 'Should be an array L1 phase=1' to 'Should be an array L1 phase=2'/ + end + + # The rest of the array items are OK and have 2 "lines" each. + 3.upto(5).each do |idx| + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueArray#{idx}\].data: data changed 'Should be an array L1 phase=1,Should be an array L2 phase=1' to 'Should be an array L1 phase=2,Should be an array L2 phase=2'/ + end + + # We should have created 2 REG_EXPAND_SZ values + 1.upto(2).each do |idx| + phase1_resources_created << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueExpand#{idx}\].ensure: created/ + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueExpand#{idx}\].data: data changed '%SystemRoot% - Should be a REG_EXPAND_SZ phase=1' to '%SystemRoot% - Should be a REG_EXPAND_SZ phase=2'/ + end + # We should have created 1 qword + 1.upto(1).each do |idx| + phase1_resources_created << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueQword#{idx}\].ensure: created/ + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueQword#{idx}\].data: data changed '1' to '2'/ + end + # We should have created 1 dword + 1.upto(1).each do |idx| + phase1_resources_created << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueDword#{idx}\].ensure: created/ + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueDword#{idx}\].data: data changed '1' to '2'/ + end + # We should have created 2 binary values + 1.upto(2).each do |idx| + phase1_resources_created << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueBinary#{idx}\].ensure: created/ + end + # We have different data for the binary values + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueBinary1\].data: data changed '01' to '02'/ + phase2_resources_changed << /Registry_value\[#{prefix}HKLM.Software.Vendor.PuppetLabsTest\w+\\SubKey1\\ValueBinary2\].data: data changed 'de ad be ef ca f1' to 'de ad be ef ca f2'/ + end + + + step "Registry Values - Phase 1.a - Create some values" + apply_manifest_on agent, getManifest(keypath, vendor_path,'1'), get_apply_opts do + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + phase1_resources_created.each do |val_re| + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + end + + step "Registry Values - Phase 1.b - Make sure Puppet is idempotent" + apply_manifest_on agent, getManifest(keypath, vendor_path,'1'), get_apply_opts do + phase1_resources_created.each do |val_re| + assert_no_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Registry Values - Phase 2.a - Change some values" + apply_manifest_on agent, getManifest(keypath, vendor_path, '2'), get_apply_opts do + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + phase2_resources_changed.each do |val_re| + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + end + + step "Registry Values - Phase 2.b - Make sure Puppet is idempotent" + apply_manifest_on agent, getManifest(keypath, vendor_path,'2'), get_apply_opts do + phase2_resources_changed.each do |val_re| + assert_no_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + + step "Registry Values - Phase 3 - Check the default value (#14572)" + # (#14572) This test uses the 'native' version of reg.exe to read the + # default value of a registry key. It should contain the string shown in + # val_re. + dir = native_sysdir(agent) + if not dir + Log.warn("Cannot query 64-bit view of registry from 32-bit process, skipping") + else + on agent, "#{dir}/reg.exe query '#{keypath}\\Subkey1'" do + val_re = /\(Default\) REG_SZ Default Data phase=2/i + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + end + end +end + diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_tolerate_mixed_case.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_tolerate_mixed_case.rb new file mode 100644 index 000000000..c9f054bae --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/acceptance/tests/resource/registry/should_tolerate_mixed_case.rb @@ -0,0 +1,158 @@ +require 'pathname' +require 'systest/util/registry' +# Include our utility methods in the singleton class of the test case instance. +class << self + include Systest::Util::Registry +end + +test_name "Registry Value Management (Mixed Case)" + +# JJM - The whole purpose of this test is to exercise situations where the user +# specifies registry valies in a case-insensitive but case-preserving way. +# This has particular "gotchas" with regard to autorequire. +# +# Note how SUBKEY1 is capitalized in some resources but downcased in other +# resources. On windows these refer to the same thing, and case is preserved. +# In puppet, however, resource namevars are case sensisitve unless otherwise +# noted. + +# Generate a unique key name +keyname = "PuppetLabsTest_MixedCase_#{randomstring(8)}" +# This is the keypath we'll use for this entire test. We will actually create this key and delete it. +vendor_path = "HKLM\\Software\\Vendor" +keypath = "#{vendor_path}\\#{keyname}" + +phase1 = < "fact_phase: $fact_phase" } + + registry_key { '#{vendor_path}': ensure => present } + if $architecture == 'x64' { + registry_key { '32:#{vendor_path}': ensure => present } + } + + Registry_key { ensure => present } + registry_key { '#{keypath}': } + registry_key { '#{keypath}\\SUBKEY1': } + # NOTE THE DIFFERENCE IN CASE IN SubKey1 and SUBKEY1 above + registry_key { '#{keypath}\\SubKey1\\SUBKEY2': } + if $architecture == 'x64' { + registry_key { '32:#{keypath}': } + registry_key { '32:#{keypath}\\SUBKEY1': } + registry_key { '32:#{keypath}\\SubKey1\\SUBKEY2': } + } + + # The Default Value + # NOTE THE DIFFERENCE IN CASE IN SubKey1 and SUBKEY1 above + registry_value { '#{keypath}\\SubKey1\\\\': + data => "Default Data", + } + + # String Values + registry_value { '#{keypath}\\SubKey1\\ValueString1': + data => "Should be a string", + } + registry_value { '#{keypath}\\SubKey1\\ValueString2': + type => string, + data => "Should be a string", + } + registry_value { '#{keypath}\\SubKey1\\ValueString3': + ensure => present, + type => string, + data => "Should be a string", + } + registry_value { '#{keypath}\\SubKey1\\ValueString4': + data => "Should be a string", + type => string, + ensure => present, + } + registry_value { '#{keypath}\\SubKey1\\SubKey2\\ValueString1': + data => "Should be a string", + } + registry_value { '#{keypath}\\SubKey1\\SubKey2\\ValueString2': + type => string, + data => "Should be a string", + } + registry_value { '#{keypath}\\SubKey1\\SubKey2\\ValueString3': + ensure => present, + type => string, + data => "Should be a string", + } + registry_value { '#{keypath}\\SubKey1\\SubKey2\\ValueString4': + data => "Should be a string", + type => string, + ensure => present, + } + + # The Default Value + # NOTE THE DIFFERENCE IN CASE IN SubKey1 and SUBKEY1 above + if $architecture == 'x64' { + registry_value { '32:#{keypath}\\SubKey1\\\\': + data => "Default Data", + } + # String Values + registry_value { '32:#{keypath}\\SubKey1\\ValueString1': + data => "Should be a string", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueString2': + type => string, + data => "Should be a string", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueString3': + ensure => present, + type => string, + data => "Should be a string", + } + registry_value { '32:#{keypath}\\SubKey1\\ValueString4': + data => "Should be a string", + type => string, + ensure => present, + } + registry_value { '32:#{keypath}\\SubKey1\\SubKey2\\ValueString1': + data => "Should be a string", + } + registry_value { '32:#{keypath}\\SubKey1\\SubKey2\\ValueString2': + type => string, + data => "Should be a string", + } + registry_value { '32:#{keypath}\\SubKey1\\SubKey2\\ValueString3': + ensure => present, + type => string, + data => "Should be a string", + } + registry_value { '32:#{keypath}\\SubKey1\\SubKey2\\ValueString4': + data => "Should be a string", + type => string, + ensure => present, + } + } +HERE + +step "Start the master" do + # A set of keys we expect Puppet to create + phase1_resources_created = [ + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\].ensure: created/, + /Registry_key\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SUBKEY1\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SubKey1\\\W+.ensure: created.*$/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SubKey1\\ValueString1\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SubKey1\\ValueString2\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SubKey1\\ValueString3\].ensure: created/, + /Registry_value\[HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SubKey1\\ValueString4\].ensure: created/, + ] + windows_agents.each do |agent| + + if x64?(agent) + phase1_resources_created += [ + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\].ensure: created/, + /Registry_key\[32:HKLM.Software.Vendor.PuppetLabsTest_MixedCase_\w+\\SUBKEY1\].ensure: created/, + ] + end + + step "Registry Tolerate Mixed Case Values - Phase 1.a - Create some values" + apply_manifest_on agent, phase1, get_apply_opts({'FACTER_FACT_PHASE' => '1'}) do |result| + phase1_resources_created.each do |val_re| + assert_match(val_re, result.stdout, "Expected output to contain #{val_re.inspect}.") + end + assert_no_match(/err:/, result.stdout, "Expected no error messages.") + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/appveyor.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/appveyor.yml new file mode 100644 index 000000000..2b56e44bf --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/appveyor.yml @@ -0,0 +1,42 @@ +version: 1.1.x.{build} +skip_commits: + message: /^\(?doc\)?.*/ +clone_depth: 10 +init: +- SET +- 'mkdir C:\ProgramData\PuppetLabs\code && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\facter && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\hiera && exit 0' +- 'mkdir C:\ProgramData\PuppetLabs\puppet\var && exit 0' +environment: + matrix: + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 21 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 21-x64 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 23 + - PUPPET_GEM_VERSION: ~> 4.0 + RUBY_VER: 23-x64 + - PUPPET_GEM_VERSION: 4.2.3 + RUBY_VER: 21-x64 + - PUPPET_GEM_VERSION: 4.2.3 + RUBY_VER: 21-x64 +matrix: + fast_finish: true +install: +- SET PATH=C:\Ruby%RUBY_VER%\bin;%PATH% +- bundle install --jobs 4 --retry 2 --without system_tests +- type Gemfile.lock +build: off +test_script: +- bundle exec puppet -V +- ruby -v +- bundle exec rspec spec/unit -fd -b +notifications: +- provider: Email + to: + - nobody@nowhere.com + on_build_success: false + on_build_failure: false + on_build_status_changed: false diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/checksums.json b/modules/utilities/windows/registry/puppetlabs_registry_library/checksums.json new file mode 100644 index 000000000..0163c7c62 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/checksums.json @@ -0,0 +1,52 @@ +{ + "CHANGELOG.md": "05c84205bd442d24aafa3bfbce36bc61", + "CONTRIBUTING.md": "77d0440d7cd4206497f99065c60bed46", + "Gemfile": "684d107a16e673f1c793f91325229112", + "LICENSE": "3b83ef96387f14655fc854ddc3c6bd57", + "MAINTAINERS.md": "761ee685b9d6edf562e6b9af2dcec8aa", + "NOTICE": "b3b228a5c87f40c082b72477fe6fceca", + "README.markdown": "d1dc9c6628a8628a39c5b5da16b9c26c", + "Rakefile": "ffc248460ba5c68e8219ac2fc572b940", + "acceptance/config/masterless-windows-2008-x86_64.cfg": "2374d99e46fd225ae577449643f0f402", + "acceptance/config/masterless-windows-2008r2-x86_64.cfg": "d39479dcf34753cb71f3d737938ac6f5", + "acceptance/config/masterless-windows-2012-x86_64.cfg": "6d76b8c217a0e11db888c20ecc025393", + "acceptance/config/masterless-windows-2012r2-x86_64.cfg": "e8315390a155f8a1ce6b4ec231f2f0e4", + "acceptance/config/windows-2003r2-i386.cfg": "f97a32f5352765601954a423c60ae09e", + "acceptance/config/windows-2003r2-x86_64.cfg": "22305912f5b5d397676676ebec03b1d1", + "acceptance/config/windows-2008r2-x86_64.cfg": "7663d1d682f54c195f26d17e72a0d900", + "acceptance/config/windows-2012-x86_64.cfg": "1c30068e296370d06157f3995e8acd61", + "acceptance/lib/systest/util/registry.rb": "3066ce6a31b989638d23dd86a766621d", + "acceptance/lib/systest/util.rb": "5fe316915896e133129bec03cfeb6f3c", + "acceptance/lib/systest.rb": "08cde457e07db7b82a1ce9ff502dd495", + "acceptance/setup/install_puppet.rb": "ca5f676e62a616a095e54891146cfbb7", + "acceptance/tests/resource/registry/should_create_key.rb": "277bc3e05e3acc271589df726b598941", + "acceptance/tests/resource/registry/should_have_defined_type.rb": "20fcafaf9188994d72d5dc5a56eba19a", + "acceptance/tests/resource/registry/should_manage_values.rb": "57fe966179f46188f4c8c0e96ef00af1", + "acceptance/tests/resource/registry/should_tolerate_mixed_case.rb": "23ad59a08e7f5b78b6f75dbef66f2011", + "appveyor.yml": "179cf97e405a1a2872f349c18922614f", + "examples/compliance_example.pp": "adb9ac7990857d5ec4fe046ffe7ee3ad", + "examples/purge_example.pp": "09152400a320825852cd9d496041c924", + "examples/registry_examples.pp": "28fe1b04385d91f301880920c7f343c0", + "examples/service_example.pp": "a19aa627ae1aefd684ff02473459b58a", + "lib/puppet/provider/registry_key/registry.rb": "5c26c6cbb1669a01361e69fadbbb408d", + "lib/puppet/provider/registry_value/registry.rb": "92b54f65f5be3c130681cbb04b216e09", + "lib/puppet/type/registry_key.rb": "bcf74b3a991cafdae54514b3c3c4a38c", + "lib/puppet/type/registry_value.rb": "140295468b773a7ad709a532e496005c", + "lib/puppet_x/puppetlabs/registry/provider_base.rb": "76fb5dc01fdbf0358719dfa64ce778f9", + "lib/puppet_x/puppetlabs/registry.rb": "8fc2b92a8362c6e5a95951cb36b02103", + "manifests/service.pp": "9028f47d1fa41da7f6bca05761f7303f", + "manifests/value.pp": "230398fc31dacc5aa5ffc0fb1696a888", + "metadata.json": "113bc4c833d15867250364ba63d13ba5", + "spec/acceptance/nodesets/centos-7-x64.yml": "a713f3abd3657f0ae2878829badd23cd", + "spec/acceptance/nodesets/debian-8-x64.yml": "d2d2977900989f30086ad251a14a1f39", + "spec/acceptance/nodesets/default.yml": "b42da5a1ea0c964567ba7495574b8808", + "spec/acceptance/nodesets/docker/centos-7.yml": "8a3892807bdd62306ae4774f41ba11ae", + "spec/acceptance/nodesets/docker/debian-8.yml": "ac8e871d1068c96de5e85a89daaec6df", + "spec/acceptance/nodesets/docker/ubuntu-14.04.yml": "dc42ee922a96908d85b8f0f08203ce58", + "spec/spec_helper.rb": "1786dac4fb79434afde64a0da40d415e", + "spec/unit/puppet/provider/registry_key_spec.rb": "3360361b6d5599933f9ca8d06524fa52", + "spec/unit/puppet/provider/registry_value_spec.rb": "86f6c1e160ae09682cf9cdc27d33b046", + "spec/unit/puppet/type/registry_key_spec.rb": "6b6c30fa62ac774407eb1c274e5c86a4", + "spec/unit/puppet/type/registry_value_spec.rb": "acca25c1598cb08bcfdd6e1d00f420b8", + "spec/watchr.rb": "0d23eac3b37babe4229307850cfc4240" +} \ No newline at end of file diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/examples/compliance_example.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/compliance_example.pp new file mode 100644 index 000000000..a03c37c56 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/compliance_example.pp @@ -0,0 +1,111 @@ +# = Class: registry::compliance_example +# +# This class provides an example of how to use the audit metaparameter to +# inspect registry_key and registry_value resources with the Compliance +# feature of Puppet Enterprise. +# +# = Parameters +# +# = Actions +# +# = Requires +# +# = Sample Usage +# +# include registry::compliance_example +# +# (MARKUP: http://links.puppetlabs.com/puppet_manifest_documentation) +class registry::compliance_example { + $key_path = 'HKLM\Software\Vendor\Puppet Labs\Examples\Compliance' + + case $::registry_compliance_example_mode { + audit: { + $mode = 'audit' + } + default: { + $mode = 'setup' + notify { 'compliance_example_mode_info': + message => 'Switch to audit mode using + \$env:FACTER_REGISTRY_COMPLIANCE_EXAMPLE_MODE = \'audit\'', + before => Notify['compliance_example_mode'] + } + } + } + + notify { 'compliance_example_mode': + message => "Registry compliance example mode: ${mode}", + } + + # Resource Defaults + Registry_key { + ensure => $mode ? { + setup => present, + default => undef + }, + purge_values => $mode ? { + setup => true, + default => false, + }, + } + Registry_value { + ensure => $mode ? { + setup => present, + default => undef, + }, + type => $mode ? { + setup => string, + default => undef, + }, + data => $mode ? { + setup => 'Puppet Default Data', + default => undef, + }, + audit => $mode ? { + setup => undef, + default => all, + }, + } + + # Create the nested key structure we want to audit. The resource defaults + # will determine the properties managed or audited. + registry_key { $key_path: } + registry_key { "${key_path}\\SubKeyA": } + registry_key { "${key_path}\\SubKeyA\\SubKeyA1": } + registry_key { "${key_path}\\SubKeyA\\SubKeyA2": } + registry_key { "${key_path}\\SubKeyB": } + registry_key { "${key_path}\\SubKeyB\\SubKeyB1": } + registry_key { "${key_path}\\SubKeyB\\SubKeyB2": } + registry_key { "${key_path}\\SubKeyC": } + registry_key { "${key_path}\\SubKeyC\\SubKeyC1": } + registry_key { "${key_path}\\SubKeyC\\SubKeyC2": } + registry_value { "${key_path}\\Value1": } + registry_value { "${key_path}\\Value2": } + registry_value { "${key_path}\\Value3": } + registry_value { "${key_path}\\SubKeyA\\ValueA1": } + registry_value { "${key_path}\\SubKeyA\\ValueA2": } + registry_value { "${key_path}\\SubKeyA\\ValueA3": } + registry_value { "${key_path}\\SubKeyB\\ValueB1": } + registry_value { "${key_path}\\SubKeyB\\ValueB2": } + registry_value { "${key_path}\\SubKeyB\\ValueB3": } + registry_value { "${key_path}\\SubKeyC\\ValueC1": } + registry_value { "${key_path}\\SubKeyC\\ValueC2": } + registry_value { "${key_path}\\SubKeyC\\ValueC3": } + registry_value { "${key_path}\\SubKeyA\\SubKeyA1\\ValueA1X": } + registry_value { "${key_path}\\SubKeyA\\SubKeyA1\\ValueA1Y": } + registry_value { "${key_path}\\SubKeyA\\SubKeyA1\\ValueA1Z": } + registry_value { "${key_path}\\SubKeyA\\SubKeyA2\\ValueA2X": } + registry_value { "${key_path}\\SubKeyA\\SubKeyA2\\ValueA2Y": } + registry_value { "${key_path}\\SubKeyA\\SubKeyA2\\ValueA2Z": } + registry_value { "${key_path}\\SubKeyB\\SubKeyB1\\ValueB1X": } + registry_value { "${key_path}\\SubKeyB\\SubKeyB1\\ValueB1Y": } + registry_value { "${key_path}\\SubKeyB\\SubKeyB1\\ValueB1Z": } + registry_value { "${key_path}\\SubKeyB\\SubKeyB2\\ValueB2X": } + registry_value { "${key_path}\\SubKeyB\\SubKeyB2\\ValueB2Y": } + registry_value { "${key_path}\\SubKeyB\\SubKeyB2\\ValueB2Z": } + registry_value { "${key_path}\\SubKeyC\\SubKeyC1\\ValueC1X": } + registry_value { "${key_path}\\SubKeyC\\SubKeyC1\\ValueC1Y": } + registry_value { "${key_path}\\SubKeyC\\SubKeyC1\\ValueC1Z": } + registry_value { "${key_path}\\SubKeyC\\SubKeyC2\\ValueC2X": } + registry_value { "${key_path}\\SubKeyC\\SubKeyC2\\ValueC2Y": } + registry_value { "${key_path}\\SubKeyC\\SubKeyC2\\ValueC2Z": } +} diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/examples/purge_example.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/purge_example.pp new file mode 100644 index 000000000..48e4783f7 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/purge_example.pp @@ -0,0 +1,119 @@ +# = Class: registry::purge_example +# +# This class provides an example of how to purge registry values associated +# with a specific key. +# +# This class has two modes of operation determined by the Facter fact +# PURGE_EXAMPLE_MODE The value of this fact can be either 'setup' or 'purge' +# +# The easiest way to set this mode is to set an +# environment variable in Power Shell: +# +# The setup mode creates a registry key and 6 values. +# +# `$env:FACTER_PURGE_EXAMPLE_MODE = "setup"` +# `puppet agent --test` +# +# The purge mode manages the key with purge_values => true and manages only 3 +# of the 6 values. The other 3 values will be automatically purged. +# +# `$env:FACTER_PURGE_EXAMPLE_MODE = "purge"` +# `puppet agent --test` +# +# = Parameters +# +# = Actions +# +# = Requires +# +# = Sample Usage +# +# include registry::purge_example +# +# (MARKUP: http://links.puppetlabs.com/puppet_manifest_documentation) +class registry::purge_example { + + $key_path = 'HKLM\Software\Vendor\Puppet Labs\Examples\KeyPurge' + + case $::purge_example_mode { + setup: { + registry_key { $key_path: + ensure => present, + purge_values => false, + } + registry_key { "${key_path}\\SubKey": + ensure => present, + purge_values => false, + } + registry_value { "${key_path}\\SubKey\\Value1": + ensure => present, + type => dword, + data => 1, + } + registry_value { "${key_path}\\SubKey\\Value2": + ensure => present, + type => dword, + data => 1, + } + registry_value { "${key_path}\\Value1": + ensure => present, + type => dword, + data => 1, + } + registry_value { "${key_path}\\Value2": + ensure => present, + type => dword, + data => 2, + } + registry_value { "${key_path}\\Value3": + ensure => present, + type => string, + data => 'key3', + } + registry_value { "${key_path}\\Value4": + ensure => present, + type => array, + data => [ 'one', 'two', 'three' ], + } + registry_value { "${key_path}\\Value5": + ensure => present, + type => expand, + data => '%SystemRoot%\system32', + } + registry_value { "${key_path}\\Value6": + ensure => present, + type => binary, + data => '01AB CDEF', + } + } + purge: { + registry_key { $key_path: + ensure => present, + purge_values => true, + } + registry_value { "${key_path}\\Value1": + ensure => present, + type => dword, + data => 0, + } + registry_value { "${key_path}\\Value2": + ensure => present, + type => dword, + data => 0, + } + registry_value { "${key_path}\\Value3": + ensure => present, + type => string, + data => 'should not be purged', + } + } + default: { + notify { 'purge_example_notice': + message => 'The purge_example_mode fact is not set. To try this + example class first set \$env:FACTER_PURGE_EXAMPLE_MODE = \'setup\' then + run puppet agent, then set \$env:FACTER_PURGE_EXAMPLE_MODE = \'purge\' + and run puppet agent again to see the values purged.', + } + } + } +} diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/examples/registry_examples.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/registry_examples.pp new file mode 100644 index 000000000..3f4496854 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/registry_examples.pp @@ -0,0 +1,87 @@ +# = Class: registry_example +# +# This is an example of how to manage registry keys and values. +# +# = Parameters +# +# = Actions +# +# = Requires +# +# = Sample Usage +# +# include registry_example +# +# (MARKUP: http://links.puppetlabs.com/puppet_manifest_documentation) +class registry_example { + registry_key { 'HKLM\Software\Vendor': + ensure => present, + } + + # This should trigger a duplicate resource with HKLM + # registry_key { 'HKEY_LOCAL_MACHINE\Software\Vendor': + # ensure => present, + # } + + registry_key { 'HKLM\Software\Vendor\Bar': + ensure => present, + } + + registry_value { 'HKLM\Software\Vendor\Bar\valuedword2': + ensure => present, + type => dword, + data => 0xFFFFFFFF, + } + + registry_value { 'HKLM\Software\Vendor\Bar\valueqword1': + ensure => present, + type => qword, + data => 100, + } + + registry_value { 'HKLM\Software\Vendor\Bar\valuedstring1': + ensure => present, + type => string, + data => 'this is a string', + } + + registry_value { 'HKLM\Software\Vendor\Bar\valuedexpand1': + ensure => present, + type => expand, + data => '%windir%\system32', + } + + registry_value { 'HKLM\Software\Vendor\Bar\valuedbinary1': + ensure => present, + type => binary, + data => 'DE AD BE EF', + } + + registry_value { 'HKLM\Software\Vendor\Bar\valuedbinary2': + ensure => present, + type => binary, + data => 'CAFEBEEF', + } + + registry_value { 'HKLM\Software\Vendor\Bar\valuearray1': + ensure => present, + type => array, + data => [ 'one', 'two', 'three' ], + } + + $some_string = "somestring" + registry_value { 'HKLM\Software\Vendor\Bar\valuearray2': + ensure => present, + type => array, + data => [ 0, 'zero', '0', 123456, 'two', $some_string ], + } + + $some_array = [ "array1", "array2", "array3" ] + registry_value { 'HKLM\Software\Vendor\Bar\valuearray3': + ensure => present, + type => array, + data => $some_array, + } +} + +include registry_example diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/examples/service_example.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/service_example.pp new file mode 100644 index 000000000..60a70e188 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/examples/service_example.pp @@ -0,0 +1,36 @@ +# = Class: registry::service_example +# +# This is an example of how to use the registry::service defined resource +# type included in this module. +# +# = Parameters +# +# = Actions +# +# = Requires +# +# = Sample Usage +# +# include registry::service_example +# +# +# (MARKUP: http://links.puppetlabs.com/puppet_manifest_documentation) +class registry::service_example { + # Define a new service named "Puppet Test" that is disabled. + registry::service { 'PuppetExample1': + display_name => 'Puppet Example 1', + description => + 'This is a simple example managing the + registry entries for a Windows Service', + command => 'C:\PuppetExample1.bat', + start => 'disabled', + } + registry::service { 'PuppetExample2': + display_name => 'Puppet Example 2', + description => + 'This is a simple example + managing the registry entries for a Windows Service', + command => 'C:\PuppetExample2.bat', + start => 'disabled', + } +} diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_key/registry.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_key/registry.rb new file mode 100644 index 000000000..ea49a02e3 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_key/registry.rb @@ -0,0 +1,65 @@ +# REMIND: need to support recursive delete of subkeys & values +begin + # We expect this to work once Puppet supports Rubygems in #7788 + require "puppet_x/puppetlabs/registry" + require "puppet_x/puppetlabs/registry/provider_base" +rescue LoadError => detail + # Work around #7788 (Rubygems support for modules) + require 'pathname' # JJM WORK_AROUND #14073 + module_base = Pathname.new(__FILE__).dirname + require module_base + "../../../" + "puppet_x/puppetlabs/registry" + require module_base + "../../../" + "puppet_x/puppetlabs/registry/provider_base" +end + +Puppet::Type.type(:registry_key).provide(:registry) do + include PuppetX::Puppetlabs::Registry::ProviderBase + + defaultfor :operatingsystem => :windows + confine :operatingsystem => :windows + + def self.instances + hkeys.keys.collect do |hkey| + new(:provider => :registry, :name => "#{hkey.to_s}") + end + end + + def create + Puppet.debug("Creating registry key #{self}") + hive.create(subkey, Win32::Registry::KEY_ALL_ACCESS | access) {|reg| true } + end + + def exists? + Puppet.debug("Checking existence of registry key #{self}") + !!hive.open(subkey, Win32::Registry::KEY_READ | access) {|reg| true } rescue false + end + + def destroy + Puppet.debug("Destroying registry key #{self}") + + raise ArgumentError, "Cannot delete root key: #{path}" unless subkey + + from_string_to_wide_string(subkey) do |subkey_ptr| + # hive.hkey returns an integer value that's like a FD + if RegDeleteKeyExW(hive.hkey, subkey_ptr, access, 0) != 0 + raise "Failed to delete registry key: #{self}" + end + end + end + + def values + names = [] + # Only try and get the values for this key if the key itself exists. + if exists? then + hive.open(subkey, Win32::Registry::KEY_READ | access) do |reg| + each_value(reg) do |name, type, data| names << name end + end + end + names + end + + private + + def path + @path ||= PuppetX::Puppetlabs::Registry::RegistryKeyPath.new(resource.parameter(:path).value) + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_value/registry.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_value/registry.rb new file mode 100644 index 000000000..0b6897cb6 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/provider/registry_value/registry.rb @@ -0,0 +1,219 @@ +require 'puppet/type' +begin + require "puppet_x/puppetlabs/registry" + require "puppet_x/puppetlabs/registry/provider_base" +rescue LoadError => detail + require "pathname" # JJM WORK_AROUND #14073 and #7788 + module_base = Pathname.new(__FILE__).dirname + "../../../" + require module_base + "puppet_x/puppetlabs/registry" + require module_base + "puppet_x/puppetlabs/registry/provider_base" +end + +Puppet::Type.type(:registry_value).provide(:registry) do + include PuppetX::Puppetlabs::Registry::ProviderBase + + defaultfor :operatingsystem => :windows + confine :operatingsystem => :windows + + def self.instances + [] + end + + def exists? + Puppet.debug("Checking the existence of registry value: #{self}") + found = false + begin + hive.open(subkey, Win32::Registry::KEY_READ | access) do |reg| + from_string_to_wide_string(valuename) do |valuename_ptr| + status = RegQueryValueExW(reg.hkey, valuename_ptr, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL) + + found = status == 0 + raise Win32::Registry::Error.new(status) if !found + end + end + rescue Win32::Registry::Error => detail + case detail.code + when 2 + # Code 2 is the error message for "The system cannot find the file specified." + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382.aspx + found = false + else + error = Puppet::Error.new("Unexpected exception from Win32 API. detail: (#{detail.message}) ERROR CODE: #{detail.code}. Puppet Error ID: D4B679E4-0E22-48D5-80EF-96AAEC0282B9") + error.set_backtrace detail.backtrace + raise error + end + end + found + end + + def create + Puppet.debug("Creating registry value: #{self}") + write_value + end + + def flush + # REVISIT - This concept of flush seems different than package provider's + # concept of flush. + Puppet.debug("Flushing registry value: #{self}") + return if resource[:ensure] == :absent + write_value + end + + def destroy + Puppet.debug("Destroying registry value: #{self}") + # On Ruby 2.1.x, due to https://bugs.ruby-lang.org/issues/10820, we see + # a FileNotFound error - hence an FFI re-implementation inside destroy + hive.open(subkey, Win32::Registry::KEY_ALL_ACCESS | access) do |reg| + from_string_to_wide_string(valuename) do |valuename_ptr| + if RegDeleteValueW(reg.hkey, valuename_ptr) != 0 + msg = "Failed to delete registry value #{valuename} at #{reg.keyname}" + raise Puppet::Util::Windows::Error.new(msg) + end + end + end + end + + def type + regvalue[:type] || :absent + end + + def type=(value) + regvalue[:type] = value + end + + def data + regvalue[:data] || :absent + end + + def data=(value) + regvalue[:data] = value + end + + def regvalue + unless @regvalue + @regvalue = {} + hive.open(subkey, Win32::Registry::KEY_READ | access) do |reg| + from_string_to_wide_string(valuename) do |valuename_ptr| + if RegQueryValueExW(reg.hkey, valuename_ptr, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL, + FFI::MemoryPointer::NULL, FFI::MemoryPointer::NULL) == 0 + @regvalue[:type], @regvalue[:data] = from_native(reg.read(valuename)) + end + end + end + end + @regvalue + end + + # convert puppet type and data to native + def to_native(ptype, pdata) + # JJM Because the data property is set to :array_matching => :all we + # should always get an array from Puppet. We need to convert this + # array to something usable by the Win API. + raise Puppet::Error, "Data should be an Array (ErrorID 37D9BBAB-52E8-4A7C-9F2E-D7BF16A59050)" unless pdata.kind_of?(Array) + ndata = + case ptype + when :binary + pdata.first.scan(/[a-f\d]{2}/i).map{ |byte| [byte].pack('H2') }.join('') + when :array + # We already have an array, and the native API write method takes an + # array, so send it thru. + pdata + else + # Since we have an array, take the first element and send it to the + # native API which is expecting a scalar. + pdata.first + end + + return [name2type(ptype), ndata] + end + + # convert from native type and data to puppet + def from_native(ary) + ntype, ndata = ary + + pdata = + case type2name(ntype) + when :binary + ndata.bytes.map{ |byte| "%02x" % byte }.join(' ') + when :array + # We get the data from the registry in Array form. + ndata + else + ndata + end + + # JJM Since the data property is set to :array_matching => all we should + # always give an array to Puppet. This is why we have the ternary operator + # I'm not calling .to_a because Ruby issues a warning about the default + # implementation of to_a going away in the future. + return [type2name(ntype), pdata.kind_of?(Array) ? pdata : [pdata]] + end + + private + + def write_value + begin + hive.open(subkey, Win32::Registry::KEY_ALL_ACCESS | access) do |reg| + ary = to_native(resource[:type], resource[:data]) + write(reg, valuename, ary[0], ary[1]) + end + rescue Win32::Registry::Error => detail + error = case detail.code + when 2 + # Code 2 is the error message for "The system cannot find the file specified." + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382.aspx + Puppet::Error.new("Cannot write to the registry. The parent key does not exist. detail: (#{detail.message}) Puppet Error ID: AC99C7C6-98D6-4E91-A75E-970F4064BF95") + else + Puppet::Error.new("Unexpected exception from Win32 API. detail: (#{detail.message}). ERROR CODE: #{detail.code}. Puppet Error ID: F46C6AE2-C711-48F9-86D6-5D50E1988E48") + end + error.set_backtrace detail.backtrace + raise error + end + end + + def data_to_bytes(type, data) + bytes = [] + + case type + when Win32::Registry::REG_SZ, Win32::Registry::REG_EXPAND_SZ + bytes = wide_string(data).bytes.to_a + when Win32::Registry::REG_MULTI_SZ + # each wide string is already NULL terminated + bytes = data.map { |s| wide_string(s).bytes.to_a }.flat_map { |a| a } + # requires an additional NULL terminator to terminate properly + bytes << 0 << 0 + when Win32::Registry::REG_BINARY + bytes = data.bytes.to_a + when Win32::Registry::REG_DWORD + # L is 32-bit unsigned native (little) endian order + bytes = [data].pack('L').unpack('C*') + when Win32::Registry::REG_QWORD + # Q is 64-bit unsigned native (little) endian order + bytes = [data].pack('Q').unpack('C*') + else + raise TypeError, "Unsupported type #{type}" + end + + bytes + end + + def write(reg, name, type, data) + from_string_to_wide_string(valuename) do |name_ptr| + bytes = data_to_bytes(type, data) + FFI::MemoryPointer.new(:uchar, bytes.length) do |data_ptr| + data_ptr.write_array_of_uchar(bytes) + if RegSetValueExW(reg.hkey, name_ptr, 0, + type, data_ptr, data_ptr.size) != 0 + raise Puppet::Util::Windows::Error.new("Failed to write registry value") + end + end + end + end + + def path + @path ||= PuppetX::Puppetlabs::Registry::RegistryValuePath.new(resource.parameter(:path).value) + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_key.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_key.rb new file mode 100644 index 000000000..8ce7f9e5f --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_key.rb @@ -0,0 +1,119 @@ +require 'puppet/type' +begin + require "puppet_x/puppetlabs/registry" +rescue LoadError => detail + require 'pathname' # JJM WORK_AROUND #14073 and #7788 + require Pathname.new(__FILE__).dirname + "../../" + "puppet_x/puppetlabs/registry" +end + +Puppet::Type.newtype(:registry_key) do + @doc = <<-EOT + Manages registry keys on Windows systems. + + Keys within HKEY_LOCAL_MACHINE (hklm) or HKEY_CLASSES_ROOT (hkcr) are + supported. Other predefined root keys, e.g. HKEY_USERS, are not + currently supported. + + If Puppet creates a registry key, Windows will automatically create any + necessary parent registry keys that do not exist. + + Puppet will not recursively delete registry keys. + + **Autorequires:** Any parent registry key managed by Puppet will be + autorequired. +EOT + + def self.title_patterns + [ [ /^(.*?)\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] + end + + ensurable + + newparam(:path, :namevar => true) do + desc "The path to the registry key to manage. For example; 'HKLM\Software', + 'HKEY_LOCAL_MACHINE\Software\Vendor'. If Puppet is running on a 64-bit + system, the 32-bit registry key can be explicitly managed using a + prefix. For example: '32:HKLM\Software'" + + validate do |path| + PuppetX::Puppetlabs::Registry::RegistryKeyPath.new(path).valid? + end + munge do |path| + reg_path = PuppetX::Puppetlabs::Registry::RegistryKeyPath.new(path) + # Windows is case insensitive and case preserving. We deal with this by + # aliasing resources to their downcase values. This is inspired by the + # munge block in the alias metaparameter. + if @resource.catalog + reg_path.aliases.each do |alt_name| + @resource.catalog.alias(@resource, alt_name) + end + else + Puppet.debug "Resource has no associated catalog. Aliases are not being set for #{@resource.to_s}" + end + reg_path.canonical + end + end + + # REVISIT - Make a common parameter for boolean munging and validation. This will be used + # By both registry_key and registry_value types. + newparam(:purge_values, :boolean => true) do + desc "Whether to delete any registry value associated with this key that is + not being managed by puppet." + + newvalues(:true, :false) + defaultto false + + validate do |value| + case value + when true, /^true$/i, :true, false, /^false$/i, :false, :undef, nil + true + else + # We raise an ArgumentError and not a Puppet::Error so we get manifest + # and line numbers in the error message displayed to the user. + raise ArgumentError.new("Validation Error: purge_values must be true or false, not #{value}") + end + end + + munge do |value| + case value + when true, /^true$/i, :true + true + else + false + end + end + end + + # Autorequire the nearest ancestor registry_key found in the catalog. + autorequire(:registry_key) do + req = [] + path = PuppetX::Puppetlabs::Registry::RegistryKeyPath.new(value(:path)) + # It is important to match against the downcase value of the path because + # other resources are expected to alias themselves to the downcase value so + # that we respect the case insensitive and preserving nature of Windows. + if found = path.enum_for(:ascend).find { |p| catalog.resource(:registry_key, p.to_s.downcase) } + req << found.to_s.downcase + end + req + end + + def eval_generate + # This value will be given post-munge so we can assume it will be a ruby true or false object + return [] unless value(:purge_values) + + # get the "should" names of registry values associated with this key + should_values = catalog.relationship_graph.direct_dependents_of(self).select {|dep| dep.type == :registry_value }.map do |reg| + PuppetX::Puppetlabs::Registry::RegistryValuePath.new(reg.parameter(:path).value).valuename + end + + # get the "is" names of registry values associated with this key + is_values = provider.values + + # create absent registry_value resources for the complement + resources = [] + (is_values - should_values).each do |name| + resources << Puppet::Type.type(:registry_value).new(:path => "#{self[:path]}\\#{name}", :ensure => :absent, :catalog => catalog) + end + resources + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_value.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_value.rb new file mode 100644 index 000000000..556cba3dc --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet/type/registry_value.rb @@ -0,0 +1,138 @@ +require 'puppet/type' +begin + require "puppet_x/puppetlabs/registry" +rescue LoadError => detail + require 'pathname' # JJM WORK_AROUND #14073 and #7788 + require Pathname.new(__FILE__).dirname + "../../" + "puppet_x/puppetlabs/registry" +end + +Puppet::Type.newtype(:registry_value) do + @doc = <<-EOT + Manages registry values on Windows systems. + + The `registry_value` type can manage registry values. See the + `type` and `data` attributes for information about supported + registry types, e.g. REG_SZ, and how the data should be specified. + + **Autorequires:** Any parent registry key managed by Puppet will be + autorequired. + EOT + + def self.title_patterns + [[/^(.*?)\Z/m, [[:path, lambda { |x| x }]]]] + end + + ensurable + + newparam(:path, :namevar => true) do + desc "The path to the registry value to manage. For example: + 'HKLM\Software\Value1', 'HKEY_LOCAL_MACHINE\Software\Vendor\Value2'. + If Puppet is running on a 64-bit system, the 32-bit registry key can + be explicitly manage using a prefix. For example: + '32:HKLM\Software\Value3'" + + validate do |path| + PuppetX::Puppetlabs::Registry::RegistryValuePath.new(path).valid? + end + munge do |path| + reg_path = PuppetX::Puppetlabs::Registry::RegistryValuePath.new(path) + # Windows is case insensitive and case preserving. We deal with this by + # aliasing resources to their downcase values. This is inspired by the + # munge block in the alias metaparameter. + if @resource.catalog + reg_path.aliases.each do |alt_name| + @resource.catalog.alias(@resource, alt_name) + end + else + Puppet.debug "Resource has no associated catalog. Aliases are not being set for #{@resource.to_s}" + end + reg_path.canonical + end + end + + newproperty(:type) do + desc "The Windows data type of the registry value. Puppet provides + helpful names for these types as follows: + + * string => REG_SZ + * array => REG_MULTI_SZ + * expand => REG_EXPAND_SZ + * dword => REG_DWORD + * qword => REG_QWORD + * binary => REG_BINARY + + " + newvalues(:string, :array, :dword, :qword, :binary, :expand) + defaultto :string + end + + newproperty(:data, :array_matching => :all) do + desc "The data stored in the registry value. Data should be specified + as a string value but may be specified as a Puppet array when the + type is set to `array`." + + defaultto '' + + munge do |value| + case resource[:type] + when :dword + val = Integer(value) rescue nil + fail("The data must be a valid DWORD: #{value}") unless val and (val.abs >> 32) <= 0 + val + when :qword + val = Integer(value) rescue nil + fail("The data must be a valid QWORD: #{value}") unless val and (val.abs >> 64) <= 0 + val + when :binary + if (value.respond_to?(:length) && value.length == 1) || (value.kind_of?(Integer) && value <= 9) + value = "0#{value}" + end + unless value.match(/^([a-f\d]{2} ?)*$/i) + fail("The data must be a hex encoded string of the form: '00 01 02 ...'") + end + # First, strip out all spaces from the string in the manfest. Next, + # put a space after each pair of hex digits. Strip off the rightmost + # space if it's present. Finally, downcase the whole thing. The final + # result should be: "CaFE BEEF" => "ca fe be ef" + value.gsub(/\s+/, '').gsub(/([0-9a-f]{2})/i) { "#{$1} " }.rstrip.downcase + else #:string, :expand, :array + value + end + end + + def property_matches?(current, desired) + case resource[:type] + when :binary + return false unless current + current.casecmp(desired) == 0 + else + super(current, desired) + end + end + + def change_to_s(currentvalue, newvalue) + if currentvalue.respond_to? :join + currentvalue = currentvalue.join(",") + end + if newvalue.respond_to? :join + newvalue = newvalue.join(",") + end + super(currentvalue, newvalue) + end + end + + # Autorequire the nearest ancestor registry_key found in the catalog. + autorequire(:registry_key) do + req = [] + # This is a value path and not a key path because it's based on the path of + # the value resource. + path = PuppetX::Puppetlabs::Registry::RegistryValuePath.new(value(:path)) + # It is important to match against the downcase value of the path because + # other resources are expected to alias themselves to the downcase value so + # that we respect the case insensitive and preserving nature of Windows. + if found = path.enum_for(:ascend).find { |p| catalog.resource(:registry_key, p.to_s.downcase) } + req << found.to_s.downcase + end + req + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry.rb new file mode 100644 index 000000000..e13ef24ee --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry.rb @@ -0,0 +1,162 @@ +module PuppetX +module Puppetlabs +module Registry + # For 64-bit OS, use 64-bit view. Ignored on 32-bit OS + KEY_WOW64_64KEY = 0x100 + # For 64-bit OS, use 32-bit view. Ignored on 32-bit OS + KEY_WOW64_32KEY = 0x200 unless defined? KEY_WOW64_32KEY + + # This is the base class for Path manipulation. This class is meant to be + # abstract, RegistryKeyPath and RegistryValuePath will customize and override + # this class. + class RegistryPathBase < String + attr_reader :path + def initialize(path) + @filter_path_memo = nil + @path ||= path + super(path) + end + + # The path is valid if we're able to parse it without exceptions. + def valid? + (filter_path and true) rescue false + end + + def canonical + filter_path[:canonical] + end + + # This method is meant to help setup aliases so autorequire can sort itself + # out in a case insensitive but preserving manner. It returns an array of + # resource identifiers. + def aliases + [canonical.downcase] + end + + def access + filter_path[:access] + end + + def root + filter_path[:root] + end + + def ascend(&block) + p = canonical + while idx = p.rindex('\\') + p = p[0, idx] + yield p + end + end + + private + + def filter_path + if @filter_path_memo + return @filter_path_memo + end + result = {} + + path = @path + + result[:valuename] = case path[-1, 1] + when '\\' + result[:is_default] = true + '' + else + result[:is_default] = false + idx = path.rindex('\\') || 0 + if idx > 0 + path[idx+1..-1] + else + '' + end + end + + # Strip off any trailing slash. + path = path.gsub(/\\*$/, '') + + unless captures = /^(32:)?([h|H][^\\]*)((?:\\[^\\]{1,255})*)$/.match(path) + raise ArgumentError, "Invalid registry key: #{path}" + end + + case captures[1] + when '32:' + result[:access] = PuppetX::Puppetlabs::Registry::KEY_WOW64_32KEY + result[:prefix] = '32:' + else + result[:access] = PuppetX::Puppetlabs::Registry::KEY_WOW64_64KEY + result[:prefix] = '' + end + + # canonical root key symbol + result[:root] = case captures[2].to_s.downcase + when /hkey_local_machine/, /hklm/ + :hklm + when /hkey_classes_root/, /hkcr/ + :hkcr + when /hkey_users/, /hku/ + :hku + when /hkey_current_user/, /hkcu/, + /hkey_current_config/, /hkcc/, + /hkey_performance_data/, + /hkey_performance_text/, + /hkey_performance_nlstext/, + /hkey_dyn_data/ + raise ArgumentError, "Unsupported predefined key: #{path}" + else + raise ArgumentError, "Invalid registry key: #{path}" + end + + result[:trailing_path] = captures[3] + + result[:trailing_path].gsub!(/^\\/, '') + + if result[:trailing_path].empty? + result[:canonical] = "#{result[:prefix]}#{result[:root].to_s}" + else + # Leading backslash is not part of the subkey name + result[:canonical] = "#{result[:prefix]}#{result[:root].to_s}\\#{result[:trailing_path]}" + end + + @filter_path_memo = result + end + end + + class RegistryKeyPath < RegistryPathBase + def subkey + filter_path[:trailing_path] + end + end + + class RegistryValuePath < RegistryPathBase + def canonical + # This method gets called in the type and the provider. We need to + # preserve the trailing backslash for the provider, otherwise it won't + # think this is a default value. + if default? + filter_path[:canonical] << "\\" + else + filter_path[:canonical] + end + end + + def subkey + if default? + filter_path[:trailing_path] + else + filter_path[:trailing_path].gsub(/^(.*)\\.*$/, '\1') + end + end + + def valuename + filter_path[:valuename] + end + + def default? + !!filter_path[:is_default] + end + end +end +end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry/provider_base.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry/provider_base.rb new file mode 100644 index 000000000..c7a120dfa --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/lib/puppet_x/puppetlabs/registry/provider_base.rb @@ -0,0 +1,229 @@ +# This module is meant to be mixed into the registry_key AND registry_value providers. +module PuppetX +module Puppetlabs +module Registry +module ProviderBase + def self.define_ffi(base) + extend FFI::Library + + ffi_convention :stdcall + + # uintptr_t is defined in an FFI conf as platform specific, either + # ulong_long on x64 or just ulong on x86 + typedef :uintptr_t, :handle + # any time LONG / ULONG is in a win32 API definition DO NOT USE platform specific width + # which is what FFI uses by default + # instead create new aliases for these very special cases + typedef :int32, :win32_long + typedef :uint32, :win32_ulong + typedef :uint32, :dword + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724911(v=vs.85).aspx + # LONG WINAPI RegQueryValueEx( + # _In_ HKEY hKey, + # _In_opt_ LPCTSTR lpValueName, + # _Reserved_ LPDWORD lpReserved, + # _Out_opt_ LPDWORD lpType, + # _Out_opt_ LPBYTE lpData, + # _Inout_opt_ LPDWORD lpcbData + # ); + ffi_lib :advapi32 + attach_function :RegQueryValueExW, + [:handle, :pointer, :pointer, :pointer, :pointer, :pointer], :win32_long + + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724847(v=vs.85).aspx + # LONG WINAPI RegDeleteKeyEx( + # _In_ HKEY hKey, + # _In_ LPCTSTR lpSubKey, + # _In_ REGSAM samDesired, + # _Reserved_ DWORD Reserved + # ); + ffi_lib :advapi32 + attach_function :RegDeleteKeyExW, + [:handle, :pointer, :win32_ulong, :dword], :win32_long + + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724851(v=vs.85).aspx + # LONG WINAPI RegDeleteValue( + # _In_ HKEY hKey, + # _In_opt_ LPCTSTR lpValueName + # ); + ffi_lib :advapi32 + attach_function :RegDeleteValueW, + [:handle, :pointer], :win32_long + + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms724923(v=vs.85).aspx + # LONG WINAPI RegSetValueEx( + # _In_ HKEY hKey, + # _In_opt_ LPCTSTR lpValueName, + # _Reserved_ DWORD Reserved, + # _In_ DWORD dwType, + # _In_ const BYTE *lpData, + # _In_ DWORD cbData + # ); + ffi_lib :advapi32 + attach_function :RegSetValueExW, + [:handle, :pointer, :dword, :dword, :pointer, :dword], :win32_long + + # this duplicates code found in puppet, but necessary for backwards compat + class << base + # note that :uchar is aliased in Puppet to :byte + def from_string_to_wide_string(str, &block) + str = wide_string(str) + FFI::MemoryPointer.new(:uchar, str.bytesize) do |ptr| + ptr.put_array_of_uchar(0, str.bytes.to_a) + + yield ptr + end + + # ptr has already had free called, so nothing to return + nil + end + + def wide_string(str) + # if given a nil string, assume caller wants to pass a nil pointer to win32 + return nil if str.nil? + # ruby (< 2.1) does not respect multibyte terminators, so it is possible + # for a string to contain a single trailing null byte, followed by garbage + # causing buffer overruns. + # + # See http://svn.ruby-lang.org/cgi-bin/viewvc.cgi?revision=41920&view=revision + newstr = str + "\0".encode(str.encoding) + newstr.encode!('UTF-16LE') + end + end + end + + # This is a class method in order to be easily mocked in the spec tests. + def self.initialize_system_api(base) + if Puppet.features.microsoft_windows? + begin + require 'win32/registry' + rescue LoadError => exc + msg = "Could not load the required win32/registry library (ErrorID 1EAD86E3-D533-4286-BFCB-CCE8B818DDEA) [#{exc.message}]" + Puppet.err msg + error = Puppet::Error.new(msg) + error.set_backtrace exc.backtrace + raise error + end + + begin + require 'ffi' + define_ffi(base) + rescue LoadError => exc + msg = "Could not load the required ffi library [#{exc.message}]" + Puppet.err msg + error = Puppet::Error.new(msg) + error.set_backtrace exc.backtrace + raise error + end + + class << base + # create instance to access mix-in methods since it doesn't use module_function + require 'puppet/util/windows/registry' + def RegistryHelpers + @registry_helpers ||= Class.new.extend(Puppet::Util::Windows::Registry) + end + end + end + end + + def self.included(base) + # Initialize the Win32 API. This is a method call so the spec tests can + # easily mock the initialization of the Win32 libraries on non-win32 + # systems. + initialize_system_api(base) + + # Define an hkeys class method in the eigenclass we're being mixed into. + # This is the one true place to define the root hives we support. + class << base + def hkeys + # REVISIT: I'd like to make this easier to mock and stub. + { + :hkcr => Win32::Registry::HKEY_CLASSES_ROOT, + :hklm => Win32::Registry::HKEY_LOCAL_MACHINE, + :hku => Win32::Registry::HKEY_USERS, + } + end + end + end + + # The rest of these methods will be mixed in as instance methods into the + # provider class. The path method is expected to be mixed in by the provider + # specific module, ProviderKeyBase or ProviderValueBase + def from_string_to_wide_string(str, &block) + self.class.from_string_to_wide_string(str, &block) + end + + def wide_string(str) + self.class.wide_string(str) + end + + def hkeys + self.class.hkeys + end + + def registry_helpers + self.class.RegistryHelpers + end + + def hive + hkeys[path.root] + end + + def access + path.access + end + + def root + path.root + end + + def subkey + path.subkey + end + + def valuename + path.valuename + end + + def type2name_map + { + Win32::Registry::REG_NONE => :none, + Win32::Registry::REG_SZ => :string, + Win32::Registry::REG_EXPAND_SZ => :expand, + Win32::Registry::REG_BINARY => :binary, + Win32::Registry::REG_DWORD => :dword, + Win32::Registry::REG_QWORD => :qword, + Win32::Registry::REG_MULTI_SZ => :array + } + end + + def type2name(type) + type2name_map[type] + end + + def name2type(name) + name2type = {} + type2name_map.each_pair {|k,v| name2type[v] = k} + name2type[name] + end + + def each_value(key, &block) + # This problem affects Ruby 2.1 and higher by introducing locale conversion + # unnecessary. Puppet 4 introduces it's own each_value patches to the + # Registry abstraction to work around these problems + # https://github.com/puppetlabs/puppet/commit/b46ede74f640a809b68a473ac8720b93b13d2ac3 + if registry_helpers.respond_to?(:each_value) + registry_helpers.each_value(key) do |name, type, data| + yield name, type, data + end + else + key.each_value do |name, type, data| + yield name, type, data + end + end + end +end +end +end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/manifests/service.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/manifests/service.pp new file mode 100644 index 000000000..eb0ea1a40 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/manifests/service.pp @@ -0,0 +1,135 @@ +# Define: registry::service +# +# This defined resource type manages service entries in the Microsoft service +# control framework by managing the appropriate registry keys and values. +# +# This is an alternative approach to using INSTSRV.EXE [1]. +# +# [1] http://support.microsoft.com/kb/137890 +# +# Parameters: +# +# ensure: [ present, absent ] +# +# display_name: The Display Name of the service. Defaults to the title of +# the resource. +# +# description: A description of the service +# +# command: The command to execute +# +# start: The starting mode of the service. (Note, the native service +# resource can also be used to manage this setting.) +# [ automatic, manual, disabled ] +# +# Actions: +# +# Manages the values in the key HKLM\System\CurrentControlSet\Services\$name\ +# +# Requires: +# +# Module puppetlabs-registry +# +# Sample Usage: +# +# registry::service { puppet: +# ensure => present, +# display_name => 'Puppet Agent', +# description => 'Periodically fetches and applies +# configurations from a Puppet master server.', +# command => 'C:\PuppetLabs\Puppet\service\daemon.bat', +# } +# +define registry::service( + $ensure = 'UNSET', + $display_name = 'UNSET', + $description = 'UNSET', + $command = 'UNSET', + $start = 'UNSET' +) { + + $ensure_real = $ensure ? { + 'UNSET' => present, + undef => present, + present => present, + absent => absent, + } + + $display_name_real = $display_name ? { + 'UNSET' => $name, + default => $display_name, + } + + $description_real = $description ? { + 'UNSET' => $display_name_real, + default => $description, + } + + # FIXME Better validation of the command parameter. + # (Fully qualified path? Though, it will be a REG_EXPAND_SZ.) + $command_real = $command ? { + default => $command, + } + + # Map descriptive names to flags. + $start_real = $start ? { + automatic => 2, + manual => 3, + disabled => 4, + } + + # Variable to hold the base key path. + $service_key = "HKLM\\System\\CurrentControlSet\\Services\\${name}" + + # Manage the key + if $ensure_real == present { + registry_key { $service_key: + ensure => present, + } + } else { + registry_key { $service_key: + ensure => absent, + # REVISIT: purge_values => true, + } + } + + # Manage the values + if $ensure_real == present { + registry_value { "${service_key}\\Description": + ensure => present, + type => string, + data => $description_real, + } + registry_value { "${service_key}\\DisplayName": + ensure => present, + type => string, + data => $display_name_real, + } + registry_value { "${service_key}\\ErrorControl": + ensure => present, + type => dword, + data => 0x00000001, + } + registry_value { "${service_key}\\ImagePath": + ensure => present, + type => expand, + data => $command_real, + } + registry_value { "${service_key}\\ObjectName": + ensure => present, + type => string, + data => 'LocalSystem', + } + registry_value { "${service_key}\\Start": + ensure => present, + type => dword, + data => $start_real, + } + registry_value { "${service_key}\\Type": + ensure => present, + type => dword, + data => 0x00000010, # (16) + } + } +} +# EOF diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/manifests/value.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/manifests/value.pp new file mode 100644 index 000000000..4c30cc557 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/manifests/value.pp @@ -0,0 +1,83 @@ +# = Define: registry::value +# +# This defined resource type provides a higher level of abstraction on top of +# the registry_key and registry_value resources. Using this defined resource +# type, you do not need to explicitly manage the parent key for a particular +# value. Puppet will automatically manage the parent key for you. +# +# == Parameters: +# +# key:: The path of key the value will placed inside. +# +# value:: The name of the registry value to manage. This will be copied from +# the resource title if not specified. The special value of +# '(default)' may be used to manage the default value of the key. +# +# type:: The type the registry value. Defaults to 'string'. See the output of +# `puppet describe registry_value` for a list of supported types in the +# "type" parameter. +# +# data:: The data to place inside the registry value. +# +# == Actions: +# - Manage the parent key if not already managed. +# - Manage the value +# +# == Requires: +# - Registry Module +# - Stdlib Module +# +# == Sample Usage: +# +# This example will automatically manage the key. It will also create a value +# named 'puppetmaster' inside this key. +# +# class myapp { +# registry::value { 'puppetmaster': +# key => 'HKLM\Software\Vendor\PuppetLabs', +# data => 'puppet.puppetlabs.com', +# } +# } +# +define registry::value ( + $key, + $value = undef, + $type = 'string', + $data = undef, +) { + + # ensure windows os + if $::operatingsystem != 'windows'{ + fail("Unsupported OS ${::operatingsystem}") + } + + # validate our inputs. + validate_re($key, '^\w+', + "key parameter must not be empty but it is key => '${key}'") + validate_re($type, '^\w+', + "type parameter must not be empty but it is type => '${type}'") + + + + $value_real = $value ? { + undef => $name, + '(default)' => '', + default => $value, + } + + # Resource defaults. + Registry_key { ensure => present } + Registry_value { ensure => present } + + if !defined(Registry_key[$key]) { + registry_key { $key: } + } + + # If value_real is an empty string then the default value of the key will be + # managed. + registry_value { "${key}\\${value_real}": + type => $type, + data => $data, + } +} + diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/metadata.json b/modules/utilities/windows/registry/puppetlabs_registry_library/metadata.json new file mode 100644 index 000000000..598cac308 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/metadata.json @@ -0,0 +1,38 @@ +{ + "name": "puppetlabs-registry", + "version": "1.1.4", + "author": "Puppet Inc", + "summary": "This module provides a native type and provider to manage keys and values in the Windows Registry", + "license": "Apache-2.0", + "source": "git://github.com/puppetlabs/puppetlabs-registry.git", + "project_page": "http://links.puppet.com/registry-module", + "issues_url": "https://github.com/puppetlabs/puppetlabs-registry/issues", + "dependencies": [ + {"name":"puppetlabs/stdlib","version_requirement":">= 2.3.0"} + ], + "data_provider": null, + "operatingsystem_support": [ + { + "operatingsystem": "Windows", + "operatingsystemrelease": [ + "Server 2008", + "Server 2008 R2", + "Server 2012", + "Server 2012 R2", + "7", + "8" + ] + } + ], + "requirements": [ + { + "name": "pe", + "version_requirement": ">= 3.3.0 < 2015.4.0" + }, + { + "name": "puppet", + "version_requirement": ">= 3.3.0 < 5.0.0" + } + ], + "description": "This module provides a native type and provider to manage keys and values in the Windows Registry" +} diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/puppetlabs_registry_library.pp b/modules/utilities/windows/registry/puppetlabs_registry_library/puppetlabs_registry_library.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/secgen_metadata.xml b/modules/utilities/windows/registry/puppetlabs_registry_library/secgen_metadata.xml new file mode 100644 index 000000000..f75cb829d --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/secgen_metadata.xml @@ -0,0 +1,18 @@ + + + + Registry library + Jason Keighley + Puppetlabs + Apache v2 + A local version of the puppet registry library + + registry + windows + + + + + \ No newline at end of file diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/centos-7-x64.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/centos-7-x64.yml new file mode 100644 index 000000000..5eebdefbf --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/centos-7-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + centos-7-x64: + roles: + - agent + - default + platform: el-7-x86_64 + hypervisor: vagrant + box: puppetlabs/centos-7.2-64-nocm +CONFIG: + type: foss diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/debian-8-x64.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/debian-8-x64.yml new file mode 100644 index 000000000..fef6e63ca --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/debian-8-x64.yml @@ -0,0 +1,10 @@ +HOSTS: + debian-8-x64: + roles: + - agent + - default + platform: debian-8-amd64 + hypervisor: vagrant + box: puppetlabs/debian-8.2-64-nocm +CONFIG: + type: foss diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/default.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/default.yml new file mode 100644 index 000000000..dba339c46 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/default.yml @@ -0,0 +1,10 @@ +HOSTS: + ubuntu-1404-x64: + roles: + - agent + - default + platform: ubuntu-14.04-amd64 + hypervisor: vagrant + box: puppetlabs/ubuntu-14.04-64-nocm +CONFIG: + type: foss diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/centos-7.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/centos-7.yml new file mode 100644 index 000000000..a3333aac5 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/centos-7.yml @@ -0,0 +1,12 @@ +HOSTS: + centos-7-x64: + platform: el-7-x86_64 + hypervisor: docker + image: centos:7 + docker_preserve_image: true + docker_cmd: '["/usr/sbin/init"]' + # install various tools required to get the image up to usable levels + docker_image_commands: + - 'yum install -y crontabs tar wget openssl sysvinit-tools iproute which initscripts' +CONFIG: + trace_limit: 200 diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/debian-8.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/debian-8.yml new file mode 100644 index 000000000..df5c31944 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/debian-8.yml @@ -0,0 +1,11 @@ +HOSTS: + debian-8-x64: + platform: debian-8-amd64 + hypervisor: docker + image: debian:8 + docker_preserve_image: true + docker_cmd: '["/sbin/init"]' + docker_image_commands: + - 'apt-get update && apt-get install -y net-tools wget locales strace lsof && echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && locale-gen' +CONFIG: + trace_limit: 200 diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/ubuntu-14.04.yml b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/ubuntu-14.04.yml new file mode 100644 index 000000000..b1efa5839 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/acceptance/nodesets/docker/ubuntu-14.04.yml @@ -0,0 +1,12 @@ +HOSTS: + ubuntu-1404-x64: + platform: ubuntu-14.04-amd64 + hypervisor: docker + image: ubuntu:14.04 + docker_preserve_image: true + docker_cmd: '["/sbin/init"]' + docker_image_commands: + # ensure that upstart is booting correctly in the container + - 'rm /usr/sbin/policy-rc.d && rm /sbin/initctl && dpkg-divert --rename --remove /sbin/initctl && apt-get update && apt-get install -y net-tools wget && locale-gen en_US.UTF-8' +CONFIG: + trace_limit: 200 diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/spec_helper.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/spec_helper.rb new file mode 100644 index 000000000..f6395ee39 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/spec_helper.rb @@ -0,0 +1,30 @@ + +dir = File.expand_path(File.dirname(__FILE__)) +$LOAD_PATH.unshift File.join(dir, 'lib') + +require 'mocha' +require 'puppet' +require 'rspec' +require 'rspec-puppet' +require 'puppetlabs_spec_helper/module_spec_helper' + +RSpec.configure do |c| + c.mock_with :mocha + + if File::ALT_SEPARATOR && RUBY_VERSION =~ /^1\./ + require 'win32console' + c.output_stream = $stdout + c.error_stream = $stderr + c.formatters.each { |f| f.instance_variable_set(:@output, $stdout) } + end + + c.expect_with :rspec do |e| + e.syntax = [:should, :expect] + end +end + +# We need this because the RAL uses 'should' as a method. This +# allows us the same behaviour but with a different method name. +class Object + alias :must :should +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_key_spec.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_key_spec.rb new file mode 100644 index 000000000..1d7a2a1cd --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_key_spec.rb @@ -0,0 +1,116 @@ +#! /usr/bin/env ruby + +require 'spec_helper' +require 'puppet/type/registry_key' + +describe Puppet::Type.type(:registry_key).provider(:registry), :if => Puppet.features.microsoft_windows? do + let (:catalog) do Puppet::Resource::Catalog.new end + let (:type) { Puppet::Type.type(:registry_key) } + + puppet_key = "SOFTWARE\\Puppet Labs" + subkey_name ="PuppetRegProviderTest" + + before(:each) do + # problematic Ruby codepath triggers a conversion of UTF-16LE to + # a local codepage which can totally break when that codepage has no + # conversion from the given UTF-16LE characters to local codepage + # a prime example is that IBM437 has no conversion from a Unicode en-dash + Win32::Registry.any_instance.expects(:export_string).never + + Win32::Registry.any_instance.expects(:delete_value).never + Win32::Registry.any_instance.expects(:delete_key).never + + if RUBY_VERSION >= '2.1' + # also, expect that we're not using Rubys each_key / each_value which exhibit bad behavior + Win32::Registry.any_instance.expects(:each_key).never + Win32::Registry.any_instance.expects(:each_value).never + end + end + + describe "#destroy" do + it "can destroy a randomly created key" do + + guid = SecureRandom.uuid + reg_key = type.new(:path => "hklm\\#{puppet_key}\\#{subkey_name}\\#{guid}", :provider => described_class.name) + already_exists = reg_key.provider.exists? + already_exists.should be_falsey + + # something has gone terribly wrong here, pull the ripcord + break if already_exists + + reg_key.provider.create + reg_key.provider.exists?.should be true + + # test FFI code + reg_key.provider.destroy + reg_key.provider.exists?.should be false + end + end + + describe "#purge_values", :if => Puppet.features.microsoft_windows? do + let (:guid) { SecureRandom.uuid } + let (:reg_path) { "#{puppet_key}\\#{subkey_name}\\Unicode-#{guid}" } + + def bytes_to_utf8(bytes) + bytes.pack('c*').force_encoding(Encoding::UTF_8) + end + + after(:each) do + reg_key = type.new(:path => "hklm\\#{reg_path}", :provider => described_class.name) + reg_key.provider.destroy + + reg_key.provider.exists?.should be_falsey + end + + context "with ANSI strings on all Ruby platforms" do + before(:each) do + Win32::Registry::HKEY_LOCAL_MACHINE.create(reg_path, + Win32::Registry::KEY_ALL_ACCESS | + PuppetX::Puppetlabs::Registry::KEY_WOW64_64KEY) do |reg_key| + reg_key.write('hi', Win32::Registry::REG_SZ, 'yes') + end + end + + it "does not raise an error" do + reg_key = type.new(:catalog => catalog, + :ensure => :absent, + :name => "hklm\\#{reg_path}", + :purge_values => true, + :provider => described_class.name) + + catalog.add_resource(reg_key) + + expect { reg_key.eval_generate }.to_not raise_error + end + end + + context "with unicode", :if => Puppet.features.microsoft_windows? && RUBY_VERSION =~ /^2\./ do + before(:each) do + # create temp registry key with Unicode values + Win32::Registry::HKEY_LOCAL_MACHINE.create(reg_path, + Win32::Registry::KEY_ALL_ACCESS | + PuppetX::Puppetlabs::Registry::KEY_WOW64_64KEY) do |reg_key| + endash = bytes_to_utf8([0xE2, 0x80, 0x93]) + tm = bytes_to_utf8([0xE2, 0x84, 0xA2]) + + reg_key.write(endash, Win32::Registry::REG_SZ, tm) + end + end + + it "does not use Rubys each_value, which unnecessarily string encodes" do + # endash and tm undergo LOCALE conversion during Rubys each_value + # which will generally lead to a conversion exception + reg_key = type.new(:catalog => catalog, + :ensure => :absent, + :name => "hklm\\#{reg_path}", + :purge_values => true, + :provider => described_class.name) + + catalog.add_resource(reg_key) + + # this will trigger + expect { reg_key.eval_generate }.to_not raise_error + end + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_value_spec.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_value_spec.rb new file mode 100644 index 000000000..3b6f612fe --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/provider/registry_value_spec.rb @@ -0,0 +1,223 @@ +#! /usr/bin/env ruby + +require 'spec_helper' +require 'puppet/type/registry_value' + +describe Puppet::Type.type(:registry_value).provider(:registry), :if => Puppet.features.microsoft_windows? do + let (:catalog) do Puppet::Resource::Catalog.new end + let (:type) { Puppet::Type.type(:registry_value) } + + puppet_key = "SOFTWARE\\Puppet Labs" + subkey_name ="PuppetRegProviderTest" + + before(:all) do + Win32::Registry::HKEY_LOCAL_MACHINE.create("#{puppet_key}\\#{subkey_name}", + Win32::Registry::KEY_ALL_ACCESS | + PuppetX::Puppetlabs::Registry::KEY_WOW64_64KEY) + end + + before(:each) do + # problematic Ruby codepath triggers a conversion of UTF-16LE to + # a local codepage which can totally break when that codepage has no + # conversion from the given UTF-16LE characters to local codepage + # a prime example is that IBM437 has no conversion from a Unicode en-dash + Win32::Registry.any_instance.expects(:export_string).never + + Win32::Registry.any_instance.expects(:delete_value).never + Win32::Registry.any_instance.expects(:delete_key).never + + if RUBY_VERSION >= '2.1' + # also, expect that we're not using Rubys each_key / each_value which exhibit bad behavior + Win32::Registry.any_instance.expects(:each_key).never + Win32::Registry.any_instance.expects(:each_value).never + + # this covers []= write_s write_i and write_bin + Win32::Registry.any_instance.expects(:write).never + end + end + + after(:all) do + # Ruby 2.1.5 has bugs with deleting registry keys due to using ANSI + # character APIs, but passing wide strings to them (facepalm) + # https://github.com/ruby/ruby/blob/v2_1_5/ext/win32/lib/win32/registry.rb#L323-L329 + # therefore, use our own code instead of hklm.delete_value + + # NOTE: registry_value tests unfortunately depend on registry_key type + # otherwise, there would be a bit of Win32 API code here + reg_key = Puppet::Type.type(:registry_key).new(:path => "hklm\\#{puppet_key}\\#{subkey_name}", + :provider => :registry) + reg_key.provider.destroy + end + + describe "#exists?" do + it "should return true for a well known hive" do + reg_value = type.new(:path => 'hklm\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareType', :provider => described_class.name) + reg_value.provider.exists?.should be true + end + + it "should return false for a bogus hive/path" do + reg_value = type.new(:path => 'hklm\foobar5000', :catalog => catalog, :provider => described_class.name) + reg_value.provider.exists?.should be false + end + end + + describe "#regvalue" do + it "should return a valid string for a well known key" do + reg_value = type.new(:path => 'hklm\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRoot', :provider => described_class.name) + reg_value.provider.data.should eq [ENV['SystemRoot']] + reg_value.provider.type.should eq :string + end + + it "returns a string of lowercased hex encoded bytes" do + reg = described_class.new + type, data = reg.from_native([3, "\u07AD"]) + data.should eq ['de ad'] + end + + it "left pads binary strings" do + reg = described_class.new + type, data = reg.from_native([3, "\x1"]) + data.should eq ['01'] + end + end + + describe "#destroy" do + let (:path) { path = "hklm\\#{puppet_key}\\#{subkey_name}\\#{SecureRandom.uuid}" } + def create_and_destroy(path, reg_type, data) + reg_value = type.new(:path => path, + :type => reg_type, + :data => data, + :provider => described_class.name) + already_exists = reg_value.provider.exists? + already_exists.should be_falsey + + # something has gone terribly wrong here, pull the ripcord + fail if already_exists + + reg_value.provider.create + reg_value.provider.exists?.should be true + expect(reg_value.provider.data).to eq([data].flatten) + + reg_value.provider.destroy + reg_value.provider.exists?.should be false + end + + it "can destroy a randomly created REG_SZ value" do + create_and_destroy(path, :string, SecureRandom.uuid) + end + + it "can destroy a randomly created REG_EXPAND_SZ value" do + create_and_destroy(path, :expand, "#{SecureRandom.uuid} system root is %SystemRoot%") + end + + it "can destroy a randomly created REG_BINARY value" do + create_and_destroy(path, :binary, '01 01 10 10') + end + + it "can destroy a randomly created REG_DWORD value" do + create_and_destroy(path, :dword, rand(2 ** 32 - 1)) + end + + it "can destroy a randomly created REG_QWORD value" do + create_and_destroy(path, :qword, rand(2 ** 64 - 1)) + end + + it "can destroy a randomly created REG_MULTI_SZ value" do + create_and_destroy(path, :array, [SecureRandom.uuid, SecureRandom.uuid]) + end + end + + context "when writing numeric values" do + let (:path) { path = "hklm\\#{puppet_key}\\#{subkey_name}\\#{SecureRandom.uuid}" } + + after(:each) do + reg_value = type.new(:path => path, :provider => described_class.name) + + reg_value.provider.destroy + expect(reg_value.provider).to_not be_exists + end + + def write_and_read_value(path, reg_type, value) + reg_value = type.new(:path => path, + :type => reg_type, + :data => value, + :provider => described_class.name) + + reg_value.provider.create + expect(reg_value.provider).to be_exists + expect(reg_value.provider.type).to eq(reg_type) + + written = reg_value.provider.data.first + expect(written).to eq(value) + end + + + # values chosen at 1 bit past previous byte boundary + [0xFF + 1, 0xFFFF + 1, 0xFFFFFF + 1, 0xFFFFFFFF].each do |value| + it "properly round-trips written values by converting endianness properly" do + write_and_read_value(path, :dword, value) + write_and_read_value(path, :qword, value) + end + end + + [0xFFFFFFFFFF + 1, 0xFFFFFFFFFFFF + 1, 0xFFFFFFFFFFFFFF + 1, 0xFFFFFFFFFFFFFFFF].each do |value| + it "properly round-trips written values by converting endianness properly" do + write_and_read_value(path, :qword, value) + end + end + end + + context "when reading non-ASCII values" do + ENDASH_UTF_8 = [0xE2, 0x80, 0x93] + ENDASH_UTF_16 = [0x2013] + TM_UTF_8 = [0xE2, 0x84, 0xA2] + TM_UTF_16 = [0x2122] + + let (:guid) { SecureRandom.uuid } + + after(:each) do + # Ruby 2.1.5 has bugs with deleting registry keys due to using ANSI + # character APIs, but passing wide strings to them (facepalm) + # https://github.com/ruby/ruby/blob/v2_1_5/ext/win32/lib/win32/registry.rb#L323-L329 + # therefore, use our own code instead of hklm.delete_value + + reg_value = type.new(:path => "hklm\\#{puppet_key}\\#{subkey_name}\\#{guid}", + :provider => described_class.name) + + reg_value.provider.destroy + reg_value.provider.exists?.should be_falsey + end + + # proof that there is no conversion to local encodings like IBM437 + it "will return a UTF-8 string from a REG_SZ registry value (written as UTF-16LE)", + :if => Puppet.features.microsoft_windows? && RUBY_VERSION >= '2.1' do + + # create a UTF-16LE byte array representing "–™" + utf_16_bytes = ENDASH_UTF_16 + TM_UTF_16 + utf_16_str = utf_16_bytes.pack('s*').force_encoding(Encoding::UTF_16LE) + + # and it's UTF-8 equivalent bytes + utf_8_bytes = ENDASH_UTF_8 + TM_UTF_8 + utf_8_str = utf_8_bytes.pack('c*').force_encoding(Encoding::UTF_8) + + reg_value = type.new(:path => "hklm\\#{puppet_key}\\#{subkey_name}\\#{guid}", + :type => :string, + :data => utf_16_str, + :provider => described_class.name) + + already_exists = reg_value.provider.exists? + already_exists.should be_falsey + + reg_value.provider.create + reg_value.provider.exists?.should be true + + reg_value.provider.data.length.should eq 1 + reg_value.provider.type.should eq :string + + # The UTF-16LE string written should come back as the equivalent UTF-8 + written = reg_value.provider.data.first + written.should eq(utf_8_str) + written.encoding.should eq(Encoding::UTF_8) + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_key_spec.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_key_spec.rb new file mode 100644 index 000000000..dd90ca901 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_key_spec.rb @@ -0,0 +1,151 @@ +#!/usr/bin/env ruby -S rspec +require 'spec_helper' +require 'puppet/resource' +require 'puppet/resource/catalog' +require 'puppet/type/registry_key' + +describe Puppet::Type.type(:registry_key) do + let (:catalog) do Puppet::Resource::Catalog.new end + + + # This is overridden here so we get a consistent association with the key + # and a catalog using memoized let methods. + let (:key) do + Puppet::Type.type(:registry_key).new(:name => 'HKLM\Software', :catalog => catalog) + end + let(:provider) { Puppet::Provider.new(key) } + + before :each do + key.provider = provider + end + + [:ensure].each do |property| + it "should have a #{property} property" do + described_class.attrclass(property).ancestors.should be_include(Puppet::Property) + end + + it "should have documentation for its #{property} property" do + described_class.attrclass(property).doc.should be_instance_of(String) + end + end + + describe "path parameter" do + it "should have a path parameter" do + Puppet::Type.type(:registry_key).attrtype(:path).must == :param + end + + %w[hklm hklm\software hklm\software\vendor].each do |path| + it "should accept #{path}" do + key[:path] = path + end + end + + %w[hku hku\.DEFAULT hku\.DEFAULT\software hku\.DEFAULT\software\vendor].each do |path| + it "should accept #{path}" do + key[:path] = path + end + end + + %w[HKEY_DYN_DATA HKEY_PERFORMANCE_DATA].each do |path| + it "should reject #{path} as unsupported case insensitively" do + expect { key[:path] = path }.to raise_error(Puppet::Error, /Unsupported/) + end + end + + %w[hklm\\ hklm\foo\\ unknown unknown\subkey \:hkey].each do |path| + it "should reject #{path} as invalid" do + path = "hklm\\" + 'a' * 256 + expect { key[:path] = path }.to raise_error(Puppet::Error, /Invalid registry key/) + end + end + + %w[HKLM HKEY_LOCAL_MACHINE hklm].each do |root| + it "should canonicalize the root key #{root}" do + key[:path] = root + key[:path].must == 'hklm' + end + end + + it 'should be case-preserving' + it 'should be case-insensitive' + it 'should autorequire ancestor keys' + it 'should support 32-bit keys' do + key[:path] = '32:hklm\software' + end + end + + describe "#eval_generate" do + context "not purging" do + it "should return an empty array" do + key.eval_generate.must be_empty + end + end + + context "purging" do + let (:catalog) do Puppet::Resource::Catalog.new end + + # This is overridden here so we get a consistent association with the key + # and a catalog using memoized let methods. + let (:key) do + Puppet::Type.type(:registry_key).new(:name => 'HKLM\Software', :catalog => catalog) + end + + before :each do + key[:purge_values] = true + catalog.add_resource(key) + catalog.add_resource(Puppet::Type.type(:registry_value).new(:path => "#{key[:path]}\\val1", :catalog => catalog)) + catalog.add_resource(Puppet::Type.type(:registry_value).new(:path => "#{key[:path]}\\val2", :catalog => catalog)) + end + + it "should return an empty array if the key doesn't have any values" do + key.provider.expects(:values).returns([]) + key.eval_generate.must be_empty + end + + it "should purge existing values that are not being managed" do + key.provider.expects(:values).returns(['val1', 'val3']) + res = key.eval_generate.first + + res[:ensure].must == :absent + res[:path].must == "#{key[:path]}\\val3" + end + + it "should return an empty array if all existing values are being managed" do + key.provider.expects(:values).returns(['val1', 'val2']) + key.eval_generate.must be_empty + end + end + end + + describe "#autorequire" do + let :the_catalog do + Puppet::Resource::Catalog.new + end + + let(:the_resource_name) { 'HKLM\Software\Vendor\PuppetLabs' } + + let :the_resource do + # JJM Holy cow this is an intertangled mess. ;) + resource = Puppet::Type.type(:registry_key).new(:name => the_resource_name, :catalog => the_catalog) + the_catalog.add_resource resource + resource + end + + it 'Should initialize the catalog instance variable' do + the_resource.catalog.must be the_catalog + end + + it 'Should allow case insensitive lookup using the downcase path' do + the_resource.must be the_catalog.resource(:registry_key, the_resource_name.downcase) + end + + it 'Should preserve the case of the user specified path' do + the_resource.must be the_catalog.resource(:registry_key, the_resource_name) + end + + it 'Should return the same resource regardless of the alias used' do + the_resource.must be the_catalog.resource(:registry_key, the_resource_name) + the_resource.must be the_catalog.resource(:registry_key, the_resource_name.downcase) + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_value_spec.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_value_spec.rb new file mode 100644 index 000000000..03deb1c7c --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/unit/puppet/type/registry_value_spec.rb @@ -0,0 +1,161 @@ +#!/usr/bin/env ruby -S rspec +require 'spec_helper' +require 'puppet/type/registry_value' + +describe Puppet::Type.type(:registry_value) do + let (:catalog) do Puppet::Resource::Catalog.new end + + [:ensure, :type, :data].each do |property| + it "should have a #{property} property" do + described_class.attrclass(property).ancestors.should be_include(Puppet::Property) + end + + it "should have documentation for its #{property} property" do + described_class.attrclass(property).doc.should be_instance_of(String) + end + end + + describe "path parameter" do + it "should have a path parameter" do + Puppet::Type.type(:registry_key).attrtype(:path).should == :param + end + + %w[hklm\propname hklm\software\propname].each do |path| + it "should accept #{path}" do + described_class.new(:path => path, :catalog => catalog) + end + end + + %w[hklm\\ hklm\software\\ hklm\software\vendor\\].each do |path| + it "should accept the unnamed (default) value: #{path}" do + described_class.new(:path => path, :catalog => catalog) + end + end + + it "should strip trailling slashes from unnamed values" do + described_class.new(:path => 'hklm\\software\\\\', :catalog => catalog) + end + + %w[HKEY_DYN_DATA\\ HKEY_PERFORMANCE_DATA\name].each do |path| + it "should reject #{path} as unsupported" do + expect { described_class.new(:path => path, :catalog => catalog) }.to raise_error(Puppet::Error, /Unsupported/) + end + end + + %w[hklm hkcr unknown\\name unknown\\subkey\\name].each do |path| + it "should reject #{path} as invalid" do + pending 'wrong message' + expect { described_class.new(:path => path, :catalog => catalog) }.should raise_error(Puppet::Error, /Invalid registry key/) + end + end + + %w[HKLM\\name HKEY_LOCAL_MACHINE\\name hklm\\name].each do |root| + it "should canonicalize root key #{root}" do + value = described_class.new(:path => root, :catalog => catalog) + value[:path].should == 'hklm\name' + end + end + + it 'should validate the length of the value name' + it 'should validate the length of the value data' + it 'should be case-preserving' + it 'should be case-insensitive' + it 'should autorequire ancestor keys' + it 'should support 32-bit values' do + value = described_class.new(:path => '32:hklm\software\foo', :catalog => catalog) + end + end + + describe "type property" do + let (:value) { described_class.new(:path => 'hklm\software\foo', :catalog => catalog) } + + [:string, :array, :dword, :qword, :binary, :expand].each do |type| + it "should support a #{type.to_s} type" do + value[:type] = type + value[:type].should == type + end + end + + it "should reject other types" do + expect { value[:type] = 'REG_SZ' }.to raise_error(Puppet::Error) + end + end + + describe "data property" do + let (:value) { described_class.new(:path => 'hklm\software\foo', :catalog => catalog) } + + context "string data" do + ['', 'foobar'].each do |data| + it "should accept '#{data}'" do + value[:type] = :string + value[:data] = data + end + end + + pending "it should accept nil" + end + + context "integer data" do + [:dword, :qword].each do |type| + context "for #{type}" do + [0, 0xFFFFFFFF, -1, 42].each do |data| + it "should accept #{data}" do + value[:type] = type + value[:data] = data + end + end + + %w['foobar' :true].each do |data| + it "should reject #{data}" do + value[:type] = type + expect { value[:data] = data }.to raise_error(Puppet::Error) + end + end + end + end + + context "for 64-bit integers" do + let :data do 0xFFFFFFFFFFFFFFFF end + + it "should accept qwords" do + value[:type] = :qword + value[:data] = data + end + + it "should reject dwords" do + value[:type] = :dword + expect { value[:data] = data }.to raise_error(Puppet::Error) + end + end + end + + context "binary data" do + ['', 'CA FE BE EF', 'DEADBEEF'].each do |data| + it "should accept '#{data}'" do + value[:type] = :binary + value[:data] = data + end + end + [9,'1','A'].each do |data| + it "should accept '#{data}' and have a leading zero" do + value[:type] = :binary + value[:data] = data + end + end + + ["\040\040", 'foobar', :true].each do |data| + it "should reject '#{data}'" do + value[:type] = :binary + expect { value[:data] = data }.to raise_error(Puppet::Error) + end + end + end + + context "array data" do + it "should support array data" do + value[:type] = :array + value[:data] = ['foo', 'bar', 'baz'] + end + end + end +end diff --git a/modules/utilities/windows/registry/puppetlabs_registry_library/spec/watchr.rb b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/watchr.rb new file mode 100644 index 000000000..5eff19b43 --- /dev/null +++ b/modules/utilities/windows/registry/puppetlabs_registry_library/spec/watchr.rb @@ -0,0 +1,85 @@ +ENV['AUTOTEST'] = 'true' +ENV['WATCHR'] = '1' + +system 'clear' + +def growl(message) + growlnotify = `which growlnotify`.chomp + title = "Watchr Test Results" + image = case message + when /(\d+)\s+?(failure|error)/i + ($1.to_i == 0) ? "~/.watchr_images/passed.png" : "~/.watchr_images/failed.png" + else + '~/.watchr_images/unknown.png' + end + options = "-w -n Watchr --image '#{File.expand_path(image)}' -m '#{message}' '#{title}'" + system %(#{growlnotify} #{options} &) +end + +def run(cmd) + puts(cmd) + `#{cmd}` +end + +def run_spec_test(file) + if File.exist? file + result = run "rspec --format d --color #{file}" + growl result.split("\n").last + puts result + else + puts "FIXME: No test #{file} [#{Time.now}]" + end +end + +def filter_rspec(data) + data.split("\n").find_all do |l| + l =~ /^(\d+)\s+exampl\w+.*?(\d+).*?failur\w+.*?(\d+).*?pending/ + end.join("\n") +end + +def run_all_tests + system('clear') + files = Dir.glob("spec/**/*_spec.rb").join(" ") + result = run "rspec --format d --color #{files}" + growl_results = filter_rspec result + growl growl_results + puts result + puts "GROWL: #{growl_results}" +end + +# Ctrl-\ +Signal.trap 'QUIT' do + puts " --- Running all tests ---\n\n" + run_all_tests +end + +@interrupted = false + +# Ctrl-C +Signal.trap 'INT' do + if @interrupted then + @wants_to_quit = true + abort("\n") + else + puts "Interrupt a second time to quit" + @interrupted = true + Kernel.sleep 1.5 + # raise Interrupt, nil # let the run loop catch it + run_suite + end +end + +def file2spec(file) + result = file.sub('lib/puppet/', 'spec/unit/puppet/').gsub(/\.rb$/, '_spec.rb') + result = file.sub('lib/facter/', 'spec/unit/facter/').gsub(/\.rb$/, '_spec.rb') +end + + +watch( 'spec/.*_spec\.rb' ) do |md| + #run_spec_test(md[0]) + run_all_tests +end +watch( 'lib/.*\.rb' ) do |md| + # run_spec_test(file2spec(md[0])) + run_all_tests +end diff --git a/scenarios/simple_examples/forensic_examples/simple_registry_example.xml b/scenarios/simple_examples/forensic_examples/simple_registry_example.xml new file mode 100644 index 000000000..99868efce --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/simple_registry_example.xml @@ -0,0 +1,37 @@ + + + + + + + windows_server + + + + + + + HKLM\System\CurrentControlSet\Services\Example1 + + + + + + HKLM\System\CurrentControlSet\Services\Example2 + + + + string + + + + String to demonstrate the module + + + + + + + From 7e26b365af503ea5289553e793cff68ef0765ea6 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Thu, 20 Apr 2017 23:59:12 +0100 Subject: [PATCH 18/24] New encoding module example. Encoding generators for hashes added: String input: MD5, SHA1, SHA256, SHA384, SHA512 File (path) input: MD5, SHA1 --- .../hash/md5_file/manifests/.no_puppet | 0 modules/encoders/hash/md5_file/md5_file.pp | 0 .../hash/md5_file/secgen_local/local.rb | 17 +++++++ .../hash/md5_file/secgen_metadata.xml | 17 +++++++ .../hash/md5_string/manifests/.no_puppet | 0 .../encoders/hash/md5_string/md5_string.pp | 0 .../hash/md5_string/secgen_local/local.rb | 17 +++++++ .../hash/md5_string/secgen_metadata.xml | 17 +++++++ .../hash/sha1_file/manifests/.no_puppet | 0 .../hash/sha1_file/secgen_local/local.rb | 17 +++++++ .../hash/sha1_file/secgen_metadata.xml | 17 +++++++ modules/encoders/hash/sha1_file/sha1_file.pp | 0 .../hash/sha1_string/manifests/.no_puppet | 0 .../hash/sha1_string/secgen_local/local.rb | 17 +++++++ .../hash/sha1_string/secgen_metadata.xml | 17 +++++++ .../encoders/hash/sha1_string/sha1_string.pp | 0 .../hash/sha256_string/manifests/.no_puppet | 0 .../hash/sha256_string/secgen_local/local.rb | 17 +++++++ .../hash/sha256_string/secgen_metadata.xml | 17 +++++++ .../hash/sha256_string/sha256_string.pp | 0 .../hash/sha384_string/manifests/.no_puppet | 0 .../hash/sha384_string/secgen_local/local.rb | 17 +++++++ .../hash/sha384_string/secgen_metadata.xml | 17 +++++++ .../hash/sha384_string/sha384_string.pp | 0 .../hash/sha512_string/manifests/.no_puppet | 0 .../hash/sha512_string/secgen_local/local.rb | 17 +++++++ .../hash/sha512_string/secgen_metadata.xml | 17 +++++++ .../hash/sha512_string/sha512_string.pp | 0 .../simple_encoders_example_scenario.xml | 48 +++++++++++++++++++ scenarios/windows_scenario.xml | 42 +++++++++++++++- 30 files changed, 326 insertions(+), 2 deletions(-) create mode 100644 modules/encoders/hash/md5_file/manifests/.no_puppet create mode 100644 modules/encoders/hash/md5_file/md5_file.pp create mode 100644 modules/encoders/hash/md5_file/secgen_local/local.rb create mode 100644 modules/encoders/hash/md5_file/secgen_metadata.xml create mode 100644 modules/encoders/hash/md5_string/manifests/.no_puppet create mode 100644 modules/encoders/hash/md5_string/md5_string.pp create mode 100644 modules/encoders/hash/md5_string/secgen_local/local.rb create mode 100644 modules/encoders/hash/md5_string/secgen_metadata.xml create mode 100644 modules/encoders/hash/sha1_file/manifests/.no_puppet create mode 100644 modules/encoders/hash/sha1_file/secgen_local/local.rb create mode 100644 modules/encoders/hash/sha1_file/secgen_metadata.xml create mode 100644 modules/encoders/hash/sha1_file/sha1_file.pp create mode 100644 modules/encoders/hash/sha1_string/manifests/.no_puppet create mode 100644 modules/encoders/hash/sha1_string/secgen_local/local.rb create mode 100644 modules/encoders/hash/sha1_string/secgen_metadata.xml create mode 100644 modules/encoders/hash/sha1_string/sha1_string.pp create mode 100644 modules/encoders/hash/sha256_string/manifests/.no_puppet create mode 100644 modules/encoders/hash/sha256_string/secgen_local/local.rb create mode 100644 modules/encoders/hash/sha256_string/secgen_metadata.xml create mode 100644 modules/encoders/hash/sha256_string/sha256_string.pp create mode 100644 modules/encoders/hash/sha384_string/manifests/.no_puppet create mode 100644 modules/encoders/hash/sha384_string/secgen_local/local.rb create mode 100644 modules/encoders/hash/sha384_string/secgen_metadata.xml create mode 100644 modules/encoders/hash/sha384_string/sha384_string.pp create mode 100644 modules/encoders/hash/sha512_string/manifests/.no_puppet create mode 100644 modules/encoders/hash/sha512_string/secgen_local/local.rb create mode 100644 modules/encoders/hash/sha512_string/secgen_metadata.xml create mode 100644 modules/encoders/hash/sha512_string/sha512_string.pp create mode 100644 scenarios/simple_examples/forensic_examples/simple_encoders_example_scenario.xml diff --git a/modules/encoders/hash/md5_file/manifests/.no_puppet b/modules/encoders/hash/md5_file/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/md5_file/md5_file.pp b/modules/encoders/hash/md5_file/md5_file.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/md5_file/secgen_local/local.rb b/modules/encoders/hash/md5_file/secgen_local/local.rb new file mode 100644 index 000000000..4d97cb99d --- /dev/null +++ b/modules/encoders/hash/md5_file/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class MD5Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(file_path) + Digest::MD5.file file_path + end + +end + +MD5Encoder.new.run diff --git a/modules/encoders/hash/md5_file/secgen_metadata.xml b/modules/encoders/hash/md5_file/secgen_metadata.xml new file mode 100644 index 000000000..16ab03493 --- /dev/null +++ b/modules/encoders/hash/md5_file/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + MD5 hash encoder file + Jason Keighley + Apache v2 + MD5 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/md5_string/manifests/.no_puppet b/modules/encoders/hash/md5_string/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/md5_string/md5_string.pp b/modules/encoders/hash/md5_string/md5_string.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/md5_string/secgen_local/local.rb b/modules/encoders/hash/md5_string/secgen_local/local.rb new file mode 100644 index 000000000..4d73161fa --- /dev/null +++ b/modules/encoders/hash/md5_string/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class MD5Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(str) + Digest::MD5.base64digest str + end + +end + +MD5Encoder.new.run diff --git a/modules/encoders/hash/md5_string/secgen_metadata.xml b/modules/encoders/hash/md5_string/secgen_metadata.xml new file mode 100644 index 000000000..1d03453cd --- /dev/null +++ b/modules/encoders/hash/md5_string/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + MD5 hash encoder string + Jason Keighley + Apache v2 + MD5 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/sha1_file/manifests/.no_puppet b/modules/encoders/hash/sha1_file/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha1_file/secgen_local/local.rb b/modules/encoders/hash/sha1_file/secgen_local/local.rb new file mode 100644 index 000000000..cd8df4d70 --- /dev/null +++ b/modules/encoders/hash/sha1_file/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class MD5Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(file_path) + Digest::SHA1.file file_path + end + +end + +MD5Encoder.new.run diff --git a/modules/encoders/hash/sha1_file/secgen_metadata.xml b/modules/encoders/hash/sha1_file/secgen_metadata.xml new file mode 100644 index 000000000..206508bfa --- /dev/null +++ b/modules/encoders/hash/sha1_file/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + SHA1 hash encoder file + Jason Keighley + Apache v2 + SHA1 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/sha1_file/sha1_file.pp b/modules/encoders/hash/sha1_file/sha1_file.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha1_string/manifests/.no_puppet b/modules/encoders/hash/sha1_string/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha1_string/secgen_local/local.rb b/modules/encoders/hash/sha1_string/secgen_local/local.rb new file mode 100644 index 000000000..0cfb7de4e --- /dev/null +++ b/modules/encoders/hash/sha1_string/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class SHA1Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(str) + Digest::SHA1.base64digest str + end + +end + +SHA1Encoder.new.run diff --git a/modules/encoders/hash/sha1_string/secgen_metadata.xml b/modules/encoders/hash/sha1_string/secgen_metadata.xml new file mode 100644 index 000000000..facabd2e8 --- /dev/null +++ b/modules/encoders/hash/sha1_string/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + SHA1 hash encoder string + Jason Keighley + Apache v2 + SHA1 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/sha1_string/sha1_string.pp b/modules/encoders/hash/sha1_string/sha1_string.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha256_string/manifests/.no_puppet b/modules/encoders/hash/sha256_string/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha256_string/secgen_local/local.rb b/modules/encoders/hash/sha256_string/secgen_local/local.rb new file mode 100644 index 000000000..44a1acc5b --- /dev/null +++ b/modules/encoders/hash/sha256_string/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class SHA256Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(str) + Digest::SHA256.base64digest str + end + +end + +SHA256Encoder.new.run diff --git a/modules/encoders/hash/sha256_string/secgen_metadata.xml b/modules/encoders/hash/sha256_string/secgen_metadata.xml new file mode 100644 index 000000000..d70d4f0be --- /dev/null +++ b/modules/encoders/hash/sha256_string/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + SHA256 hash encoder string + Jason Keighley + Apache v2 + SHA256 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/sha256_string/sha256_string.pp b/modules/encoders/hash/sha256_string/sha256_string.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha384_string/manifests/.no_puppet b/modules/encoders/hash/sha384_string/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha384_string/secgen_local/local.rb b/modules/encoders/hash/sha384_string/secgen_local/local.rb new file mode 100644 index 000000000..72398f9cc --- /dev/null +++ b/modules/encoders/hash/sha384_string/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class SHA256Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(str) + Digest::SHA384.base64digest str + end + +end + +SHA256Encoder.new.run diff --git a/modules/encoders/hash/sha384_string/secgen_metadata.xml b/modules/encoders/hash/sha384_string/secgen_metadata.xml new file mode 100644 index 000000000..76da3e080 --- /dev/null +++ b/modules/encoders/hash/sha384_string/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + SHA384 hash encoder string + Jason Keighley + Apache v2 + SHA384 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/sha384_string/sha384_string.pp b/modules/encoders/hash/sha384_string/sha384_string.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha512_string/manifests/.no_puppet b/modules/encoders/hash/sha512_string/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/encoders/hash/sha512_string/secgen_local/local.rb b/modules/encoders/hash/sha512_string/secgen_local/local.rb new file mode 100644 index 000000000..743e66f64 --- /dev/null +++ b/modules/encoders/hash/sha512_string/secgen_local/local.rb @@ -0,0 +1,17 @@ +#!/usr/bin/ruby +require 'fileutils' +require 'digest' +require_relative '../../../../../lib/objects/local_string_encoder.rb' +class SHA512Encoder < StringEncoder + def initialize + super + self.module_name = 'MD5 hash' + end + + def encode(str) + Digest::SHA2.new(512).base64digest str + end + +end + +SHA512Encoder.new.run diff --git a/modules/encoders/hash/sha512_string/secgen_metadata.xml b/modules/encoders/hash/sha512_string/secgen_metadata.xml new file mode 100644 index 000000000..2a8c10c8f --- /dev/null +++ b/modules/encoders/hash/sha512_string/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + SHA512 hash encoder string + Jason Keighley + Apache v2 + SHA512 hash encoder module + + hash + windows + + strings_to_encode + + hash + diff --git a/modules/encoders/hash/sha512_string/sha512_string.pp b/modules/encoders/hash/sha512_string/sha512_string.pp new file mode 100644 index 000000000..e69de29bb diff --git a/scenarios/simple_examples/forensic_examples/simple_encoders_example_scenario.xml b/scenarios/simple_examples/forensic_examples/simple_encoders_example_scenario.xml new file mode 100644 index 000000000..3f495bcac --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/simple_encoders_example_scenario.xml @@ -0,0 +1,48 @@ + + + + + + + windows_server + + + + + C:\Users\vagrant\Desktop\Hash_file + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + + + + + diff --git a/scenarios/windows_scenario.xml b/scenarios/windows_scenario.xml index d0cfbe195..95c45ac4f 100644 --- a/scenarios/windows_scenario.xml +++ b/scenarios/windows_scenario.xml @@ -6,10 +6,48 @@ - storage_server + windows_server - + + + + + HKLM\System\CurrentControlSet\Services\Puppet + + + + string + + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + + String to demonstrate the module + + + + From 9383eef84b680f6090d01658c14d544624673d39 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Sun, 23 Apr 2017 00:28:56 +0100 Subject: [PATCH 19/24] Added VM configuration options to help so machines can be provisioned to a faster extent if the host computer is powerful enough. --- secgen.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/secgen.rb b/secgen.rb index 43c527e2d..58764f4fc 100644 --- a/secgen.rb +++ b/secgen.rb @@ -24,6 +24,10 @@ def usage --nopae: disable PAE support --hwvirtex: enable HW virtex support --vtxvpid: enable VTX support + --memory-per-vm: set amount of memory to give each VM + --total-memory: set total amount of memory to give all VMs + --max-cpu-cores: set total amount of cpu cores for each VM + --max-cpu-usage: set total amount of cpu usage (percentage) COMMANDS: run, r: Builds project and then builds the VMs From fe2a879fb0357973fc1f40cbef46b4429d6ded64 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Mon, 11 Dec 2017 12:30:12 +0000 Subject: [PATCH 20/24] Made alterations to the multiple windows module example scenario --- .../multiple_module_example.xml | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scenarios/simple_examples/forensic_examples/multiple_module_example.xml b/scenarios/simple_examples/forensic_examples/multiple_module_example.xml index 07cb9777c..2e640d675 100644 --- a/scenarios/simple_examples/forensic_examples/multiple_module_example.xml +++ b/scenarios/simple_examples/forensic_examples/multiple_module_example.xml @@ -4,9 +4,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.github/cliffe/SecGen/scenario"> - - storage_server + windows_box @@ -43,22 +42,29 @@ - C:\Users\vagrant\Desktop\Hello.jpg + + C:\Users\vagrant\Desktop\Illegal_image.jpg + + + + + + C:\Secret - C:\Users\vagrant\Desktop\Hello.txt + C:\Users\vagrant\Desktop\Crime.txt - File contents + I robbed a bank last week, got about 1 mil. I think I'm set now unless the rozzers find me. - C:\Users\vagrant\Desktop\Hello.txt + C:\Users\vagrant\Desktop\Crime.txt @@ -74,7 +80,7 @@ - C:\Users\vagrant\Desktop\Hello.txt + C:\Users\vagrant\Desktop\Crime.txt @@ -90,7 +96,7 @@ - C:\Users\vagrant\Desktop\Hello.txt + C:\Users\vagrant\Desktop\Crime.txt From a5571d6f0e2242f97eef7cfc3159ee4c290269f9 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Mon, 11 Dec 2017 12:33:07 +0000 Subject: [PATCH 21/24] Fixed incorrect class naming --- .../illegal_images/select_cat_image/secgen_local/local.rb | 4 ++-- .../select_cat_image_path/secgen_local/local.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb b/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb index 4ef69e420..1b4a14414 100644 --- a/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb +++ b/modules/generators/forensics/illegal_images/select_cat_image/secgen_local/local.rb @@ -2,7 +2,7 @@ require_relative '../../../../../../lib/objects/local_string_generator.rb' require 'date' -class GenerateRandomDate < StringGenerator +class SelectCatImage < StringGenerator attr_accessor :selected_image_path def initialize @@ -29,4 +29,4 @@ def generate end end -GenerateRandomDate.new.run \ No newline at end of file +SelectCatImage.new.run \ No newline at end of file diff --git a/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb index 9bbc86a9e..35ed55081 100644 --- a/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb +++ b/modules/generators/forensics/illegal_images/select_cat_image_path/secgen_local/local.rb @@ -2,7 +2,7 @@ require_relative '../../../../../../lib/objects/local_string_generator.rb' require 'date' -class GenerateRandomDate < StringGenerator +class SelectCatImagePath < StringGenerator attr_accessor :selected_image_path def initialize @@ -29,4 +29,4 @@ def generate end end -GenerateRandomDate.new.run \ No newline at end of file +SelectCatImagePath.new.run \ No newline at end of file From b287d67cbbaac2991d13c775fb4b50a8ab387c33 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Wed, 13 Dec 2017 17:22:57 +0000 Subject: [PATCH 22/24] Prefetch file insert module. Need to check that all files are necessary, some may be redundant/unused code. --- lib/helpers/constants.rb | 1 + .../prefetch/CMD.EXE-087B4001.pf | Bin 0 -> 11778 bytes .../prefetch/EXPLORER.EXE-082F38A9.pf | Bin 0 -> 95952 bytes .../prefetch/FIREFOX.EXE-28641590.pf | Bin 0 -> 132810 bytes .../insert_prefetch_file.pp | 7 ++++ .../insert_prefetch_file/manifests/init.pp | 13 +++++++ .../insert_prefetch_file/secgen_metadata.xml | 32 +++++++++++++++++ .../select_prefetch_file/manifests/.no_puppet | 0 .../secgen_local/local.rb | 19 ++++++++++ .../select_prefetch_file/secgen_metadata.xml | 18 ++++++++++ .../select_prefetch_file.pp | 0 .../enable_prefetch/enable_prefetch.pp | 1 + .../enable_prefetch/manifests/init.pp | 33 ++++++++++++++++++ .../enable_prefetch/secgen_metadata.xml | 17 +++++++++ .../place_prefetch_file/manifests/init.pp | 5 +++ .../place_prefetch_file.pp | 1 + .../place_prefetch_file/secgen_metadata.xml | 17 +++++++++ .../simple_prefetch_example.xml | 18 ++++++++++ 18 files changed, 182 insertions(+) create mode 100644 lib/resources/forensic_artefacts/prefetch/CMD.EXE-087B4001.pf create mode 100644 lib/resources/forensic_artefacts/prefetch/EXPLORER.EXE-082F38A9.pf create mode 100644 lib/resources/forensic_artefacts/prefetch/FIREFOX.EXE-28641590.pf create mode 100644 modules/forensics/windows/prefetch/insert_prefetch_file/insert_prefetch_file.pp create mode 100644 modules/forensics/windows/prefetch/insert_prefetch_file/manifests/init.pp create mode 100644 modules/forensics/windows/prefetch/insert_prefetch_file/secgen_metadata.xml create mode 100644 modules/generators/forensics/prefetch/select_prefetch_file/manifests/.no_puppet create mode 100644 modules/generators/forensics/prefetch/select_prefetch_file/secgen_local/local.rb create mode 100644 modules/generators/forensics/prefetch/select_prefetch_file/secgen_metadata.xml create mode 100644 modules/generators/forensics/prefetch/select_prefetch_file/select_prefetch_file.pp create mode 100644 modules/utilities/windows/prefetch/enable_prefetch/enable_prefetch.pp create mode 100644 modules/utilities/windows/prefetch/enable_prefetch/manifests/init.pp create mode 100644 modules/utilities/windows/prefetch/enable_prefetch/secgen_metadata.xml create mode 100644 modules/utilities/windows/prefetch/place_prefetch_file/manifests/init.pp create mode 100644 modules/utilities/windows/prefetch/place_prefetch_file/place_prefetch_file.pp create mode 100644 modules/utilities/windows/prefetch/place_prefetch_file/secgen_metadata.xml create mode 100644 scenarios/simple_examples/forensic_examples/simple_prefetch_example.xml diff --git a/lib/helpers/constants.rb b/lib/helpers/constants.rb index 518df596c..9f07e3543 100644 --- a/lib/helpers/constants.rb +++ b/lib/helpers/constants.rb @@ -47,6 +47,7 @@ URLLISTS_DIR = "#{ROOT_DIR}/lib/resources/urllists" INTERNET_BROWSER_FILES_DIR = "#{ROOT_DIR}/lib/resources/internet_browser_files" ILLEGAL_IMAGES_DIR = "#{ROOT_DIR}/lib/resources/illegal_images" +FORENSIC_ARTEFACTS_DIR = "#{ROOT_DIR}/lib/resources/forensic_artefacts" # Path to secgen_functions puppet module SECGEN_FUNCTIONS_PUPPET_DIR = "#{MODULES_DIR}build/puppet/secgen_functions" diff --git a/lib/resources/forensic_artefacts/prefetch/CMD.EXE-087B4001.pf b/lib/resources/forensic_artefacts/prefetch/CMD.EXE-087B4001.pf new file mode 100644 index 0000000000000000000000000000000000000000..cda0ea11f58699b4a232dba0abd14893c360b6f6 GIT binary patch literal 11778 zcmdVg3z$_^{l@XdbYK8Y5l@PV3VISz84*WDjg(;y49E-+b6`MGtWMg+ifS}dGptFw zP*F{$X+?H2^Oosk=9OylUa6gKre->onR%2ZGlE0!cfD(5$mjY0<8h|vX~SlJ_V28{ z*4k_R_T>z-O9-K@si|>iZENa6XbP<%AL_#FuuN+R*B-KDwKieo$mh;w%Q8mm%tv;8 zAuPJ`x^WNh`oh}b&z;Nuu$gC#h9d-Axu;#pGg+l*U#exot%b{sIP1_o+?6aP%^>D4k-l$d8g$K3PYt3jqMJsj8 zP-BOs*xS;!q!`Ci?wwjWW{fJL9Q9qQbuX=FXlH7Q-LCCetq%<-_ki|y(mG2G_4n_$ z+GlJoSB!e8ucDQ5W7S}P|2AuXPpxMwMqfr~9jAT~dosm1_Atd5(1K2|zkjb)tX8pG z6zgwemSQ_9w!bRs@5=(knA0@_VkamzTCoj^^^e~viqXc2s;|^Qr@^^|cemM<9|Cnw{<<=^;P@mDp<`kQ%jxS0vh2pYtOGE4v z1vhS4={(HhhInkq;pUicba3P5N|pZTf{A8cLV+^v@q9yv<2uC3blK{D2ql~O z3*GXTNpVa=4{nIL6UsL88G3E#p_{m0F5yhF+H@x;6FDe?LVJ$PlznNYU5Cxl*E<2C{J*%NW5 zCLX&l&f2-4fMj`w(2JRlGLd8$vfeCT!6XdT>L`Wr|_0S?5wi%;%s{ozQT5uRzF{hJ6&c5?!pb>b-3jC(YPB|s>k3SdnPX1 zjkwpIh5K-Wo&{Pm%v}TB(!AjI_*`&aRjPA{*SkzJ&f9Zw!R6=SB5n@H;trRYk2__Z zOAGFD`Bq$Vd;#vZxrUurl`7Y6%q5-I>v5Xv#hAI){I=ne^Pzytt*#h=IZ_S+lgzT z%&B0i{SG`6r#ctm_V6y^TIlmCka5nu8)xnJ;GF$lTraEL4qS5lG~Dg@>A1&!A1=Gh z8MxPeKaM$5sh){b&YXo~&Qz*rV{%60{{c+S$mif5=g$Xm*^Nyc~3-gRFEz5ti)FW^4=LQFnu?k>Vb=k^zI z2W}4+<4))KCAbSWhcDrh<6p+zF8>w$IIdLJ;SKgzan0};kNG(oH;1ng&yWM{q>gh; z{rNi1IcF}z_4YS#-o6|c>~G?teFg5cuf!S{&haYTV_%Ik&W&p@d7?gi3&%WZ2-jl0 zU~ufW@s6IO>+pE{dVH|mjc3^3!Drhy;5GJl@g??+_+k5dc)aFCZQX|1b) zeJfsJ--b`M>aoU=7FFOKt%aNhAda2n@3aZz#g^Df-s zGI!%n`ySk7--}E3eOT8l`Tt|wtA46KKf!tOq&EB%mz^i~;}-_xOon@GL;N1VNBCzT z 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%0DS000d{al~N;wTQY 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⁣JCO0PKZ0OX8LApm-HLBu>GB>2_LP9R$b3y>MLI2Pee+a8kS)PKnpRY4KV( zBVGq*#p~gm_+U6MJ_Ig^2jHUkP`D(19Xu%B01t`NaIlSw|LftH_ziGe{6;t-J`7HZ z-vp<`Z-&$2x4;?k;c!-b1e_Be3FpN}!3FWra8Y~=ToNA(4~pLk4~a8yu&s;#+u)e^ zI5;kTJDd<74=2U%fKy^8)6(J-u(uDo@h8G2hvAc8lf&@Iu*qTg6xd`t{4V%-@u~2A z;*Id5Vi(rqA-1omKd0fwb6CbY_V#Cl+?)xUJ}U2?1)B~kd^VgHp97l?D)#S%i(=7dt-_ z;*U7@^7CT2Aie}HioxSEMVP#?kmX z{PFIuhfQ~mcW-d^&vJYt920*Lj*D-C6JqCoQhc*}S8@0?sM+NKXUHHKX&fL zKY>$X7jFDC_ii}fevTXa5QksDg{=;pYRm@%$hh zm;G;>Vi)H`h|L-PKjPjM|IxV@KME(rkHJat<8Vss{7i?~ zzTz&#aQ-}jd+aT&Kf`hHlW->VZcaR44S%s6Nb3$d%p3@o2JLo@Sf~B#4*G)S_RXrG zCs-Y54L(J`1PU%BkOu4w059!)mu& zTVrXm-=T`Uqy9*4T~<=jm(gmgZ?xU0 zTkQ%9XN|87QA!8oEWy?N*Qb zCndk=r8=w*tc|qQAaB-N9!8%gYIM8(t+bxFc{QN62d|_kYKcSt-9Jfv!x~q=`%J>E zvK)*u|1?HooJ8uP52@d3AW}8ju)1x8*B*3Pnvg`Ph||?$eG#%Q>Rtl5${2DoMv?2R zgb0nXbd_w%~XQLA4o9k>NYJRZ6 za*Y`oV`JtrOPuVho}0GW_p8j^3McPpx+mtQyu+MlgZVQr*x&vZ8O^mmb*}yEuzyR8 zJMDj5wArM*&E}W$&4lLJzghOY!D2GgMiy(0=9pcldv~$LZjQxLA(I=;mMhh1tVpjm zOJ}>4Z@0yJv8ArR39YbpWKfygJ@_Qc8yt$o=7ZhLPM#?)JF6N6Krg z6gNM3YPa{b4zki}I1-uiAsx+-`L%ha*rBHo{(F${}xy(mFkJNsFahTT`r`Ju2zMm@kVQ}16Jpj z*gvf*A}jMNX!v-cKQZc=&+tWT=$hvu7G@_M0tM~M$uyDzVS zD8zbYXyZA~T&S7z@Otu_cF|+GSzmt7t;ubA`-JuV3*G2pz_q&`XV<)XRVx*HtxVf3 zH&>eUjWQhS4zq|ZtAo+5uXk8Ttfof0x~Wj2-igPjxPD=TXgm#@QROKlyf%p;qd zYwDL)Sy}X2>`BLfwVCpql_&OsR$Be55F<~Gkc#J_k*j#aC zb*ablth{PMOj;=~#~;5|$5jD%IdhT31_Z+gvrex%nS1 zmu{2P%%i$Z%B=qnNgpF?X6#5#t!qnq_HVZIHPYh4rNtia=50ldrKHpPG4)rptMWPr z^ZB?vGq-15cXPF}M$*=0HH}&}HDlizQm&EwAFY*y?OQ|EMUl$#K1o^6t+IYNHS1%z z)WR}v%aH8hnPN0@aeVW*s8@5C%d^7VWfH%<2Z(+YGw+GdgC{nh8L1VHy>}8`CD)$i zz%#Q9c5oQ)vi8FGBW~kGR?qwGS!)UOYn7dxHXC`ZFgFuV>DZ;GJtRh~q^Xf|oLH`g z)5qHNddr1cA6lt@k1}@Q8PUXcS@yR*gic$vZETli?0Q1Y?6Go+TAzxVpqy@G&#bnr z#=mPy3uG=z%iLV`kN8*iaYt_*`er2Q!T55y8!0!k@-ue5VD)C5#TDI$sCOvJ;h?!% z@493oHDj!DSm7i*lDv8KX6zD>=3!Vnq~5$4J1p(0)t~!ghrm85dLYf+>j;mNSVdR= ziWYC9z24X;-)=echR#LQJK_pwGBlqUy-C-O z_S=Xo3XfM;*vOr^nf8MRtk$hC?GMe(s-LQ7s9|?n;X})?-u>EU$Eds`6itk3XNPes zn~E0IXgXD8ohFULS&64aqlqKy;VVpfpz9*4Kb*1qF745l=NI+Xf{BgpMw;1I);GgD zsnxZMm9pv-#jW;tJ8iSkzlodV(x|@8T(7jzdE@hWR>&1&_A%MV7zj?W@>*a#*Zx># zXgA&Ex%NLAX3%V>#Wd4Bqn$R>Y&xCGCH4(kWwV^jO9ZF7boPYeEsfBL$9rPsf@{rw z7^$_67HcD^7`a|;p8d^{>OK7_8cdXMWlcD&4O}a;>Rr)7!>ryFt>%|OZQI^FrBYq% zN3JimaUE^_dn%=&lGfJPIY;VL;+P>pr*HJNnpS8Tt+mET zeIGr)qF!TcM52!s>nK*_CKlh3`r7fzmf9Y7_$*;87v-HMR)aY?#OW$d6+}G(-QPmt zbVB(o%1lRd?0*sVBaz%Q51M7;ki~8UDoLu5r)I6KKRXz!Txf5kD14@|x@_Z?`gJZ3 z&`+F-<+jd`Cd+FLTSlpI@~J#apPJcVp*zfeb<{Jz%`GjNPcN}_>fHO%DcbUs zib~mS87)tH5-Y9r(CD0+ZEu;2{gzj9?2oa|v&vS*re?{Z%fmTSq+}!Kj;7Y=GG(b< zYpsow?yD^I(q@{9qZL-a%H>!8I`>p7z16l3$C{tC2YRPy+e=y7TV!e2Qp#5H_t;)gd1JR7gpdI!!Q=5Rj#r6-EOAVf0x;J?3YpF zquohr<~*JmFK55ElvWxp?~!Kv+{l4G1M~h~s|~u*=U~^HPNj0Cwo;e3tdY-Hr5oPO zugGcki7DqU>#=mdUhU01^_TtB8huPG2f3}59AkdR`YH9T5&8F@oLANQ!0ca9=TzU& z7MVJI26GB-CTUc*RBLODbXOC#5gqr++I;=})YzdhXW|Yjw7xproet6I?4n=B-MGsm z*JLB_R$02r_vr@UWBD#>eEgfi(p<~j#azaB$y+W1KYxd1~D<7`L+0DtNCeGx}QcZ@_e6bvlh!4xM7!f%8qO%(rq_V!1un z_VlpCEb-Xa`YUmc9nlaqtaVQb7if)SFM>m;C5RRmJ;163e7RC zJN>@KaM#hKlk?`xH5$2Tr+ICll4oJfn$4q?G17ih^qO$|N(M7RZsSpnqc5XPO_pBV z=rI_-=F{dXvC;Bw_?au+X2uCyV%lr9zuW$=+?z^^WoD+^7HOBWTw9&2-LmnggYkT) zn`5su8A^=j*&k+W#+4NTMi=w~71`N}v84@49x+bXU~y)g(uy(VK09u;L(7}>!lhQ1 zo2^YA9uw%49K8mb>g5qZ^G0vAVX=3_J_NsSFnQslx|>REqbEMXvQqPjvaIAR(@5tWs$I3B{AXW?ITt%Z z>#PT;^s8~}XElbwoU9Q+?Y)WBOoLU3YBW++%ng!~JL_M_Vly%VWR# zS8l5%eD!(F@aR&dRtc}(+O(}!K54Ji@&8J3lS90y_BAPN9 zU)ZC~cv?9ZtL56{8_{N*>h?}5L{({^Njgu#ihI!I@a$A2f;LZ=W6>~ zbTn^I3(kF>%=dV6`|b7o?ia|s$C+CN&-eDv@#fPn()VZh_jdC3XZh#uWBGoo;2Iy! zb>6(zo3}f2>)?I<`FwAl;my;%`7Ups?9I!MP`JmOF7xnxWgg+p1JBg+0p2|41U(<= z-OpXY^Ymb*H|O=}`N>`6?rE2ft%L2n{r9@`{WSml4~~r6`wx2aPH*1s-CggWulDAZ z-n`74mwNMJZ(iih`|;Mw<6GXJH3#!NJ?Qu5a&Ip6=8pcoTfO^-m&yH4y?Kkb|Ef2$ z-u@llJnFf)-zqrLKlgcaxi|On=I-8%do$+EWn1(9v|wBR{8Ts3(}Tynd4mu4CjWe` zf8N%|e}lI_)SHKRbG!0`b<{sXRd$WC8#p_b<{+^|Z*F6{(-!J*+ z8@zeFH?Q^P72dqSzki;8ey=yr_GZ?b8@+joHxKsafH!-*dAIj(=NBs;zd2jxN&Cut z@DQ2SwCyt^xXqQ%wBUAc?%>bjfyDt7gu+s!3FyX#(!tYG14-9NKZ({v|?&9J%Vc5~tD&VGW>l zY(IP@JO~b-f+<2O4%XzBEfv?8I~zlNuy ze5ZaZO0n?oPz2UiTDLc-$Yt$D2DTm+ZEWdFD)%o3HZGma@HV~w6`1x}>%?X}W8kRA zU)7~G`l?`HEBBL`bEC&Z--K_c39QDoQrxr;6aGblAb9np#;|(DG_W=3$;_XM%&8tV zM?KH1uFE6GVx;l%KJq8hQ( zSHCOutvmwQYr^rYF74_)@E~}3Q~j&#s|BW=G0_~|R7}fr8#MBVr(xo&3-yImyLyB^ z3R?TfY+@0ZCT%lQJyM9_6PARSoa7M0;p-(ZViR2 z-?IsV{hHuQxE^dY4QO(uvGz6oU)u?SiRXjHFzanys(d?hV6Aqdb!7Nyt{~XAVLvo( z*F9HgL6MUauf4-#MtV65j=V1aW~0sy)l>c>`Oc}G@Gq7J?j+5`daUvITm4Ju^~^XY z9!ll+)|*kn;q?`1a|glR4c8l`ir@UvI_<>w42I`RwU6uUY}9OEC?QF*!``h2_XITxLWj2eQ%?2asM$WVM7MR4~?n~}K-(V@2 zX*+Xkjpmzc{GDh2&vo@F2##uUPK_^J+z8oUce)l>N`|M5``l++x;S~4aIO60NlaOF zS+`kPX=N6fFw;G;--mWCzvfCw%(GJD7W(fF^87y6_w+8W=a1)~Cog&B?YF<{I@dYpI-^P24sg!xyT=~8 zuW8S(RaUrhZlas!_O$1zZl>GXG}Hb$_u+sS&$TE2==@{T`E{$`vgDusw=7xL{?*hD z^8epFC^?*h>+D~3FKaW%xqa4kF1u4NmwU{)nVr3y#cuZ6&b96|uRb~Y)n4w>1>{ZJe?YY`I9UEjjRXMk%`5Mbu26abR zcC|fsSymThe_6JogPPfb!?TVJMZY<+$%NYBv z_J!{3T+7&QcU#uo_DajFnX9(tch_BGSzpWMSk}EwZ?$Yg%i34Tp0R9{W#jC*yT29Y z)hv6^p6l$HvHWYEZO^sV>8?R`l6j!{L3WapY$k66E+x(kAAiDli}hzn%UEC;`=iCO?(KZBWgA)6&vr_8-QAY4{TeOn z-sg{4#`=6}S@$-0(lVBH&;Yfi@24%ZY3r6+#xlsB39>D1W78kmvzBojUuRkOetW|* zwt-u#XFk6Zv`t&Kr+taG%Yv*rqWwnpu4RMm`5Mc**R3O{bAx)u*J;`MR(GUj)Ui(e zY-JfkTTn+f#xm>5jk4pPF|(fgSjM_7vTT|?v)pMxHoVw z*04Eio5Y>lF5kXe6tUk!Lu*{}~TQH<&pN|RZ z2HL=<26eky#__SlvME6}KB%jY+O;CcCIs0Zma!fzcVdv;y_p?fLAIA=wt8;b(4Kw1 zUr_g)Ws`%t=|OhDu%2v2ko{)aUP0Z=AiH#U&-(5kWKla{Ck1r}1ler33hh^Pumss|VZPbNZICnwN-*t8#u*1|X+Op@^y4y1LLBFkf=Fo$dv43Y< z);-rAwTylAsb$^s<*^`Z+gjHvw%_AH_Te@?>-I#DHP$+JuGLWYq-7`Ab81`XCIs0~ zJMe4Fl_Pacq;8yL%<+&Ni2Lwt|0Y^y&D`CVjSI3lmNCCZ*?{YUY;Mr@3CqR?**TV7 zYu?o+U-xp?wvECv+AP~6s9Vo6)_0~&e_UEeacRUyb&UX5j^*mLa;J^fu}S&d~& zEn~T~dBie~g$Xk4H!ZV6*Dc+tr|vt;7|XcP+8?y}KFAhZ);+!- zf^6{4J=u>zcA;hLOWOVvWGhz5eh#vOcIj#ROOU-{nQdy{w!<>|sI{Ao?*9H0)ZJiN zcU{>swoiqfgu3f~3+j$vCHp-HPo5TJ-&kh<`L?GAS<~d6^VJzaw$YTH=0Y%d6qH0 zvx4kM+bEX=*<8yQ=e&KizSK2Ywhq}eZ7Z^CEn}NrzOQpv+B4Y=mhrsAR<=94G05KE zuP3`H$o{gEYIohumT_OV(M;VR)6Y|uF_yjd*M25@E@=ChWee>8$?`!~IG|^{v|7e| znRTFZ=LdC5E#tg<;z7>c9AvKsZSOs}XDqJ;*;|M7?910JV}0K^)H$09ez|W2bq6){ z)V&>K2OQSZUz=r&W!^09bH=^sYjv;QZ1FUVSt(0PsQ z{UCenNZsdIMbbHBGk8LAJJKjOBPM+Aqk~ z4YGCT_iX3&gSJC$r`dG#%iSQzIxXuS-{2q{YzLN2L0`9FkbP&_H9@vfkgaVSp!-gWjZHPH`Frb{fCyNgSuUVy5*Pm^tYR3?BDrU=v++Ov6fZY^I*FG>=U$|XxRXB z>nhm+ma*-VcA}dW)E#8m>gIk6b-tp%V=U`u&v#p9r&K@YQ!L|a^;hY-fK!$+7MJPS zey0a@_gj{*FHv`fWsGmS?X>AZc8+Cz%%N+Xv(uulyCBG(v8;PNuCR<{G+f)Wtrl9w zdi-OVoj!frOwjhR>+C)*$ZiVij=o;kTE=oqkoCKvXFXahV|zSh+4P|83d`6oN8i}9 z9xE+d&9Z?v^=!X>b}{N}*%Ov^&*!x)^V``@?A`NVkY%(D-O|&x#xl0^y_VVK!mq~& z%k0$V8gA`5-o{wQG9oR`9U9b)vy5Z4Xj%6@nq(R0rn$HEjHTW(w*Bx$&e>(fFJp#f zgUuz&y4#)-^mpm)J=^6>%UF+Hvby(X`<-PO`{Omsy7%uq%NTR&j-GY9(lYATzq6;m zg_beC*DRZC&wTxwpuZ#U>gn(Lpl$WtJ^Sqj%NX+#%eu$>m}Tsv1McbBZ!cIDwye*+ z&e#=0GUUr&Fp2X&GAdya)SEMplI2 zwe-V1=dkfXws48=S^4^eAp6I%?Si%wgDm-ozMrCQuONHHvh9Pqy@M>4>&YesS<5Qf zWs6w%21l8ROyc*UFVE*)MFQzgAZHxyFkynC<4G zGW(bgspmN0MK@}g>rBkQR^HW%^9vV`zt+>faVgbs%yE*Xo@0{p7w(U<_^w{;bC*-T z1}@-ycuibXUX4q*qiX;zD<6nMy?mc*;fNf;QF#!K;k$a-X~o7b+mx~KtHw5yjUVU3 zwhDHDDPJF_op6?RV*cSCVp^}}#NHN$ZdcXVxzQ>qz()2i75XOwS=vlVX6zg2E4oU3pnpLO=C zzCA*{eLvgaXm2;CtIE~lnC1U=bZv{{)KvM`FkQlI8;$wzGFIO=AeCw15+ z1!LYF+XlvCaSZ3fd*HZodkW@um46-7&uyz8N1js6c$~&7S5Ckg^}~5ZbGypFHl?}U z(X}^uftrpk&NG_Z9bJ=gN%<69mbsp3Zr|0b9!E5n_Q6rjCC-zY+g1Ktt+`$0uNRuz z`S5KS;JCl{^mE(xHP=c%x6Rx;Ft@2W2p6p%%emHRZddv1 zfS=ozbN%#l+uVR{ezJ`Z!!|$hENt`eJUjCqj%^;&XA&1|PX1fz?=@Yq!mW5R?%&^W zCC%HTs3|j_JGzd>A?9R9*D*LEAB&^%aX6+qc|49=Zp%6WCscDHPO9c4oRS-HT0R-) zdb`;+=V#+GZS&z%u+44eyiE}sL&&jSr{bvQ`Dr*NpN?}OH@mOCZ*fKPD*xJti&pw| znM2K}K0ZGS+Z!*;x4C$_^0Viaw!ODo?nI| zwk>VDU5=yj6*$KI&J_Q8i;m5Xt}DqC_I=#H`S3!V#8vJpY#W|^n!&abbvN ztZJ^sIr%zVkgvx@^?w5{DZdeymEVLzn*TRrj!hen-Cp`*ljYuuV;pN8T`f4Sn%i(f z`68TDemhR#jxHObKQ?K52hQ~J>w728s^%`7lkdg_^>Ys{D!1D(e{8a??!#sEb3YF0 zSbG3RbWA;nqskw`F`N%C#&P8j;{^5j@DiL<%_BIa+%~I?Uw#y4R*^{yo!sec@3AyJGx%SW%7LZ4Q%HM{3f<@1#{*t z9GolO#+)mx#tu0fziQsW3G$AvWjM)sqoeCxoKkMn*T%2<_i#pjA7^ns`~l9%AL4>) zKEg%y^D!>reE1Vw#^c?mIHYUzXE%D=!dxgE#DejUHW33)kAs_j=exbE8_ zXX97fuW?5H24~gwTb!e9-hGD){A(r>Fy9LH7eSDaAI3Y?VxgY82C)Yxgl#;-pAi!-YE4QJKo?>I+3!~KB^YWpWH zs^%|TQro|A8CSV~aLDd?Y@Gk%2+oJQaJ0&gVI_{q{+pS&>U;IFJfXY-C-HdK8>iGy z2&YvO#u>|Pj#c8U@+zED-Uk=de_vcw-Vc|^^WoKSS$TgP>f^_`I*!=2%xc!aQRTc& z?_ylzI=cAGh>NRc08Xf8AWq6_;S@DhoQ`e$$_L>Lc}LgUI7{BqwGPgyW?fuR&3d?~ zn)Pu>`3AU5o(~Vkp}u}>8{&xB@{Mp5&u|;#81Cq*!Et#LoREj$WMALsrZ`2O4@YrY zHJjm#JQQcCuX4k1PW8iaLHXvmD38D;c?(>Yx5S}-er#Leh`cq9_VeS}2HOWS7=JB} z<9v8qoG{yd8HtnBSGnzQN^N5}t$cf&Q9q+_mVB7o0q0b+BQB^<`$&+DU-@WU(sFmk zWtq?KxX@~TY-4a_H9v-3ag;nC-VMi;*WtMG-LZW@V2;&{#Yxrdfm5o9Tz7&2PafB4JXxhUz}2&z-hCMXFr^wrpisnS=G$IIe8{7sGt3DQ9b~d?DP7&UhPkK@V@!}cK?)^Qe2s-MGg3g^Q~oL0>dIHQ^)an^EM zucL5IHAmxud<-ti$KsNF94^bpY` zqw+a8wx(~}gyY!m|8YV!^Knu>52xfbPRr-xjM>Jp0B7Y3a87+*hzs&XxF}zYORB#F zm*q=wsM?S1G90P)6i> z58}A;hj2pqVw|L(eE4CUQlCq3T7Cp)sGq^NB{qIsi=c1+nbS$`57FQpT#k=wS5l9{zl@8je+8G6zlzJsU&En@U+(KT67l`KfuqXb#4-6T99PZTIDzf{ zA1A5l=z0gI%(lMEa9VyBXH-+fS@}JjQ=jkSg7Ob=QT>02OXT_RN4Tu|k8x;_~_a-3G5U*U}UEa9x#*70kc zQ`>KFLHW11NPU(24wrC8*Y~(geMi?1*xt}(-~EW~%~Sjnwl@s%&p5WWwatfr!EtQ& z|2UzVPMnl~#VL6OPUC#|KRBbjjI;88aZWYA;ewX+J1*j>?hjnTRqjt*Hru-Yg+uH3 z@%)V=I3NB8N7wPo{TIh@mFvQBc_mKBdXr4{5{c%+J>NrMj_y0Jqnl*7kHGB@+ zCFKD)g=e^dIIa4%a0c7`KhCOt5YDM)ZCsGo!9{spTr%4_u7}I=`Z%Y!jT|;nM^_$|1YN9wxO_kdW=Ttuw7jQm2 z3>Q^D9G8@Dj?3~099rKmYYQA%-|vepany1f^Hw+}Z;j*fHaMZSwKyqni&Hos9*NWP zb~vM&7|zPuj|y93}7Q8iQlwa6bg*BS$=E(b%<($~$F0W3JQXM8dYn|-eQ-+k(|rAg{^a2m}-v2arro$kdMbn`2?JjPsC~Zsd6Xb zjPgdDQ~xL9SdCxqY#gug<2eN<$nEuioHRdK=}yHd`81rCPsbV6pMkT=&%`oRcrd1+yJ%SKy-Dj7zGy5|_!V+(I1M z)Yo5yBW62xGB`?}4_}RAxXN9F%q7jQm&FD_cW^?4sIspfuMmLI^O&1}pw+=Do>nP2WhI4Up3 zvCZtgn2>uI$Eok=T7nbQRJuoSlDvwS^=p?(a{;>b{+KZm13?Y+fHm&dW8Zo7*@t`*0J`Y}9@6Y^4= z9O{?*0!|HeSA9R7w<2tt5B2-^MVwL1OE@dPjB{GmVia#se+c>7Y4aadl{0>eGv-N!@ybLFY`MLBiPN~l#POI&E zI5W(z`};U6e}HqtY+Hrghqz$<^a{C;aB-NMW@G*smxj5~8#?z1F0)+wc?BFAZp#h1 z&v0b8U&qgJbhuxaFK`Uo{XdS&U*d%7mt*_UK#tk3a9V9kIHUf*##yVee!jsuYisA# zZ*f8O-{GQazQ?8Eew;txvhp8sWOM5$AN~p34~6iVho5n5bK4HTbp3+ko7r&Y5S&d3p*-NGNggK$oL zu8j-im2Mqe)Y#U=C9~!0;WGV)-1<1QrC-(tI3f?m(JlR$H^edJ8{s$(xs7pROW$V= zPO4@ToRWv&^p<{`Y>G3=qd2>z-{za)+?Iad4aEi348ujnQ0a!_lE$++E^E0XaA+$( z&Mk0cE59$c#L=z%wP`CH+sf~Yt#MrS+u+1jelFGGB)Pr*k5j4{iPQ3SI759EZ@b#~ zaX!2~&Z%Y;E^OtVvN^v4E-K#SsJI$rEsSYrnrH;?OpJTkVA-+xRi;jicmMZW4}dwx zN96r+6z9VS;8?96^MN>y?e%}0sP)_GV4PI_Avi@|=?=wdwQazeT3fG5cNosr`k%*| zg>%-{_TAyQKtJ2?mb{H$%Q^y=YW=Z%Bo1wBV;Jv_!Vz5Mj>b{>7#x$2#ffeGc#gwK z`FLzUBuCp5aC%$69ZtlVZTyt&|4-tm{1lGw=(oeu zII*Me{~4Uz(H|Gj;uJNN?m3)ReI93&x8kh&d>-e_w!TYoaYx5J@C&%4ngT9c|MtDx zi#W8C@Bbwn*~yRdWgJ!h3XaLI;<)*lO7|L0?BwU$>o}>lZ{XBU{#bhxr+4!E@hzO$ z$*<$vI4if|ocs}30EyjzAtqy2c^#gWl|KNfLxv|sLfIH8*NaZ>&Or$_ts`VePE z`|a=%4(;sw{1``e_H*D99NpP}kMJpuS&eP;&v0DzpW}r51y1hlKUdO@Q_8=@S$R3m zS)aC@zrux`{d_3l;?90-U*power(_1vhr_nbQfR$9ggkd{`&J9_dQPR;>Z63PRT#w z>@NQN^Aiq@@ijl=$Qb{9;x9O=yaUJNPMjFy$MY*rj`90w1x~5vKR7+ckF$(3)K|Ix z;;ia_!?`hj-u{jYWBfVu4_qAM_vxRwq(1+`(OrE%f8*G$e!2hPxco0p?CSgM!b#;T zaay+NZR3&cTRa<&)!R0&z&Yi;aY@Sx;qtD2`-E|5H^1CU9NEn;s|rVz_rWpQKK^ZE z+s%);A5QG%@Ap>2$=z%nJG%Pg6wZfN$Hm?Jvev+%I=`$naYU}hu{uAW2jEnl-_8SZ zn!M7jg)_<{I9uoY9E6K?emraAlKNZ+m(_M%9NOL2uZLs1`!TGK-Tl7X z5U0&{ENp}`yW4tAbsOXC?tW}FxIq6Qw+SxFLvU$#KPNZE(XoCEQ5=&u!|}1cpP@K0 z*00MjoK($loKn6yPLK7+#R!~Lz6H+7TjGM%*u>Zh7suN6w9o(JGJS^JHaN70A6qSs z?cw{}7AN=c;~a@omRmpD;k5D?&dA&2tULPe z+|D=__v6_G$K!qukHN{fAJ48hCGUna>ZcB8)&K4|7x(uGV{t)!?tzPOKd<7rtbX>y zp*?;7<8VYCkE46~@l3$6J$>7WIIfz#aAHrt-S)=GJ^i_K5>BaRGS0|TaCT3Bj-85g z>a!jfRI?8*%F}R3-WP|)`EzmtN5=X2xgU;>^V@Sej>$7{+-hvz&cq4j`{Sg108YyX z;;eiSE{$`W+K0Xm#${Ws-FF>=Bjf$D4#iQq0Vl`%n!|8f`7E51567kPer_jmS@{t- zG{N#pcO;HX@ckTxqZ9nLIvU3&*z0EF+%Y(=`eSiI^~d3))!TXRc$`vx0#3^(;*8p! zgtHU;zH7uevpt=R3lseDG8-4IPn-Xz;F4-mxJ*A)?o=F`=*N5-j!*RKaym{-^!w-x zoSf*cvY)It6Q?HHvh19N)2crUXXLp!OO5^BC!Cw;U(cC`i(1w>xI}%G{~c4Vton1w zqkGwSLT)~e<4SiPPVD8^H;t3Z&&R2~{M=rEi^?y+p}qYWF2oV}A{>=3#__#<+e>g_ zZ@=!B;-qRW!zr`f2VRb|%CEq=z5UplaY4Qk7v+Vxw6{MuUWLobGdMKK@9V2^)NI#? zYjA9mzt&%ilau`Tufr*7LhgE;p5(XN4LGBk8*x^?3FlOQGcHc@qT%RRWL zntO3cz7LnFx6l9J&=i0F`2dbg@%!jO9F-r!u_?aK#W+62kNIJonBuqX5}Z{22u>-_ z;q(-L@AoLqs^&4Ao8q_6;_gXKih+ zuEYh||DbkJ^}WKDho<{&Qh^g@d+LpoatNp7Fiy*rI6K|hoLxnpsyK*V+Z=S z!*SYjo9CP3jMdwH&Ip`U{T4VUZ|U=cd_Py>t#RFj#J7< z;@lyAn{0;@hx+;$P9Ey(x5sJaqi{yv0cYhMeNBUJyOXbJ@Y`@SE-Bv`m&xt-B>0-c zeEk?)Fx$HCisQ3<+ud+tmfxp!I6ccBo4fm(!+qPazUFX05BI>C!)<$x=SP)nJE&$) zUz7CPa~zHw;oFYK(Ifo+o#1PfPsB-iFPuBVx7`~Tj_~_^5-ytUebvdhtokW9bfoWp zDvloM`>)3_<@?~YJPqgMeQ`lf`1~k$p8KwAKVN^eKVGKetUSZl9PPK;Ok6nH&$s)g6e1CvT6>; zp<{j9B#z5R;G}#cP9N)+brjCXN8{YFew@eP_;J3UWAU`(?EQr*cO0IDJGzd?OONxf z0iJ-D$tU9F$N6jYNx0&8??ycEc>lfC$#}@|e!tAdBhA+5DR?Y3_W2(?OFk9PR{d#s zzUoiM3(520Gw_n*ZQec?J`*oxx%T=$UZ$F}@CxN~am5L~=4`w~o`;v7;QKrWFH_!x zm&@nk71)0M7gwC<$A2Ckc%skKc!+#H9w{%tW919*H2Fe2OTGxtmM_NhRji<|MHQ+$34UUG`xkGJBbatof8@-?^N zv8Q`4!b9em^Wod^NIC0kx&08HWwyD!7|)g;#`EPRc%l3VUZj3+x7Qx zyj*?)uTa}3am8F;^AsM4^WmrQ5arL{k!Bm)vv{oX=kPS;c|3crU$0g?A6L2O@xr-& zn=HkPv*Ku#{32z zOTGR4FPn$_^MbN&5!e>_VytK-@78hF0?SraeB`EWH} zqgc$V7kglC^``zRkCjpr-h884)*{r(@k=zM<; z9D|oA=Xc+`rK;ZzFH>HJm!I#?$-Cnf@>pE4z~_75fpQ#=T;P|rCmySO9G<3pJf5|{ zukQprTlqvh-*P*@?S&W0d*el_pM;kz@Z+D1mtE-h(G&WDf2-zYx@ z|0W-cD=+o+$Kkc*6U% zvk2Sk|K#>qy7Ia3X*gnkC%~?Qr{k!629C*R;CVTYUUt7!>t8u8pkNFxLk*~!u`8phzug3}b z2Aq^{#3}hEoR)9K8Tl5Rm2btl3j17TrE9?j<+tIYya<=%+i_XW;!tlt<~wjiz7t2~ zyKqdt8^`5)a6-NpC*}KaO1>Yb{+N9Z$K*U7h4bN7JWln`NAe~#;V`Sa};xL$cXZjiskjq-BbB!7jQu+&q zFS~|1_Z@Z>wk}iM_qbaA0oPVIeh=G^*nW_L@&DxOdwc)v>*Zf?jog82$n0p%m2YGsxRZkz5Q!J|HZAe?dbXqx5>ZbcB`=o{|D|=+dpwr$dBzWY;S%t z=D%@^{13J_<@nuw|Ke85t^Y1;Zx-=B)Joj0di}Ca&Cgy{mRHLaxF+nkVQ*Y(x%Csm zb#fTj%ayo6ZL4r&*x$eP!A;amb$#(-)%3${YP%Y4m;2*R)vS(PrC-N2aCN2Mwrk=V z<@~K8S1S*|b*dSN8|AfdQ>DMRkKndSe|{K*+tqe$+^PC?ux%K=CtVlYLU`?NJzQh9 z{jxr8(C;kT05__BFm58RavS1i)og@YBCbzeWuVYo&fj_r>vG5*bQojd~9_w}!fZh;%*Epby{zsUISl7;K0FKCo51vQIJP%baS~VP{NU$9KV0RGB)12@L-HtW^TFT$ zW1A277;N(aAB$~1%>H-0I}XRmtK9K8p_&tLQu&EErTiqEmK$+KJ{f0KKO5(ipMnd@ zQ@E)7R9uoz!)4W+jzhXuo`ECsnK&x*_t?0vX3S^dxI7mpT`oRlxe zDftSVmYZ=#z7l8Ug*b=v;j3^#&fubaH7?25;Ie!z4sq?yhp)pCUB|D-QQgPhfMcq; z5y$15a6-NrC*@mkO1>4R8tCf|qS^8GlWW9^-XIb5{Q zgxb@ixTKoLaM|updWGEM*zVzZ{pSfB3Hx<<65IX!zIL!ah3$Tx&lNn4a8`a3=j69=L4F$-IN!l#c^M8>`TLxAaYQcS zXq8`=_i&8g4;^yvOaRt z`3qc<+i_X`5=Z*@u`S0@`70ckt(lES{u-y{Z*WHb7MJ??b^i|g-_2O%zQ>`yzW*O^ zT>cRsqI;E}@I3CR?Dv1-%jIA2Epi98KUO)~j?GT|obq4sTk2;8{v6xy|H1YLD6y@o z{jpE^zjzJqXY%3S@Ic+?{Emlk|C0~@fk(=J;<2mT3&nLW^f$RZxVHX-t99RE!?XJ< zU0=Fzt>t!Ku@cv*#{U73dfET7{sy_CkJU8Fy>XKq!p(9Rx5$-vv94cLxK-|h+f>sR zx6A!-r`g7{8g}+NlKm`ae_X9=%IdgIUIW+5YvP7pem_>@M!UbVeg@zs-D?fR&AJ|~ zgj%#0x#tG#+Kb)4QZosyG@nM+#Yo3K||Kh`O zQcmKOd<0Hw4jhRyI3GR=XH|1F&dJB%f_yA4s_k*Oq_)R%bE`-V)na@=Opa+ zdq-Czj&k2sgSW&NDfp%2(sDvfoxVX8OFgyRDr=uKQoxeE9nRwatfb zz)`im5!<>mhMRE4F4^|;wm0K~@>{U|Qo#R>vyyq054Vup1K);2s<++$zx{hV4wjY0 z!Lshaan;|66Y^a+Dc_A#=6|QUdvF?eblr<>ocKPR)&9C47nDDMi^?CwCHWz2`+}eG zT#TbSjvmH#tTCULU^~93dBoT2n9AXZj;Tj+Mt%$zOKkkC_;b+P1fuHkjaYt9)x0PFQRDK@E9Zg?sZ(m!`vIVgr~YUaT#}Xy@hSg?`w1NZ5*??WO*A- zYF@pAQ}QyLmfyu0)faJA`Fl7gzmE&@2e>GIh)d>wUv?kivht5{h&yE zi_}!QZ*a+M`}bR%2>Cw0!x^r>2f6QY8BcOQ;E-N({t-vQKK}_P^!n1zI4A#t?fXZ@ z*@5kYIV`Iahj;IVhVXOJw*K0ff zfva&o{3ouF|H8q!?r&VD{2yGewtfr^%Dc$zK|d>TwZ`D=^SiD_?uBdR3S1}m#`Us& z`{c*ZdWCVL^XpiNo9(=3_wZG?MR^~*Sni8k)lWa%rhGNrsk}dSc5iR}ua0fq>2nQS zqjSre*w&q6zZ%!6W&mzb+kv=I`C7P1^%2}G55g_7-E#Tw3+Qtl+-m)t8*=O7Hubq4 zZdd*KxKrK$JDslv<7#v5~R4{npE;dXgn+$krp)9XC@;c9t0u90WpI(a6pm-ojF z@&UL}J`gv{2jLcdUvn^CEFXefaXx$~Zc}{&ZkG?ko$@Sf2M62#aBMq@wn=O&jgP?A z0X`Bp%17ZQ`DolMAA?)uV{xl|9Bz}3$L;b7xKln6`}eJa`%M47RX%Ks^bf{xvahkv z$=Ladzum=o6`z7@b$(0XI_0P0digZmpqkThqpk~Q;3oM@+^lo>9K2ZdXW>?PE^d?0 z#_g(~hdbqSu+w$HHj}N_D%Xn|<@0@wKAUtN4q02f7N>E9e(djm;UVgC0j^cE%M!Xv3w71#lakIQ+^-0 zJ-Te$-jDr0%7-7oG27<0-yg(r?U#peLiu8xRQ@nd$xCoregqe_O>(%T{81dycpk%1 zvn}g!9G9QKG43Jl_y7A`*RrQ@Qr|B;jZ^Y7I4wVmGiv)B&dPb5lkF6s`&H}zd0bT6 zrMM)&fXk{c;E?V!U&ImlB^>2mw4>`~T##SECDpu&%kpd3zL#NNypCgfec%mj_oB>` zH*r*-Pk77Mn63Y}aa?&DPRQ@zq`VBLYtf(h`qg~>rO@Qr2gK-=Q#L%*I)R2W54cpsqV z|9`@*^3S+=i2qxM{+w2`sdoptJs4Xj4#~gb2=3@wfur(&a7-@axcpz7kblETvu(HE zaZ33gIIa9ooRR;+S^00AlmEeG{%&!9_b(3VIO@V2Yu5itpYz!OTSz}@G!J`WKTq=E z3d}sQb?l9qC*}}t#lbnmKCnh!Np27FDjbsg;E2Z77squT=!X;XYS`A1Io}^=PcTYH<$mMi;X|0&hS$?d^%_rxK29A=(dJ|6EX`?huR#GvhpCx7YMYn8TpuhMoB zj;nq$wqt=lr{G|IPQ}bm%k864HU^tNX1}b#@-&~T?Y=k|TLR~l@8@eY$9&s?@(gl& z(C18FBkzxceh$D<Fc%bN8y-!G)~CJ;Iw=!&dA4MjxSr^<1xpV`2@`IWj+zxF@;aUS-BCH+OfH=mE|wf%jc)8q@t?ZMwo zx)6ud&qdh&F&tJGhHQ+!6N(p`>i4EPG1k(+T=z7iMYh1lj2zw6*C z9MNmh8EoUHpQ~|9_ukjwxbkapLcR_s38O^KPvCS)L?6he0nqzn1U6tR7?U$^uJ@3Np%Kdh> zU;aUU54k;Fvp)cOFAnz0eK^?H_v5Jc%L6!OwsYfyxUBI!gzY>}pNnx(ei)bJCD=X$ zL5&~7K+Q=zmHWrimA`a7ii0sch6if7k7NIRWk=T&*nbZg^fO#FPm$Y$zeDZ&uU^Hg z=7FE1zA5BZv!^_6UBew<^RpE?n|G` z%W;U;>fdl*Vf$W&@sw~d$G*lf&8u&Gjr=X<9>d1)9nQ$#wl@uE z`!nvN^W-mh-Br$?8?9pd65IJ#YV6TvUV%f}CjY?^xs0P`JEs1NZTsWja9sDgzhm1M zjNuQQRL!3_rTj0PmjA{X`5&BB{l7l9ePR7~`CR*DCC-q(n0ye9%Ln6xd~NVmrRrZpUFerttAN zE}wvt@`*mTxoR~h`CM+q8Tn)!<9pnW6PS0xfe2@jceq2xK=&~*U7%0`c-US8f@=b%%>*sdD!|S zPvaW-d~AJ^FTi#31=#u={OosK7vcux7vVdU032x)hxuWmmk|zxEenZ&fps5SL0gz$I5GPo$_n3eP6?RU56WFzpWba6X6@k z?Lp0rxLUpm*Wh}0Gp<#B3$BxI#r3Lb!41l9!;SJH+=RdDx*azw&*B#O4!l^t6SvBD z;Wqhh+>Wc=J-Ac(z1V5J?!$ijICnp;QT_n-+s8icjO&y?goEv~7&nm5bPr>{eeB2h zag*vF!Oi%Ia1OU9e-tm4AH%JUK;=k%J~>$|S!aqFC3t6RPl*PPYsGV=?#RxaQ=`9)kW zzl0m)mvN)~3T~2L#m(|-xJ7;)FP7iHZmu8Oo48tj3)jeR_jGx;~R zKKbnHKiK-j|6=PCcj1`4631nGo4{YAsp*B2$}4bM?u|3{J0I+RFNCwo!?++<;-cQ` ztimPbeXw1Z>Ax?wYccMJ?OKdi!zo@H&xiZtw7fc=&TH59nI(I0&xzM0x5pkf->Pv) z%Nl^=@<5!H*TOb_+D33z9)zPB!`e6{uj6avb#XAx^>9XGTi@5n8{mRG7~6d{%i0hJ zW8MhceKPsR*zSjM4YvDXya_JKL$KW&lW&Uc4_RQp9qf-w;LXVG!5kQhgY7U3N41W_ zvAq`kzqaF6v3+T7cb|c`4EiMB3RkOUYg{95gKOnlTqkdf>*bNSLEa8G$}!v|Z;zYh zQMg6k0WX$!#I5p9xJ~wdyRKc{nY>fp1-o87>s7sq?JKO;ZoWpY!`1TcxJDj}Yvny~ zogByY@}9Us9)}xczmJ+$v3=?P-p;-z$oHnLJ($mvu-}hWZZeLrzwGn>I3`cUak(BR zxsd?s#{=ioN^EZi>7 z#hvom*j4zsHxJuA67%pJTq8H(TKQaDC(pEZ>D&9?D?g6wpcU&D>^>$pjN12@ZW;uiTW zyjXr4x5{m}O@0Tr%gb=5{4RDCZbQ4S6mhlu9&i|4(~&0XH|1|8YDmZ7Eh3Elz;qw4{bh>Od`2XbS~` zI|L57gM$n15Zn$3?y$H+a1LGca2A(C7I*mnKKbTpw@@g3o<4n^-u<_)r+Gi~%qEk` zWHOnV&6ed)aVFPrMg9y|<$9LYc6 zSpE?w@=rLGf5v6`7o5q?9OjDL23O^_xF*kpo%VV9+hMB*I7aQUD|f(F4-h^x_T`S) z>H)%M!J#}Wj^x>JEYFS;c@CV)op4#66K8S>SLC^HRh}Ey$MPaLkr&0OycjOai{ngoaYbGNSLG#fO~zf2 z-wT)I-q@8_!JfP-_T|-ZAg_)?c?}%NYvNd53n%j0IF;AIWqDnk$v&>geQ;Ioi)(T} z?97sP{dy!hw=tEk_Y2h9)c5jC{E>JxGWFHnH=DX zydkd2BXCU~iEZKqX&8k|@@VYJ8(~i#gME1{4&-q-lsCqaya|rw@i>t;#i_g*F3X$a zOb&5H-U3(UEpbiW3Olpq>E9Zck$1;cc@JEZ|AL*_^YrhDOY&aWmG{P;ybt!}zv4jt8xG}taU}1DWBKnm zk@v@`d;l)X|G=3Xy<0V_!Z3TOlR< zNF2&XVaq(ikH)cl3{K=@aVj5&%kuF!lP#*HUp@g>{5}rk4{#`dh$HzwIF>)ciTp86OVKgTus3v3;E zr2k7?lE1>P{5AIEZ?G?aiv#&P9LnG0Nd5uG@{c%?f5NH!GcL=&;7oQpnJaP|T$S76 znmiMB=F8LH4%=Kc_N6_xxoWrr_T-tdFL%U&JPQuxS#c!KhGThloXB(FRPKb!@|-x6 zOSmG>g{$)1xF*koo%!>0&WlU(eAt!e$DZ67`|<)fkQcft|f)jaBoXU&g zvb;FXWEWTDC2&<<64&IVu+uqDe-~Vmm&UHV4EE$@u`e%&19^EI$}8YVUJ=LgN;r|b z;#BU2%ks)NlRaFKyW^_d1J~r9*jXS?e=l5;dt+B#1$*+U*q2wsfxJ2nz)A+E|Ja7`YG?E{var=xI59*tdjBkajzurH6rfjka} z^2RulH^H$y9w+jqIF&cUWqEU)$sw-DTi~j^C9cU^Vf!{^(!Vt>$=hI8-WGfEcG#D< z$AP>94&@zjB=3Y{d1su+yWmvb6_@4Ra3)8%BJYl?@*cP*{{=gXUQ<5WHXm*s!pOpb9yJ`h*sgK$kg7(0vR=|2RQ zo_GfWcviNu6K)wry^4&O+ zGn~wlzxN)T%J<^3d>_u_`*B5n09WM)aZP>*JG18H?O|M!|Ak%o5$ws2Vqbm?2lC@M zl%K$n{3MR$3Qpvwa4J8I%kncglb^*E`8iybpT{-%-`JTgPyY+JB)^DV`6cYhFJoVR z1qbr0IFw(*k=c&X>o}gxS;;2ZRBg?8_hFK>iqq@+UZwKgF?J!-@PEPUX*WS^ffN@|U_BfF{;8dO&m*tK)lV`ydc~)GNXTvpXE870fjvZ@vndiVIxf6EfIk6{~urJSr z19@&7%Jbkzo)$`bb!tF)h$!lS!ZNA;Lu`92GJ$YU1+t{LQ*T;#%`{1(N7iVT$ z^uv|54)aC(0nezA}u|JbDz!syhrIYxhaU^eqV|fft z#EHBYu5`$c#oo9o?}KadU$HZD9_MekB&#gR`{i+%vsASAti0j< z>%tGn-&+^Qm1Dv^>&vk;ABcVVARNdC<4`^XyY@W^YuopKVNX5``|{y9kdMHjd?b$K zqi`%AoyW1Zho#||JdS*99!EY7)6TIt$79+#<^PURGr<@0bRpN}iE zIYTXN7vQRVA+E_6VQ2O{&c(Q-_QNIEl`qAfd>QuT%W)uIfkXL99LZPVSiTx3@-;Y> z%eX9Gi!=E;T#>KGRrv;7lW)Y)9C_Kk3CHryIFWC`seCIg%eUdc##Zb&-i|~04jjpM zVrw9CeDA{6*u-~ZYq;SIJ9Fl~2bavYxEH(feb}2bPv`yEmmk1^{2&hHhj1i6jAQv< zIFTR0sn-7}F3XSMOnw|!iAU}^o z#rZdmfGhIMxGHzVHF*~7 z=pJNNT#{$Qt~@*TI$)XwaR{Wg|{4o)|0?au+WSQ&?HtPNmMaU^%gvDVWAC+d^wiBq{3F3Y`fCa;1k z@~XHhuZC;#>ew=ueP08YwnCrOZ!#0@fxOrRb*fr6- z9rotT`!}}7zPtktZ3y%!2-!$C3Y*$C3BNp1dFS<-g-V-XDkZ0XUNXfnzzwiF_bV z<%4ipJ{V{6A-EzRimUQrxF#Q-dtN&)?D;>~?vU}3xFjEiUHNG2$;V({J{AY^aX6HZ z$B~@iSUv$K@`*T=^E$RHpG>&D=mS3md-}}Uso1xA#lla+f$HbeaVVdGBl%1m%V*(4 z{wGfL9PG1kS>fm4OxrpaSL75|<@0b&J|8i~fHc6#f4=lrP1R zd>M}A%W)!Kfm8WPT$ZoGnS3>_$k*ViT*fu|TI{Gl|2k~-^Mo-7SH2;SWA*W!9h@8U zI6VKhgL4yh<(si5-+}}ARvgN=;Yhw6$MPLGQ6AojQ-$A!%L>06X9~}7MZO1D<$G~W zz7IQ==a$a3eqG#Xg^l zoY{E~+jV?G*$eAzf11!wG7Vf~TlQMZw)+{jAN)CX<%Z)(c3Mye z3AX(Ct|7j8Li`4^&PLdo%idVLw+$u@ww|`wm1n}9+z$J7+Y0Jzf8xC}6USbZ?T$r! z%JwYS;^SFySXa&?dG@;ZHtZwuI~C#78*}24&dm~b<+-pY&y8(coU`-bP@Wgtwg{gO z$MXC*kvrp5UI3Tn1#u=Xgey8n7RFV15nPiO#U+*V#jq7MzPtnu1S#lE~8wsVT}Yk6$v5ncf&@`^Z>SHhXx6<6eLxGJxVYqEz+ItROB zSMGs}bF(M*?fW|{&wF7zH*s$q%B$c=UKPjkYB-Ts$EmyqF3W4;OkN9Dc_dEcQ8<-H6f>`WqDhi z$=l(IygjbUJK&nUBX;Z_(9*vXF3CG%SKbAC@~+sIcf)}k;ZWWkNAeyxmj8kic~6|m zd*QOYH_qgJa7F$ruF8MIHF;m`*nO*|e?MH3|BhXGf9%NzU|;?R4&)ez@_{7lp0 zFizw{a4H{)%kp73lMlxg`3PK-x(z5plkg*cTj!e#kloXMBqihL=q%9r7qd^vV(+{)5_1un@~ zVpqNjd-B!Tm#@KrT*je%Eso^ta4cVs6Zr<5$~WS&d=t*(n{h?H1y|);aZSDrJ2U6$ zza87S82fSucI7*&iT$R7WHTiq&XsqxDT#|ppuKW}Bp z!oEBw4&)LJ<+*Sq&y8bw9-PSY;#8gwm*x3!CU?dac>!FN7sNGrA#8W%^fxYyOY$Pv zHQQoQ?8%E^UtSysvWr7`32c3&ymv_)%S+)z?t)W!X9^QAJ^m!u-!KqewfkI)vo*Ho8gUQ14er&(dHTD}oNaM`p65Bp z*$$7wGdtVkt>qo?p7M_P0C^{T9P1hG?2ON8lc#?de6`~2itmzl!%xWhJ>5%nwm&U0 zN708vkjFnO_ldaldG3>N^hNHIar%1x+`m(>_fGCpu`lPxC6G@i z++O6{8MtKo)vh>pZk>xb4ab!D^**OzJ@(#ni}lQBPO+^ApNB1PITq(*U%mia-V%Nx zw!Fm`bu;n>vF5|>(i)-`v`{%w6XJ_TUJ`b>SR794z# z`&L|)mA4<};kOfRubHg;+<`4!Jge|dJinaBU!czRr}^5nn{bSJS>Mh*#d?@4o5#0f zO&ad23%?%|ZgC#KuKXbOoF_7j`i=o_o=$~ z=5dPjPcHt_PR9vp_QjuVG()9b29dUd5sO z29D%6aV)=u6ZvhN%J1N^{4UPq_i#miA6MlMa83RYJ8kkb{0EohkFYC$j6L}i?8~3x zK(66X&ht5vKPTK??Bf@>B!7ur`77+nUt?eX1_$!DIF!G`k^DW5Blld2mV2kC7|Sm&eim&Y#DTJ7ZT~0DJO+ z*q0Z=fxIvdRIFgUWv3wj(+o^^KnJK09WM;aZSDmJG12JzZjR~ORy_niaq%HBRJfa4MH^S-uu$@^!c(UyrNu4Y(%Xh@DyU^xuR_^3B+lZ^52?EB58va3J4~ zL-`II$#>#dz6&Sv-8hvqT$b;_nS3v<$oJu@d_S(q4`64uJpB*ilKc>M<%h8+{|o!_ zBRG&B#i9Hdj^xL2EI)x0`AMA06tW&ieHWMH z_pmF!k3IPV?8_hGK>iO7<&SVAe~e@K6P(DO;#98Tviup&$v@#({uw9oFF2Lyx1$f1JZytAxh<~9GvTV- z4%g)N*qJkbZwFkGXU4AF5qt71*q3L;fjk=y<=Js0&w*pP6HeqgaVpzK2rT{bTsV{G z#ua%UT$Sg=HF-X4H)ibn{J11{#;&{o_T&YzFE4}xd0`yNi{MCJ6vy&nIFT2}sqEsi zyadkVC2>Vw3RmSWxF#=+ow@S#FM~_+ve=cE!=Ai6_T?3DAg_o+c_kdlU2!aT!->2y zPG$S3hNWNbjx)IjuE;%cRqlmra&PR+ou_{lT#{GCuDlxdDu@ z9LYm*EDytpJRGO8eI&-xFK>u5c?7P=BXLz8g=_L?Y!v|IeP|s zcG#7-$DX_c_T?RMAn$}jd1oBSyWm*f6({m;IF%z@mUqXQya%qxf5BCGPh6At!q!1a z`uD~qc^~Y`f5o2sH|)#%;y~UHhw|TXB=3)7`2d{A|G=pnDB42>3@`bo2Uxb|n^KD&>OY$Yyl`qAfd>QuT%W)uIfkXL9 z9LZPVSiTx3@-;Y>%eX9Gi!=E;T#>KGRrv;7lW)Y%LV5ac!X^1;?8>)bPreoV@@+Vf zZ^xm02ae=BaV+136ZvkO%2wT2`sI6YCf|!I@_o1}-;ZnZ1K3$OPyd6sBtL{*`C;tI z|H8if2oB^&aVS5ABl&S0%TM4$eiEm01()Tga3(*EEAlhADnE;B@^jc(Bv1eIxFr7@ zyYdUzlV8NX{1Oi2mvJb+f+P7=9LulaM1CEovQ_Jre)$cY$#3F{{1&dtZ{wQ$4z^Kh z&b4>3eU=lyhh6!7?8zTsU;YpW@_%qBe}p6XV;sw$;6(lur*aLK<?e~v5i7q}{a ziEHv#*q%#9n!m>OTr&I(w&#-JZ?Qd>41b61xnwx+#}4XjVH3h9)J4RIuoz_C0MC-Nwq%A;{v-Uw&%7+jIZ;;K9j z*W`_{qj7{ya7iAIU3pXN$(vzc-W&&Vh(mb`9LZbaSl$XJ^42(&x4~t3Tb#+;;flOH zuF5;$n!F=+G>)(nF3CG%SKbAC@~+sIcf)}k;ZWWkNAeyxmj8kic~6|md*QOYH_qgJ za7F$ruF8MIHF;m`XdGcbT$2BeU3q`($p>Ix{s#`^7>DwKIFb*-v3xL2OABxNJ zVK|cy#})YqT$PW+HTfv)XdK~aT#}E$u6!)^$_m*jt9S3Vni@;TU-&&7e9;!r*hNAmeNmM_4G zd?8Ndi*Q-K7-#Y&xFTPQtMX;ICSQ&njU!xvOY)W2m9N5{d^PsvYj7Z!aVTGlBl$WU z%h%&Xz5%E5jkqk|gfsbOT#;|VRryw2lW)V0#u0AECHW5Q%6DQ@8O-^Qu@4lc{@;!J)ISLF9`RsI0ii8HwuuE@P{RbBjYBy|=5oXDeaDv!oxc_W<3V{kvZDT$5Gi$YYp1cM2Y27j$7v<&fn_d z?^_ptKU~EBJNES6{jo0}fP=bqhH^}}y-3f2xJbi6*j4z!xLD^Q*jMKARk@67^0nAe zop2q_>+N>V_1IPT4LGm2+c`I4U*R|5yxwl-+>Aqo--7dcyPb0@jun0zPUPEhD&K+2 z@|`%7@4^-NZd{c!T$As?PTTx=-HS`|eb|-n$DaHE_T>k0AU}jd`C%N%|H85S2u|cj zaVkHC%ktwmlb^s9`AJ-rE4U^nmeg&81S8*o4hAZ;xxGGn1O@0Gg+DZSLxFo-YUHNV7$?sraeisMwdpMNe z$C3O2j^z(=BL4@c@<+HVe~dHv6I_u$#Z|e6Yw~BXf$!=d~=j^rP3EdPiT`6rypKjX5T*F~A^bhdDNk>_o2Np6c>c_!@1?XWMm z$AR1dhw{ugk~`v9o&_gzo`zJOjc|LBhS_mRo&&pbC+x{{VqY%dK%NVS^4vI*^KHfQ zyoB3}ZPnMw^VfxU#zh(yz^>vih>J8VgnflCjEgiZf#b$BCklez1Z$bxFmPQuG|fK^2*qk zJsim0aVYn|k=zrTk9g3I!%IFnby6?t`BmDj*Ec}?u74qpqGk$vpSeXu9@#lGAR2XcQL$^&pD=i7?qL4@0j{9F&0*5#d*|0ADh`RVA>*9~XMf}m&*LpU>#dh;FgbE)^xV=cjI9#M* zW9%w?6I`rwJoXj7DK6Hz84eY`IgaEI$MP09k+;ODycI6XTjNaL23O>5aaG<9*W~T7 zqcX4qF3QG^*j4yW*pqk0zPt+#`S=4CWPnf8Rh)FE_{DntUphKt9AZkLY)3guPNdTaSp_J zoOaGZI8VcP=V0vXy@%i;4Ts{8@bS)JI8vO$aV#H!6U8|ar?|g!6fP_LXq@3u&M~-x zhd9ULs^TAqYYIOeJLKmmrUVtow7>GlRd~L?_9x<;l*h-toRjnTc)W88cD2qtzK8oe zr%i}I#5sLJ{2|5q6=&l5hd5_Vh(F5t=Y;tEowISi{!z|3IA8xL=UnWwo>5F=D)Mub za~=-0{_}ApUw~uzLY&AK;Z(jDmvI;85}e^)&ZW45$2*tds=_bFH9W+*0_XYJ-?6BSV~Paj5v$;YjhX$1(2Z+<+6sxe=$hICf=xzjHI; z_9FZiT*9NATd}L~+pwqf-;RBS-+=@9P8{O#&RsZ?@5V7+iRofRevWeP!71TGoO^Lu z@$+MyDg1uI?L~SXz$Jx0h+Vvv^APqFp1;>u{E20zzw^k1^%VP_ucxrRoqd0NLYywn z6F864%Xt#J#2L>Nw_;oUou{x*crWK^9N;0&GdRSfoM&-_`#aC!SbiQS^1pG4yErf4 zGVZ1FpW*S&ON85tb>{n3Qk+-n!e7OC_;}|v>?;22*u(vqmN((py@3OTzllTK#d!-y zxJW~c$2;#3Zm$9M^DfS}W%GZqtMK=6zAeZ30Q(C65a-*n=l|eP;UD3ATaNQFjurk1 zPUKH4T|G_2s8|=#8Vo&}K`||fVkbl6T{3DL! zpKvVyj1&16oXU=UCSLc;mJ*yPye+QCGvTV-4%g)N*x}yOMsjgU&dY-M`rPP7F3TRy zaLMV8E7+cYjjM7`T*DomUf5w@Iy${^oWJ6uKp3}z}VPX9(WTZ3L{xaOI45RIPKL$A6 z?eAkOe3<=pxM`&QKGGRyKcnogL!F*Z4+|M)zxB8G3~{;@OMBXpZwZD(Ga@CYl(v)qFbT+hYub-FP zo=qq{O-Y0Dr(b>~x;v{kAr(_TD~H+<*~rQkN4@dqN>lcB14{vCYHz#bv`h+ySqj*L zUQO8klyka|)e;+7c@L~~HFwQ4!nUGg+PwQT($3m`cD9aa_C4aNLLE4Gx@@V3v-xz| z&U%)EL+ofZzT{82Eg#pnp8=MJ#^+~K zQb2u0o!OFGA8pqQuIMe5iomwruSqp_Q;q;z?{CMTDYa`;w$JfzTHa5&));ARe(I*? zZ%4DGA$H%OYh&X}^_2ICdlt^y=Djc2D6gSL+W9fu?g^T6f3(N6UWVI0wV6j)E2BB? zLAcKuYNdXNwd$vB4mRw?SS!`E{#SEWvFk6jENz(K7CPMS4QQWEdW5^#-$@hg1DC{+1k3>o(#-mbj#bFJuHTotGC7PZY5-M zOG#JT;uup;3+tc1cYvj?r=?O`?q^$T{_}&YjqakGvn+8Zz?P=<(fw^t0N3!q^0+DO z{Ygv08K`_*QV6eWAm0crL;MZ9p!MGZE5=S4$#Lk&h}?o^><8qEGJ%f zx>>IEv8}9cXDU4hqnf2&)7Gic2HQTVv`&5f*_1Lu-^j%Nk8ZZFW9?`9rh#iOts3gw zK90nbX)*Oj@!gS4;G_Yr~+TrPI83VY(-$<<^`v!DuTx zqw@XX4y8G_#Px!DXJE5#g}c2`R)+^oZXMD$Ou2bXlZU6S{a4i<)v`-$N8fvipHr&$MEnib(&YR{<<)l5Yd3b8Vq_eTXe}Z{H(t4X$SYuWnZc+K#KvaC@z5 z(!`$k8MRdYdOsLT8f53@P&+0)tp4V@%{bCPJBExLjIz?I_Bpi%N3^f~s8^qMOY>b# z`q@_c+P2y1hRs?pYX4|8Ov~#D>l(9jMTZ@+PO^xJ={+Foj}XS6z@F{O&@ z0b`of7-}Jr`;9G6En6oxX8*aHp-gbTQ9Aw?j(I~(qI0A1d)~$BD|#`eKF@Td8aI~O zka~JZ8rnVMt&XCPgfSKR(&@LSbxIf`{?w%-nzi<#?Kp8nhdkxlJM}p;<@Wu0`F@YG zI%=TRY|XhP_GdjizWppG*U#I4joquJ9B)d=fc#i;Jyd@h>8WqybaNPUo%-CKw&|km zY3z{E_HowN#`=}&){#QB2wNg8leT6!iVb3=l;D1DROQ?$0+n>2M@ zt=6F*l3x1OoQqxa8e4a(oXgZ?tt-9#tnMRynt3%X&gCiB?bNc295uafpeZ#Qd(M@U z-hy9WS*Jx0=Cn&2?R4($X}k8b64dnG)oGbFjpVehRMBGUn)iIPu5_t|5!jtCZSmH0 z9DjRtYE7x5caCu(@{RGtmQEXGo@@EM}Z%pi&`ZGN`8cE}r4YVUT_4zU7>q&n*4kN9GPfP0tqEKPiJfU9Z! zS=i0~GRjiin6r7tJ3f=|JEX6e7Uc9E@BQOe>jsvp5sj#0XS}1Lv1UHw(mKu+-A9eE z)G+Qz55)TRKWWpM$Q%zwjl@6-_9me3|g#%@|JYt=g&-eTeLx$yVk?@ zs%ax{O)2@49`Rq_)wjeEZZ4yYi%~Z_1u(vp}jNG+8KSVP0^Gz%y?HZ^`&#y z)j!YszSFrT9GIUE11#s8dscdvq6Nteo-Vep+(`{=#NJN%TxSk&Z~O6V_D4^fX+Rwo z*xmTlUt^|bTdUbNnHSr9+ginPZn*7}KKIx7Gj+;))yJ;P{L}pG=H3$ZAgv-wM)Piq z8RXRdw8}V6jk$mMGr7lhn1|7{E6tzX67e>+J!_OGAG<{=;lPxW8@Q-W>Yu$K-p&T|sk}@WEC;Q0f@Z>1J&>N_%tEddvbCWHoDd z`$t*mXEjB0Y=;_w&x167&z@#a4z*HE-O$+a|0$nIv_Xg2Ps6x=bDp&u+4Yq^YT8wt zv(2|xJQEQ~G2w=u1^8E>EUDH65Q8?(nvzf<7TO({9QKc|m5Xe09LXj4AR z(v)M>GUGiHO9(TU)q3e>Il*TMKWlF5yn>Ul_-y+sfEIQyIh*Kor1)S^)q zQa>)rvDV*pU&9?vZ|eseV{N7lZMG(#PZ($Sr+h}Gzx~GkcenpHwpfh(czGPgUwT-0 zFZ+Krd&ef`O|9PBz^=E0rgILpObMZfgdC@qYsoRO`gCyTINI`GtvHS#^AR@Ak2qzJ zxunbxU=BdzM#-8|%lvUl>1!L4ms+k{tu1|gj)~*VI0>J|S3asAqBW+Mp0hsrXKGtp zPSU4N&$^yG*4k3Xof30-7!~7QpOK)}md0_Gs`c_*Y<$|9az$#H`nI7}%h-RzHJQ&| zHElM+e`h{)wd?S3yAsd>pl@gTwpd!Gd{84ZTBLF8|GjeYNBiHh<>8N8uq{K1TCVQQ zOIY9ToLWN-(-I{Y+TX&XJ~++tyg;{`vZ(>81R?E$`~D>3yyK9ccf0*$;ic z^oVw!&e!yoIoIm@66yQWXWbk3jD|m|2PeHcxz_$}=y`Fi9nSS|Tz(y-hnmkeO$=%L z7FX8N{KcB66Ymh0Oo4-DqeC@-`=L3`ew6{9($(0f`OJv>OfTuQ{*?NRmk*P#`6srkr{6e4 z%hiOx-o~F$tZ%Tf&OuJ{| zQD~SM#y1sA@BY!2I|J%x);4vtc*Z;Wt!rjr%awvZlMnxkErCCp8TY@?F8rhYZ`txN zvEH8CXIon4S~BsD>DM#5rvJ=9>XO!&M}K~HL35v9%vs{zpZ-m*{rZjxKCjh!$8egK z<|*|XwU%$(X}P^@J>xsgn+LxhA87n&kJi+O;HQ5gu#DRWozA z>uT(C*{1HkqxBuv`dpd#-DdTn(=M-C=BQ6P_wxUbd&BpF)jzF&;yLpFxiz`kZ(?h9 zTJ+R3?|Cq7(>kNqZ_{!HV)GuiX_?X){iu=CO?z&6^B$|Fr*B3-LJhMw^lX7?^SzQy zIcD|qzIi?_PX*&U+y+?7mgkIccf}L%G}ESa-``K^URn9naHqoFPPzU5>lsGfEH`NV zPJK=^129hN`ggQur4v!o?*?~l`{y>ED8D1BWmk;Y#iVfx>O z+kEX+@)`Gf+Sh6!WT~ynf~8__xow(2|mhd$^@^a9&!uvtvYhMBZ*7{jDuuxjOP>JJP5UOdUYGrZtz{Nk0+H6O^bY zS|V?MUH9{(B#plC&FH$qx30R1bAQq!tu*WV-07v`hn`uLik8UL->(TKO*z+p-P5(+ zb8OOHZ(S8ySDtbe7-Q!=HJ5smxoY*X&z6v@e9uVhO7Cyy_cER`F~zMbwUgGPt@&!e zH)ZjSuN-x?xOg6Q%X}a1@7Dmd0(tJ>#4)tid+u{a=^2#t6)-EQb(PJD>1)kr(C?*E zcYpLaw6^;euCesJG6R9@=xF;*<#k+fRiz)P^|c49@A*cr z9IY=tVcu}xt*0uquJcYMcdR|np-V#8ByTGPE3cZ&20O>A>hmklbe5Uux~ zoRUjfZFn+qYc98wrjzFu)AphyH=K1XQGS1YZ$qh^crVjBdMg`_GRL|hulR09`U$87 zS|Z+rMYOm`YwJ4B^;h-QoRhylCzz{c-&U8azxTXFQp+`{HQyU3uNyw^!Pr1+ycghU z{(XPdqpCaX)^mo{k9N0YkLu*Fo|HqbmFku1YBdy3c;Ly!t!b=cl#TAvJ2leMM}0~f z`F)_ZMTS{gTedwn`J7eR)cr?8-@ksNm!+v=zyB?BQgket!%Q91JTo0MpM*STCIjDA zL)qcHR4b8BaxnX(KCJOi{?k2Z<7T=~z9jv6K1ger2hxzzNsr&ur)=uVF|m1))>xbO z{%1aY!<`o2&^N^X^|5!*51^5a;kJ(PRtD%ts-K<19XNfUtJ>3``r2sYZ)ey4ek)|+ zr^KnRMm1u-&ZJMOoZL5uE8p1%%5USJqCDj-@hzcx66V-u-46YA^ffl;zUas>TZubZ zzHeIJ$v*Yvb;|k1H-s|M%aP*lsX6zH@nk&%Qf|eBY?a_k{M($IZs% zHFD!?;wc|1uEad|t%rS{riUFV=9o0*dmX2|C4Iw>>YI(~%EWS3OLVrM50_6Lxx2@| z+8lDzV?XkJ-DBH3PT77IH8YMpJ}*lRz1!pSHZLVvW6XnXcxn;53s#tDaEVn6^+ZM~ci{(DW@_=F)7t4c+<>AHhtYX=|*pCjy za^_;$u~^PhEN3m2vlYu7i}&wbEO#lEyB5o+Snggdzg$Ge;pRECT#ABb$E8cLoS&}( zvg5m4@q4tfexI)he{p{OK5y~+t26U^8>ed#|53i$&))yYU@adge!s5x{S|^O{?Wzn zn-YEd65Hw^)uS;*T$uYZT#Y z7Rx@xa)V-d#+=&z`g!@Z`lu+U8y3sa#q!R4*~Zzp_&&P1QSXx9 z2NcUUi~U?+Dr@5L_WqTp`aM(EDt2r}?9^Q!h)sW_t{1c-MmnywjfPqHNY;dn3o$W(G^~A3c}Aal#pFZ+?4k z>Nva4@cS_3`)kKqu+#HQp%;kR??WbA^EuYCZat~_Gd1C6GgNwiTTgB~L#O|@wdijD zRr@o!S}Fc7-@`vsXDu_;d7zw}d_QqxT6BN3zDdur-+rHn)*Ed& z`pW)n|3*X4?c{TB%3G%wnK^CspKNrTb*F18CY=YuCm8AJnV$LZ+Z0fSnD_}i*)QWb5mQYe!ULzhPgJ37VG(E zniEDjS-#0@S6@71q(k$5Sk}61lh>+!7-@aT%E^7F@fyeGpG4)`d?(IFW-N(O%t`Z) zCl*oTP%jP3Kf%b7oTSfoq|I uPrstx_OHABzPasTSKHzkQ%?))pTBp2rLL##v18j|{|DK&M%&+pJO2myu-okb literal 0 HcmV?d00001 diff --git a/modules/forensics/windows/prefetch/insert_prefetch_file/insert_prefetch_file.pp b/modules/forensics/windows/prefetch/insert_prefetch_file/insert_prefetch_file.pp new file mode 100644 index 000000000..2caa06701 --- /dev/null +++ b/modules/forensics/windows/prefetch/insert_prefetch_file/insert_prefetch_file.pp @@ -0,0 +1,7 @@ +$json_inputs = base64('decode', $::base64_inputs) +$secgen_parameters=parsejson($json_inputs) +$prefetch_file_name=$secgen_parameters['prefetch_file_name'] + +class { 'insert_prefetch_file': + prefetch_file_name => $prefetch_file_name[0], +} \ No newline at end of file diff --git a/modules/forensics/windows/prefetch/insert_prefetch_file/manifests/init.pp b/modules/forensics/windows/prefetch/insert_prefetch_file/manifests/init.pp new file mode 100644 index 000000000..24d6a6dd2 --- /dev/null +++ b/modules/forensics/windows/prefetch/insert_prefetch_file/manifests/init.pp @@ -0,0 +1,13 @@ +class insert_prefetch_file ($prefetch_file_name) { + file { 'ensure_prefetch_directory_exists_test': + path => "$system32\\Prefetch", + ensure => directory, + } + + file { 'add_prefetch_file': + path => "$system32\\Prefetch\\$prefetch_file_name", + ensure => 'file', + source => "puppet:///modules/file_transfer_storage_module/$prefetch_file_name", + source_permissions => ignore, + } +} \ No newline at end of file diff --git a/modules/forensics/windows/prefetch/insert_prefetch_file/secgen_metadata.xml b/modules/forensics/windows/prefetch/insert_prefetch_file/secgen_metadata.xml new file mode 100644 index 000000000..f495cdacc --- /dev/null +++ b/modules/forensics/windows/prefetch/insert_prefetch_file/secgen_metadata.xml @@ -0,0 +1,32 @@ + + + + Add prefetch file + Jason Keighley + Apache v2 + Insert prefetch files + + prefetch + forensic_artefact + windows + + + + + prefetch_file_name + + + + + + + + + + + Store files for transfer + + + \ No newline at end of file diff --git a/modules/generators/forensics/prefetch/select_prefetch_file/manifests/.no_puppet b/modules/generators/forensics/prefetch/select_prefetch_file/manifests/.no_puppet new file mode 100644 index 000000000..e69de29bb diff --git a/modules/generators/forensics/prefetch/select_prefetch_file/secgen_local/local.rb b/modules/generators/forensics/prefetch/select_prefetch_file/secgen_local/local.rb new file mode 100644 index 000000000..75beb75ed --- /dev/null +++ b/modules/generators/forensics/prefetch/select_prefetch_file/secgen_local/local.rb @@ -0,0 +1,19 @@ +#!/usr/bin/ruby +require_relative '../../../../../../lib/objects/local_string_generator.rb' +require 'date' + +class SelectPrefetchFile < StringGenerator + attr_accessor :selected_file_path + + def initialize + super + self.module_name = 'Random cat image selector' + self.selected_file_path = Dir["#{FORENSIC_ARTEFACTS_DIR}/prefetch/*"].sample + end + + def generate + self.outputs << Base64.strict_encode64(self.selected_file_path) + end +end + +SelectPrefetchFile.new.run \ No newline at end of file diff --git a/modules/generators/forensics/prefetch/select_prefetch_file/secgen_metadata.xml b/modules/generators/forensics/prefetch/select_prefetch_file/secgen_metadata.xml new file mode 100644 index 000000000..bab3e70f5 --- /dev/null +++ b/modules/generators/forensics/prefetch/select_prefetch_file/secgen_metadata.xml @@ -0,0 +1,18 @@ + + + + Select prefetch file paths + Jason Keighley + Apache v2 + Select prefetch file paths from prefetch resources + + prefetch + forensic_artefact + windows + + + + prefetch + \ No newline at end of file diff --git a/modules/generators/forensics/prefetch/select_prefetch_file/select_prefetch_file.pp b/modules/generators/forensics/prefetch/select_prefetch_file/select_prefetch_file.pp new file mode 100644 index 000000000..e69de29bb diff --git a/modules/utilities/windows/prefetch/enable_prefetch/enable_prefetch.pp b/modules/utilities/windows/prefetch/enable_prefetch/enable_prefetch.pp new file mode 100644 index 000000000..3e59e897f --- /dev/null +++ b/modules/utilities/windows/prefetch/enable_prefetch/enable_prefetch.pp @@ -0,0 +1 @@ +include enable_prefetch \ No newline at end of file diff --git a/modules/utilities/windows/prefetch/enable_prefetch/manifests/init.pp b/modules/utilities/windows/prefetch/enable_prefetch/manifests/init.pp new file mode 100644 index 000000000..bbe5e369b --- /dev/null +++ b/modules/utilities/windows/prefetch/enable_prefetch/manifests/init.pp @@ -0,0 +1,33 @@ +class enable_prefetch { + # exec { 'add_prefetch_parameter_keys_to_registry': + # # command => 'C:\windows\system32\cmd.exe /C reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnablePrefetcher /t REG_DWORD /d 3 /f', + # } + # + # exec { 'add_prefetcher_keys_to_registry': + # require => Exec['add_prefetch_parameter_keys_to_registry'], + # command => 'C:\windows\system32\cmd.exe /C reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Prefetcher" /v MaxPrefetchFiles /t REG_DWORD /d 8192 /f', + # } + # + # exec { 'enable_MMAgent -OperationAPI': + # require => Exec['add_prefetcher_keys_to_registry'], + # command => 'C:\windows\system32\cmd.exe /C Enable-MMAgent -OperationAPI', + # } + # + # exec { 'start_sysmain': + # require => Exec['enable_MMAgent -OperationAPI'], + # command => 'C:\windows\system32\cmd.exe /C net start sysmain', + # } + + exec { 'enable_prefetcher': + command => 'C:\windows\system32\cmd.exe /C reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnableSuperfetch /t REG_DWORD /d 3 /f', + } + + exec { 'enable_superfetcher': + command => 'C:\windows\system32\cmd.exe /C reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnablePrefetcher /t REG_DWORD /d 1 /f', + } + + exe { 'reboot': + command => 'C:\windows\system32\cmd.exe /C shutdown /g /t 0', + } + +} \ No newline at end of file diff --git a/modules/utilities/windows/prefetch/enable_prefetch/secgen_metadata.xml b/modules/utilities/windows/prefetch/enable_prefetch/secgen_metadata.xml new file mode 100644 index 000000000..05ed9ff05 --- /dev/null +++ b/modules/utilities/windows/prefetch/enable_prefetch/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + Enable prefetch + Jason Keighley + Apache v2 + Enable prefetch files on a system + + prefetch + windows + + + + + \ No newline at end of file diff --git a/modules/utilities/windows/prefetch/place_prefetch_file/manifests/init.pp b/modules/utilities/windows/prefetch/place_prefetch_file/manifests/init.pp new file mode 100644 index 000000000..12295ed54 --- /dev/null +++ b/modules/utilities/windows/prefetch/place_prefetch_file/manifests/init.pp @@ -0,0 +1,5 @@ +class place_prefetch_files ($prefetch_files) { + file { $prefetch_files: + path => $prefetch_files + } +} \ No newline at end of file diff --git a/modules/utilities/windows/prefetch/place_prefetch_file/place_prefetch_file.pp b/modules/utilities/windows/prefetch/place_prefetch_file/place_prefetch_file.pp new file mode 100644 index 000000000..41cc66289 --- /dev/null +++ b/modules/utilities/windows/prefetch/place_prefetch_file/place_prefetch_file.pp @@ -0,0 +1 @@ +include place_prefetch_files \ No newline at end of file diff --git a/modules/utilities/windows/prefetch/place_prefetch_file/secgen_metadata.xml b/modules/utilities/windows/prefetch/place_prefetch_file/secgen_metadata.xml new file mode 100644 index 000000000..7e38301ad --- /dev/null +++ b/modules/utilities/windows/prefetch/place_prefetch_file/secgen_metadata.xml @@ -0,0 +1,17 @@ + + + + Place prefetch file + Jason Keighley + Apache v2 + Place a prefetch file onto the system + + prefetch + windows + + + + + \ No newline at end of file diff --git a/scenarios/simple_examples/forensic_examples/simple_prefetch_example.xml b/scenarios/simple_examples/forensic_examples/simple_prefetch_example.xml new file mode 100644 index 000000000..16947dbc7 --- /dev/null +++ b/scenarios/simple_examples/forensic_examples/simple_prefetch_example.xml @@ -0,0 +1,18 @@ + + + + + + + windows_server + + + + + + + + + From 880588008d1089f4828197dfe043d248e1309535 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Wed, 13 Dec 2017 17:40:47 +0000 Subject: [PATCH 23/24] Install procmon (Windows process monitor). Install is for windows machines and will automatically also install chocolatey. --- .../install_procmon/install_procmon.pp | 1 + .../install_procmon/manifests/install.pp | 8 ++++++++ .../install_procmon/secgen_metadata.xml | 20 +++++++++++++++++++ .../procmon_program_install_example.xml | 17 ++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 modules/utilities/windows/system_monitors/install_procmon/install_procmon.pp create mode 100644 modules/utilities/windows/system_monitors/install_procmon/manifests/install.pp create mode 100644 modules/utilities/windows/system_monitors/install_procmon/secgen_metadata.xml create mode 100644 scenarios/examples/services_utilities_examples/procmon_program_install_example.xml diff --git a/modules/utilities/windows/system_monitors/install_procmon/install_procmon.pp b/modules/utilities/windows/system_monitors/install_procmon/install_procmon.pp new file mode 100644 index 000000000..6c6349be9 --- /dev/null +++ b/modules/utilities/windows/system_monitors/install_procmon/install_procmon.pp @@ -0,0 +1 @@ +include install_procmon::install \ No newline at end of file diff --git a/modules/utilities/windows/system_monitors/install_procmon/manifests/install.pp b/modules/utilities/windows/system_monitors/install_procmon/manifests/install.pp new file mode 100644 index 000000000..40baf889b --- /dev/null +++ b/modules/utilities/windows/system_monitors/install_procmon/manifests/install.pp @@ -0,0 +1,8 @@ +class install_procmon::install { + include chocolatey + + package { 'procmon': + ensure => installed, + provider => 'chocolatey', + } +} \ No newline at end of file diff --git a/modules/utilities/windows/system_monitors/install_procmon/secgen_metadata.xml b/modules/utilities/windows/system_monitors/install_procmon/secgen_metadata.xml new file mode 100644 index 000000000..26006c702 --- /dev/null +++ b/modules/utilities/windows/system_monitors/install_procmon/secgen_metadata.xml @@ -0,0 +1,20 @@ + + + + Procmon install + Jason Keighley + Apache v2 + An installation of procmon + + system_monitors + windows + + + + + + Chocolatey install + + \ No newline at end of file diff --git a/scenarios/examples/services_utilities_examples/procmon_program_install_example.xml b/scenarios/examples/services_utilities_examples/procmon_program_install_example.xml new file mode 100644 index 000000000..3b0c59d7e --- /dev/null +++ b/scenarios/examples/services_utilities_examples/procmon_program_install_example.xml @@ -0,0 +1,17 @@ + + + + + + + windows_server_with_procmon + + + + + + + + From b5b29416f2e913d8e3b946e801677fd966440d62 Mon Sep 17 00:00:00 2001 From: Jjk422 Date: Mon, 21 May 2018 14:16:27 +0100 Subject: [PATCH 24/24] Conflict and non user input module quick fix: - Fixes conflicts with the main SecGen branch. - Also adds a fix for the chocolatey module (removes registry value as seems to be incompatable with current registry module function RegistryKeyEx) - Adds notify to show end of install for sqlite browser module Note: - Currently only non user input modules work with the new SecGen code, this seems to be due to a lack of a windows secgen_functions build module (current module only runs for linux) - The user input modules will be addressed in the next commit. --- lib/templates/Puppetfile.erb | 5 +- .../sqlite_browser/manifests/install.pp | 2 + .../chocolatey/manifests/install.pp | 2 +- .../multiple_module_example.xml | 196 ++++++++++-------- 4 files changed, 110 insertions(+), 95 deletions(-) diff --git a/lib/templates/Puppetfile.erb b/lib/templates/Puppetfile.erb index b53a5521a..05da9c2a8 100644 --- a/lib/templates/Puppetfile.erb +++ b/lib/templates/Puppetfile.erb @@ -8,9 +8,10 @@ forge "https://forgeapi.puppetlabs.com" -mod 'puppetlabs-stdlib', '4.18.0' # stdlib enables parsejson() in manifests and other useful functions +mod 'puppetlabs-stdlib', '4.6.0' # stdlib enables parsejson() in manifests and other useful functions +mod 'puppetlabs-powershell', '2.1.0' +mod 'puppetlabs-registry', '1.0.0' mod 'SecGen-secgen_functions', :path => '<%= SECGEN_FUNCTIONS_PUPPET_DIR %>' -# mod 'puppetlabs-powershell', '2.1.0' <% @currently_processing_system.module_selections.each do |selected_module| -%> <% case selected_module.module_type diff --git a/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp b/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp index 6fb04964c..49c428d97 100644 --- a/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp +++ b/modules/utilities/windows/database_editor/sqlite_browser/manifests/install.pp @@ -7,4 +7,6 @@ ensure => installed, provider => 'chocolatey', } + + notice('Sqlite browser install finished') } \ No newline at end of file diff --git a/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp b/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp index 4a7fb9c76..9778bf3e0 100644 --- a/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp +++ b/modules/utilities/windows/repository_managers/chocolatey/manifests/install.pp @@ -22,6 +22,6 @@ timeout => $::chocolatey::choco_install_timeout_seconds, logoutput => $::chocolatey::log_output, environment => ["ChocolateyInstall=${::chocolatey::choco_install_location}"], - require => Registry_value['ChocolateyInstall environment value'], + # require => Registry_value['ChocolateyInstall environment value'], } } diff --git a/scenarios/simple_examples/forensic_examples/multiple_module_example.xml b/scenarios/simple_examples/forensic_examples/multiple_module_example.xml index 2e640d675..6d8a8499d 100644 --- a/scenarios/simple_examples/forensic_examples/multiple_module_example.xml +++ b/scenarios/simple_examples/forensic_examples/multiple_module_example.xml @@ -8,112 +8,124 @@ windows_box - + + + + - - - - - 100 - - - 3rd july 2013 15:16:20 - - - 5th june 2015 15:16:20 - - - 10 - - - 4th july 2013 12:00:00 - - - 4th july 2013 15:00:00 - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - - - - C:\Users\vagrant\Desktop\Illegal_image.jpg - - + + + + + + + - - - C:\Secret - - + + + + + + - - - C:\Users\vagrant\Desktop\Crime.txt - - - I robbed a bank last week, got about 1 mil. I think I'm set now unless the rozzers find me. - - + + + + + + + + + - - - C:\Users\vagrant\Desktop\Crime.txt - - - - - 4th july 2013 12:00:00 - - - 4th july 2013 15:00:00 - - - - + + + + + + + + + + + + + + + + - - - C:\Users\vagrant\Desktop\Crime.txt - - - - - 4th july 2013 12:00:00 - - - 4th july 2013 15:00:00 - - - - + + + + + + + + + + + + + + + + - - - C:\Users\vagrant\Desktop\Crime.txt - - - - - + + + + + + + + + - - - C:\Users\vagrant\Desktop\Hello - - + + + + + + - - - C:\Users\vagrant\Desktop\Hello - - + + + + + +