diff --git a/.kitchen.appveyor.yml b/.kitchen.appveyor.yml index e350da9e..3986ab23 100644 --- a/.kitchen.appveyor.yml +++ b/.kitchen.appveyor.yml @@ -21,6 +21,12 @@ suites: - name: app run_list: - recipe[test::app] + - name: module + run_list: + - recipe[test::module] + - name: pool + run_list: + - recipe[test::pool] - name: root run_list: - recipe[test::root] @@ -30,9 +36,6 @@ suites: - name: site run_list: - recipe[test::site] - - name: pool - run_list: - - recipe[test::pool] - name: vdir run_list: - recipe[test::vdir] diff --git a/.kitchen.yml b/.kitchen.yml index 2ae3a42c..b084a829 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -26,6 +26,12 @@ suites: - name: app run_list: - recipe[test::app] + - name: module + run_list: + - recipe[test::module] + - name: pool + run_list: + - recipe[test::pool] - name: root run_list: - recipe[test::root] @@ -35,9 +41,6 @@ suites: - name: site run_list: - recipe[test::site] - - name: pool - run_list: - - recipe[test::pool] - name: vdir run_list: - recipe[test::vdir] \ No newline at end of file diff --git a/.rubocop.yml b/.rubocop.yml index fd96e390..f1d33855 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -5,6 +5,7 @@ Lint/ParenthesesAsGroupedExpression: Style/PredicateName: Exclude: - 'test/integration/app/libraries/*' + - 'test/integration/module/libraries/*' - 'test/integration/pool/libraries/*' - 'test/integration/root/libraries/*' - 'test/integration/section/libraries/*' diff --git a/libraries/section_helper.rb b/libraries/section_helper.rb index a73353ed..8cea5e5c 100644 --- a/libraries/section_helper.rb +++ b/libraries/section_helper.rb @@ -32,6 +32,27 @@ def unlock(node, section, location = '', returns = [0]) cmd_list_section node, :unlock, section, location, returns end + def override_mode(node, action, section, location = '', returns = [0]) + cmd_list_section(node, action, section, location, returns) + end + + def get_current_lock(node, section, location = '') + command_path = 'MACHINE/WEBROOT/APPHOST' + command_path << "/#{location}" if location + cmd = "#{appcmd(node)} list config \"#{command_path}}\"" + cmd << " -section:#{section} -commit:apphost /config:* /xml" + result = shell_out cmd + if result.stderr.empty? + xml = result.stdout + doc = Document.new xml + value(doc.root, 'CONFIG/@overrideMode') + else + Chef::Log.info(result.stderr) + end + + nil + end + def cmd_section(node, check, section, location, returns) cmd = "#{appcmd(node)} set config \"MACHINE/WEBROOT/APPHOST/#{location}\"" cmd << " -section:\"#{section}\" -overrideMode:#{check}" @@ -47,21 +68,11 @@ def cmd_section(node, check, section, location, returns) end def cmd_list_section(node, action, section, location, returns) - command_path = 'MACHINE/WEBROOT/APPHOST' - command_path << "/#{location}" if location - cmd = "#{appcmd(node)} list config \"#{command_path}}\"" - cmd << " -section:#{section} -commit:apphost /config:* /xml" - result = shell_out cmd - if result.stderr.empty? - xml = result.stdout - doc = Document.new xml - check = action == :lock ? 'Deny' : 'Allow' - unless value(doc.root, 'CONFIG/@overrideMode') == check - cmd_section node, check, section, location, returns - end - else - Chef::Log.info(result.stderr) - end + current_lock = get_current_lock(node, section, location) + check = action if action == 'Inherit' + check = (action == :lock ? 'Deny' : 'Allow') if action != 'Inherit' + + cmd_section node, check, section, location, returns unless current_lock == check end end end diff --git a/providers/module.rb b/providers/module.rb deleted file mode 100644 index 1ab764de..00000000 --- a/providers/module.rb +++ /dev/null @@ -1,123 +0,0 @@ -# -# Author:: Jon DeCamp () -# Cookbook:: iis -# Provider:: site -# -# Copyright:: 2013-2016, Nordstrom, 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. -# - -require 'chef/mixin/shell_out' -include Chef::Mixin::ShellOut -include Opscode::IIS::Helper -include Opscode::IIS::Processors - -# Support whyrun -def whyrun_supported? - true -end - -# appcmd syntax for adding modules -# appcmd add module /name:string /type:string /preCondition:string -action :add do - if !@current_resource.exists - converge_by("add IIS module #{new_resource.module_name}") do - cmd = "#{appcmd(node)} add module /module.name:\"#{new_resource.module_name}\"" - - if new_resource.application - cmd << " /app.name:\"#{new_resource.application}\"" - end - - cmd << " /type:\"#{new_resource.type}\"" if new_resource.type - - if new_resource.precondition - cmd << " /preCondition:\"#{new_resource.precondition}\"" - end - - shell_out!(cmd, returns: [0, 42]) - - Chef::Log.info("#{new_resource} added module '#{new_resource.module_name}'") - end - else - Chef::Log.debug("#{new_resource} module already exists - nothing to do") - end -end - -action :delete do - if @current_resource.exists - converge_by("delete IIS module #{new_resource.module_name}") do - cmd = "#{appcmd(node)} delete module /module.name:\"#{new_resource.module_name}\"" - if new_resource.application - cmd << " /app.name:\"#{new_resource.application}\"" - end - - shell_out!(cmd, returns: [0, 42]) - end - - Chef::Log.info("#{new_resource} deleted") - else - Chef::Log.debug("#{new_resource} module does not exist - nothing to do") - end -end - -# appcmd syntax for installing native modules -# appcmd install module /name:string /add:string(true|false) /image:string -action :install do - if !@current_resource.exists - converge_by("install IIS module #{new_resource.module_name}") do - cmd = "#{appcmd(node)} install module /name:\"#{new_resource.module_name}\"" - cmd << " /add:\"#{new_resource.add}\"" unless new_resource.add.nil? - cmd << " /image:\"#{new_resource.image}\"" if new_resource.image - cmd << " /preCondition:\"#{new_resource.precondition}\"" if new_resource.precondition - - shell_out!(cmd, returns: [0, 42]) - - Chef::Log.info("#{new_resource} installed module '#{new_resource.module_name}'") - end - else - Chef::Log.debug("#{new_resource} module already exists - nothing to do") - end -end - -# appcmd syntax for uninstalling native modules -# appcmd uninstall module -action :uninstall do - if @current_resource.exists - converge_by("uninstall IIS module #{new_resource.module_name}") do - cmd = "#{appcmd(node)} uninstall module \"#{new_resource.module_name}\"" - - shell_out!(cmd, returns: [0, 42]) - end - - Chef::Log.info("#{new_resource} uninstalled module '#{new_resource.module_name}'") - else - Chef::Log.debug("#{new_resource} module does not exists - nothing to do") - end -end - -def load_current_resource - @current_resource = Chef::Resource::IisModule.new(new_resource.name) - @current_resource.module_name(new_resource.module_name) - cmd = if new_resource.application - shell_out("#{appcmd(node)} list module /module.name:\"#{new_resource.module_name}\" /app.name:\"#{new_resource.application}\"") - else - shell_out("#{appcmd(node)} list module /module.name:\"#{new_resource.module_name}\"") - end - - # 'MODULE "Module Name" ( type:module.type, preCondition:condition )' - # 'MODULE "Module Name" ( native, preCondition:condition )' - - Chef::Log.debug("#{new_resource} list module command output: #{cmd.stdout}") - @current_resource.exists = !cmd.stdout.empty? -end diff --git a/resources/app.rb b/resources/app.rb index b7529ce5..d19ad5ad 100644 --- a/resources/app.rb +++ b/resources/app.rb @@ -32,7 +32,7 @@ load_current_value do |desired| site_name desired.site_name - cmd = shell_out("#{appcmd(node)} list app") + cmd = shell_out("#{appcmd(node)} list app \"#{desired.site_name}#{desired.path}\"") Chef::Log.debug("#{appcmd(node)} list app command output: #{cmd.stdout}") if cmd.stderr.empty? Chef::Log.debug('Running regex') diff --git a/resources/module.rb b/resources/module.rb index 2d5c5d5c..dc127b5c 100644 --- a/resources/module.rb +++ b/resources/module.rb @@ -1,9 +1,8 @@ # -# Author:: Jon DeCamp () # Cookbook:: iis # Resource:: module # -# Copyright:: 2012-2016, Nordstrom, Inc. +# Copyright:: 2017, Chef Software, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,14 +17,111 @@ # limitations under the License. # -actions :add, :delete, :install, :uninstall +include Opscode::IIS::Helper +include Opscode::IIS::Processors +include Opscode::IIS::SectionHelper + +property :module_name, String, name_property: true +property :type, String +property :add, [true, false], default: false +property :image, String +property :precondition, String +property :application, String +property :previous_lock, String + default_action :add -attribute :module_name, kind_of: String, name_attribute: true -attribute :type, kind_of: String, default: nil -attribute :add, kind_of: [FalseClass, TrueClass], default: nil -attribute :image, kind_of: String, default: nil -attribute :precondition, kind_of: String, default: nil -attribute :application, kind_of: String, default: nil +load_current_value do |desired| + module_name desired.module_name + application desired.application if desired.application + cmd = "#{appcmd(node)} list module /module.name:\"#{desired.module_name}\"" + cmd << " /app.name:\"#{desired.application}\"" if desired.application + + cmd_result = shell_out cmd + # 'MODULE "Module Name" ( type:module.type, preCondition:condition )' + # 'MODULE "Module Name" ( native, preCondition:condition )' + + Chef::Log.debug("#{desired.name} list module command output: #{cmd_result.stdout}") + unless cmd_result.stdout.empty? + previous_lock get_current_lock(node, 'system.webServer/modules', desired.application) + cmd = "#{appcmd(node)} list module /module.name:\"#{desired.module_name}\"" + cmd << " /app.name:\"#{desired.application}\"" if desired.application + cmd << ' /config:* /xml' + cmd_result = shell_out cmd + if cmd_result.stderr.empty? + xml = cmd_result.stdout + doc = Document.new(xml) + type value doc.root, 'MODULE/@type' + precondition value doc.root, 'MODULE/@preCondition' + end + end +end + +# appcmd syntax for adding modules +# appcmd add module /name:string /type:string /preCondition:string +action :add do + if !current_resource.type + converge_by("add IIS module #{new_resource.module_name}") do + unlock(node, 'system.webServer/modules', new_resource.application) + cmd = "#{appcmd(node)} add module /module.name:\"#{new_resource.module_name}\"" + cmd << " /app.name:\"#{new_resource.application}\"" if new_resource.application + cmd << " /type:\"#{new_resource.type}\"" if new_resource.type + cmd << " /preCondition:\"#{new_resource.precondition}\"" if new_resource.precondition + + shell_out!(cmd, returns: [0, 42]) + override_mode(node, current_resource.previous_lock, 'system.webServer/modules', new_resource.application) + end + else + Chef::Log.debug("#{new_resource} module already exists - nothing to do") + end +end + +action :delete do + if current_resource.type + converge_by("delete IIS module #{new_resource.module_name}") do + unlock(node, 'system.webServer/modules', new_resource.application) + cmd = "#{appcmd(node)} delete module /module.name:\"#{new_resource.module_name}\"" + cmd << " /app.name:\"#{new_resource.application}\"" if new_resource.application + + shell_out!(cmd, returns: [0, 42]) + override_mode(node, current_resource.previous_lock, 'system.webServer/modules', new_resource.application) + end + else + Chef::Log.debug("#{new_resource} module does not exist - nothing to do") + end +end + +# appcmd syntax for installing native modules +# appcmd install module /name:string /add:string(true|false) /image:string +action :install do + if !current_resource.type + converge_by("install IIS module #{new_resource.module_name}") do + unlock(node, 'system.webServer/modules', new_resource.application) + cmd = "#{appcmd(node)} install module /name:\"#{new_resource.module_name}\"" + cmd << " /add:\"#{new_resource.add}\"" unless new_resource.add.nil? + cmd << " /image:\"#{new_resource.image}\"" if new_resource.image + cmd << " /preCondition:\"#{new_resource.precondition}\"" if new_resource.precondition + + shell_out!(cmd, returns: [0, 42]) + override_mode(node, current_resource.previous_lock, 'system.webServer/modules', new_resource.application) + end + else + Chef::Log.debug("#{new_resource} module already exists - nothing to do") + end +end + +# appcmd syntax for uninstalling native modules +# appcmd uninstall module +action :uninstall do + if current_resource.type + converge_by("uninstall IIS module #{new_resource.module_name}") do + unlock(node, 'system.webServer/modules', new_resource.application) + cmd = "#{appcmd(node)} uninstall module \"#{new_resource.module_name}\"" -attr_accessor :exists + shell_out!(cmd, returns: [0, 42]) + override_mode(node, current_resource.previous_lock, 'system.webServer/modules', new_resource.application) + end + else + Chef::Log.debug("#{new_resource} module does not exists - nothing to do") + end +end diff --git a/test/cookbooks/test/recipes/module.rb b/test/cookbooks/test/recipes/module.rb new file mode 100644 index 00000000..45541939 --- /dev/null +++ b/test/cookbooks/test/recipes/module.rb @@ -0,0 +1,38 @@ +# +# Cookbook:: test +# Recipe:: module +# +# copyright: 2017, Chef Software, 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. + +include_recipe 'iis' + +directory "#{node['iis']['docroot']}\\v1_1" do + recursive true +end + +iis_app 'Default Web Site' do + path '/v1_1' + application_pool 'DefaultAppPool' + physical_path "#{node['iis']['docroot']}/v1_1" + enabled_protocols 'http,net.pipe' + action :add +end + +iis_module 'example module' do + application 'Default Web Site/v1_1' + type 'System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' + precondition 'managedHandler' + action :add +end diff --git a/test/integration/module/README.md b/test/integration/module/README.md new file mode 100644 index 00000000..264be453 --- /dev/null +++ b/test/integration/module/README.md @@ -0,0 +1,3 @@ +# iis_module InSpec Profile + +This will allow the testing of iis_module until it can be added into inspec. diff --git a/test/integration/module/controls/module_spec.rb b/test/integration/module/controls/module_spec.rb new file mode 100644 index 00000000..5110b4d7 --- /dev/null +++ b/test/integration/module/controls/module_spec.rb @@ -0,0 +1,18 @@ +# encoding: utf-8 +# copyright: 2017, Chef Software, Inc. +# license: All rights reserved + +title 'iis_module section' + +describe service('W3SVC') do + it { should be_installed } + it { should be_running } + its ('startmode') { should eq 'Auto' } +end + +describe iis_module('example module', 'Default Web Site/v1_1') do + it { should exist } + it { should have_name('example module') } + it { should have_pre_condition('managedHandler') } + it { should have_type('System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35') } +end diff --git a/test/integration/module/inspec.yml b/test/integration/module/inspec.yml new file mode 100644 index 00000000..3e1fd345 --- /dev/null +++ b/test/integration/module/inspec.yml @@ -0,0 +1,6 @@ +name: module +title: iis_module InSpec Profile +copyright: 2017, Chef Software, Inc. +license: All Rights Reserved +summary: An InSpec Compliance Profile for iis_module +version: 0.1.0 diff --git a/test/integration/module/libraries/.gitkeep b/test/integration/module/libraries/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/integration/module/libraries/iis_module.rb b/test/integration/module/libraries/iis_module.rb new file mode 100644 index 00000000..fce23bfa --- /dev/null +++ b/test/integration/module/libraries/iis_module.rb @@ -0,0 +1,98 @@ +# encoding: utf-8 +# frozen_string_literal: true +# check for application modules in IIS +# Usage: +# describe iis_module('module_name', 'Default Web Site') do +# it { should exist } +# it { should have_name('module_name') } +# it { should have_type('System.Web.Security.FileAuthorizationModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a') } +# end +# +# Note: this is only supported in windows 2012 and later + +class IisModule < Inspec.resource(1) + name 'iis_module' + desc 'Tests IIS module configuration on windows. Supported in server 2012+ only' + example " + describe iis_module('module_name', 'Default Web Site') do + it { should exist } + it { should have_name('module_name') } + it { should have_type('System.Web.Security.FileAuthorizationModule, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a') } + end + " + + def initialize(module_name, application) + @module_name = module_name + @application = application + @cache = nil + + @module_provider = ModuleProvider.new(inspec) + + # verify that this resource is only supported on Windows + return skip_resource 'The `iis_module` resource is not supported on your OS.' unless inspec.os.windows? + end + + def name + iis_module[:name] + end + + def type + iis_module[:type] + end + + def pre_condition + iis_module[:pre_condition] + end + + def exists? + !iis_module.nil? && !iis_module[:name].nil? + end + + def has_name?(module_name) + iis_module.nil? ? false : iis_module[:name] == module_name + end + + def has_type?(module_type) + iis_module.nil? ? false : iis_module[:type] == module_type + end + + def has_pre_condition?(pre_condition) + iis_module.nil? ? false : iis_module[:pre_condition] == pre_condition + end + + def to_s + "iis_module `#{@module_name}` - `#{@application}`" + end + + def iis_module + return @cache unless @cache.nil? + @cache = @module_provider.iis_module(@module_name, @application) unless @module_provider.nil? + end +end + +class ModuleProvider + attr_reader :inspec + + def initialize(inspec) + @inspec = inspec + end + + # want to populate everything using one powershell command here and spit it out as json + def iis_module(module_name, application) + command = "Import-Module WebAdministration; Get-WebManagedModule -Name '#{module_name}' -PSPath 'IIS:\\sites\\#{application}' | Select-Object name, type, preCondition | ConvertTo-Json" + cmd = @inspec.command(command) + + begin + mod = JSON.parse(cmd.stdout) + rescue JSON::ParserError => _e + return {} + end + + # map our values to a hash table + { + name: mod['name'], + type: mod['type'], + pre_condition: mod['preCondition'], + } + end +end