diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2021-08-16 21:36:46 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2021-08-16 21:36:46 -0700 |
commit | 28d4c2d010b3545a0092d9ad4136e092b8872f9a (patch) | |
tree | 7832ba96747ff5ea3035f5b15dd1f70855be7093 | |
parent | 9657ee55c8d9a86c741dfacca9ff7b4996dcc352 (diff) | |
download | chef-28d4c2d010b3545a0092d9ad4136e092b8872f9a.tar.gz |
WIP
Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
-rw-r--r-- | lib/chef/compliance/input.rb | 108 | ||||
-rw-r--r-- | lib/chef/compliance/input_collection.rb | 107 | ||||
-rw-r--r-- | lib/chef/compliance/profile.rb | 13 | ||||
-rw-r--r-- | lib/chef/compliance/profile_collection.rb | 2 | ||||
-rw-r--r-- | lib/chef/compliance/runner.rb | 22 | ||||
-rw-r--r-- | lib/chef/compliance/waiver_collection.rb | 10 | ||||
-rw-r--r-- | lib/chef/dsl/compliance.rb | 5 | ||||
-rw-r--r-- | lib/chef/dsl/reader_helpers.rb | 44 | ||||
-rw-r--r-- | lib/chef/dsl/universal.rb | 2 | ||||
-rw-r--r-- | lib/chef/event_dispatch/base.rb | 26 | ||||
-rw-r--r-- | lib/chef/formatters/doc.rb | 35 | ||||
-rw-r--r-- | lib/chef/resource/inspec_input.rb | 89 | ||||
-rw-r--r-- | lib/chef/resource/inspec_waiver.rb | 118 | ||||
-rw-r--r-- | lib/chef/resources.rb | 2 | ||||
-rw-r--r-- | lib/chef/run_context.rb | 10 | ||||
-rw-r--r-- | lib/chef/run_context/cookbook_compiler.rb | 40 |
16 files changed, 608 insertions, 25 deletions
diff --git a/lib/chef/compliance/input.rb b/lib/chef/compliance/input.rb new file mode 100644 index 0000000000..30efc04eaf --- /dev/null +++ b/lib/chef/compliance/input.rb @@ -0,0 +1,108 @@ +# +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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 "yaml" + +class Chef + module Compliance + class Input + # @return [Boolean] if the input has been enabled + attr_accessor :enabled + + # @return [String] The name of the cookbook that the input is in + attr_accessor :cookbook_name + + # @return [String] The full path on the host to the input yml file + attr_accessor :path + + # @return [String] the pathname in the cookbook + attr_accessor :pathname + + # Event dispatcher for this run. + # + # @return [Chef::EventDispatch::Dispatcher] + # + attr_accessor :events + + def initialize(events, data, path, cookbook_name) + @events = events + @data = data + @cookbook_name = cookbook_name + @path = path + @pathname = File.basename(path, File.extname(path)) + disable! + end + + # @return [Boolean] if the input has been enabled + # + def enabled? + !!@enabled + end + + # Set the input to being enabled + # + def enable! + events.compliance_input_enabled(cookbook_name, pathname, path) + @enabled = true + end + + # Set the input as being disabled + # + def disable! + @enabled = false + end + + # Render the input in a way that it can be consumed by inspec + # + def for_inspec + path + end + + HIDDEN_IVARS = [ :@events ].freeze + + # Omit the event object from error output + # + def inspect + ivar_string = (instance_variables.map(&:to_sym) - HIDDEN_IVARS).map do |ivar| + "#{ivar}=#{instance_variable_get(ivar).inspect}" + end.join(", ") + "#<#{self.class}:#{object_id} #{ivar_string}>" + end + + # Helper to construct a input object from a hash. Since the path and + # cookbook_name are required this is probably not externally useful. + # + def self.from_hash(events, hash, path, cookbook_name) + new(events, hash, path, cookbook_name) + end + + # Helper to construct a input object from a yaml string. Since the path + # and cookbook_name are required this is probably not externally useful. + # + def self.from_yaml(events, string, path, cookbook_name) + from_hash(events, YAML.load(string), path, cookbook_name) + end + + # @param filename [String] full path to the yml file in the cookbook + # @param cookbook_name [String] cookbook that the input is in + # + def self.from_file(events, filename, cookbook_name) + from_yaml(events, IO.read(filename), filename, cookbook_name) + end + end + end +end diff --git a/lib/chef/compliance/input_collection.rb b/lib/chef/compliance/input_collection.rb new file mode 100644 index 0000000000..97f13f13ed --- /dev/null +++ b/lib/chef/compliance/input_collection.rb @@ -0,0 +1,107 @@ +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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_relative "input" + +class Chef + module Compliance + class InputCollection < Array + + # Event dispatcher for this run. + # + # @return [Chef::EventDispatch::Dispatcher] + # + attr_reader :events + + # @return [Hash] Hash of raw values (not from files) + attr_reader :raw_hash + + def initialize(events) + @events = events + @raw_hash = {} + end + + # Add a input to the input collection. The cookbook_name needs to be determined by the + # caller and is used in the `include_input` API to match on. The path should be the complete + # path on the host of the yml file, including the filename. + # + # @param path [String] + # @param cookbook_name [String] + # + def from_file(filename, cookbook_name) + new_input = Input.from_file(events, filename, cookbook_name) + self << new_input + events.compliance_input_loaded(cookbook_name, new_input.pathname, filename) + end + + # @return [Array<Input>] inspec inputs which are enabled in a form suitable to pass to inspec + # + def for_inspec + select(&:enabled?).each_with_object([]) { |input, arry| arry << input.for_inspec } + end + + # DSL method to enable input files. This matches on the name of the control being waived, it + # does not match on the filename of the input file. + # + # @example Specific input file in a cookbook + # + # include_input "acme_cookbook::ssh-001" + # + # @example Every input file in a cookbook + # + # include_input "acme_cookbook" + # + # @example Matching inputs by regexp in a cookbook + # + # include_input "acme_cookbook::ssh.*" + # + # @example Matching inputs by regexp in any cookbook in the cookbook collection + # + # include_input ".*::ssh.*" + # + def include_input(arg) + # if we're given a hash argument just shove it in the raw_hash + if arg.is_a?(Hash) + raw_hash.merge!(arg) + return + end + + (cookbook_name, input_name) = arg.split("::") + + input_name = "default" if input_name.nil? + + inputs = select { |input| /^#{cookbook_name}$/.match?(input.cookbook_name) && /^#{input_name}$/.match?(input.pathname) } + + if inputs.empty? + raise "No inspec inputs matching '#{input_name}' found in cookbooks matching '#{cookbook_name}'" + end + + inputs.each(&:enable!) + end + + HIDDEN_IVARS = [ :@events ].freeze + + # Omit the event object from error output + # + def inspect + ivar_string = (instance_variables.map(&:to_sym) - HIDDEN_IVARS).map do |ivar| + "#{ivar}=#{instance_variable_get(ivar).inspect}" + end.join(", ") + "#<#{self.class}:#{object_id} #{ivar_string}>" + end + end + end +end diff --git a/lib/chef/compliance/profile.rb b/lib/chef/compliance/profile.rb index 507c84a5be..ff5ef32031 100644 --- a/lib/chef/compliance/profile.rb +++ b/lib/chef/compliance/profile.rb @@ -18,7 +18,6 @@ class Chef module Compliance class Profile - # @return [Boolean] if the profile has been enabled attr_accessor :enabled @@ -31,10 +30,7 @@ class Chef # @return [String] the pathname in the cookbook attr_accessor :pathname - # Event dispatcher for this run. - # - # @return [Chef::EventDispatch::Dispatcher] - # + # @return [Chef::EventDispatch::Dispatcher] Event dispatcher for this run. attr_reader :events def initialize(events, data, path, cookbook_name) @@ -48,11 +44,15 @@ class Chef end # @return [String] name of the inspec profile from parsing the inspec.yml - # def name @data["name"] end + # @return [String] version of the inspec profile from parsing the inspec.yml + def version + @data["version"] + end + # Raises if the inspec profile is not valid. # def validate! @@ -60,7 +60,6 @@ class Chef end # @return [Boolean] if the profile has been enabled - # def enabled? !!@enabled end diff --git a/lib/chef/compliance/profile_collection.rb b/lib/chef/compliance/profile_collection.rb index 9688314061..e238087628 100644 --- a/lib/chef/compliance/profile_collection.rb +++ b/lib/chef/compliance/profile_collection.rb @@ -41,7 +41,7 @@ class Chef def from_file(path, cookbook_name) new_profile = Profile.from_file(events, path, cookbook_name) self << new_profile - events.compliance_profile_loaded(cookbook_name, new_profile.pathname, new_profile.name, path) + events.compliance_profile_loaded(cookbook_name, path, new_profile.pathname, new_profile.name, new_profile.version) end # @return [Boolean] if any of the profiles are enabled diff --git a/lib/chef/compliance/runner.rb b/lib/chef/compliance/runner.rb index 993939534c..583b663506 100644 --- a/lib/chef/compliance/runner.rb +++ b/lib/chef/compliance/runner.rb @@ -133,8 +133,16 @@ class Chef end end + def inputs_from_raw_hash + safe_input_collection&.raw_hash || {} + end + + def waivers_from_raw_hash + safe_waiver_collection&.raw_hash || {} + end + def inspec_opts - inputs = inputs_from_attributes + inputs = inputs_from_attributes.merge(inputs_from_raw_hash).merge(waivers_from_raw_hash) if node["audit"]["chef_node_attribute_enabled"] inputs["chef_node"] = node.to_h @@ -145,15 +153,23 @@ class Chef backend_cache: node["audit"]["inspec_backend_cache"], inputs: inputs, logger: logger, + # output: STDOUT, output: node["audit"]["quiet"] ? ::File::NULL : STDOUT, report: true, reporter: ["json-automate"], + # reporter: ["cli"], reporter_backtrace_inclusion: node["audit"]["result_include_backtrace"], reporter_message_truncation: node["audit"]["result_message_limit"], waiver_file: waiver_files, + input_file: input_files, } end + def input_files + # the audit cookbook never supported input _files_ being specified via attributes + safe_input_collection&.for_inspec || [] + end + def waiver_files from_attributes = Array(node["audit"]["waiver_file"]) from_cookbooks = safe_waiver_collection&.for_inspec || [] @@ -347,6 +363,10 @@ class Chef def safe_waiver_collection run_context&.waiver_collection end + + def safe_input_collection + run_context&.input_collection + end end end end diff --git a/lib/chef/compliance/waiver_collection.rb b/lib/chef/compliance/waiver_collection.rb index b877c9cecf..8fbf69898b 100644 --- a/lib/chef/compliance/waiver_collection.rb +++ b/lib/chef/compliance/waiver_collection.rb @@ -26,8 +26,12 @@ class Chef # attr_reader :events + # @return [Hash] Hash of raw values (not from files) + attr_reader :raw_hash + def initialize(events) @events = events + @raw_hash = {} end # Add a waiver to the waiver collection. The cookbook_name needs to be determined by the @@ -69,6 +73,12 @@ class Chef # include_waiver ".*::ssh.*" # def include_waiver(arg) + # if we're given a hash argument just shove it in the raw_hash + if arg.is_a?(Hash) + raw_hash.merge!(arg) + return + end + (cookbook_name, waiver_name) = arg.split("::") waiver_name = "default" if waiver_name.nil? diff --git a/lib/chef/dsl/compliance.rb b/lib/chef/dsl/compliance.rb index 8dafbb1a8e..0375c4835a 100644 --- a/lib/chef/dsl/compliance.rb +++ b/lib/chef/dsl/compliance.rb @@ -28,6 +28,11 @@ class Chef def include_waiver(*args) run_context.waiver_collection.include_waiver(*args) end + + # @see Chef::Compliance::inputCollection#include_input + def include_input(*args) + run_context.input_collection.include_input(*args) + end end end end diff --git a/lib/chef/dsl/reader_helpers.rb b/lib/chef/dsl/reader_helpers.rb new file mode 100644 index 0000000000..5ab2290167 --- /dev/null +++ b/lib/chef/dsl/reader_helpers.rb @@ -0,0 +1,44 @@ +# +# Copyright:: Copyright (c) Chef Software Inc. +# License:: Apache License, Version 2.0 +# +# 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_relative "toml" +require_relative "../json_compat" +autoload :YAML, "yaml" + +class Chef + module DSL + module ReaderHelpers + + def parse_file(filename) + case File.extname(filename) + when ".toml" + parse_toml(filename) + when ".yaml", ".yml" + parse_yaml(filename) + when ".json" + parse_json(filename) + end + end + + def parse_json(filename); end + + def parse_toml(filename); end + + def parse_yaml(filename); end + + extend self + end + end +end diff --git a/lib/chef/dsl/universal.rb b/lib/chef/dsl/universal.rb index 84566c6da7..972ebacc7e 100644 --- a/lib/chef/dsl/universal.rb +++ b/lib/chef/dsl/universal.rb @@ -23,6 +23,7 @@ require_relative "chef_vault" require_relative "registry_helper" require_relative "powershell" require_relative "secret" +require_relative "reader_helpers" require_relative "render_helpers" require_relative "toml" require_relative "../mixin/powershell_exec" @@ -50,6 +51,7 @@ class Chef include Chef::DSL::ChefVault include Chef::DSL::RegistryHelper include Chef::DSL::Powershell + include Chef::DSL::ReaderHelpers include Chef::DSL::RenderHelpers include Chef::DSL::Secret include Chef::Mixin::PowershellExec diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index b52c7df29c..08fe2f10fa 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -182,18 +182,42 @@ class Chef # Called when compliance file loading ends def compliance_load_complete; end + # Called when compliance profile loading starts + def profiles_load_start; end + + # Called when compliance profile loading end + def profiles_load_complete; end + + # Called when compliance input loading starts + def inputs_load_start; end + + # Called when compliance input loading end + def inputs_load_complete; end + + # Called when compliance waiver loading starts + def waivers_load_start; end + + # Called when compliance waiver loading end + def waivers_load_complete; end + # Called when a compliance profile is found in a cookbook by the cookbook_compiler - def compliance_profile_loaded(cookbook_name, pathname, name, path); end + def compliance_profile_loaded(cookbook_name, path, pathname, name, version); end # Called when a compliance wavier is found in a cookbook by the cookbook_compiler def compliance_waiver_loaded(cookbook_name, pathname, path); end + # Called when a compliance wavier is found in a cookbook by the cookbook_compiler + def compliance_input_loaded(cookbook_name, pathname, path); end + # Called when a compliance profile is enabled (by include_profile) def compliance_profile_enabled(cookbook_name, pathname, name, path); end # Called when a compliance wavier is enabled (by include_waiver) def compliance_waiver_enabled(cookbook_name, pathanme, path); end + # Called when a compliance wavier is enabled (by include_input) + def compliance_input_enabled(cookbook_name, pathanme, path); end + # Called before attribute files are loaded def attribute_load_start(attribute_file_count); end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index 04a4de715e..0efc0e49ba 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -362,32 +362,45 @@ class Chef end end - def compliance_load_start - start_line("start loading compliance profiles", :red) + # Called when compliance profile loading starts + def profiles_load_start + puts_line("Loading Inspec profile files:") end - def compliance_load_complete - start_line("done loading compliance profiles", :red) + # Called when compliance input loading starts + def inputs_load_start + puts_line("Loading Inspec input files:") + end + + # Called when compliance waiver loading starts + def waivers_load_start + puts_line("Loading Inspec waiver files:") end # Called when a compliance profile is found in a cookbook by the cookbook_compiler - def compliance_profile_loaded(cookbook_name, pathname, name, path) - start_line("profile #{cookbook_name}::#{pathname}", :red) + def compliance_profile_loaded(cookbook_name, path, pathname, name, version) + start_line(" - #{cookbook_name}::#{pathname}", :cyan) + puts " (#{version})", :cyan if version + end + + # Called when a compliance waiver is found in a cookbook by the cookbook_compiler + def compliance_input_loaded(cookbook_name, pathname, path) + puts_line(" - #{cookbook_name}::#{pathname}", :cyan) end - # Called when a compliance wavier is found in a cookbook by the cookbook_compiler + # Called when a compliance waiver is found in a cookbook by the cookbook_compiler def compliance_waiver_loaded(cookbook_name, pathname, path) - start_line("waiver #{cookbook_name}::#{pathname}", :red) + puts_line(" - #{cookbook_name}::#{pathname}", :cyan) end # Called when a compliance profile is enabled (by include_profile) def compliance_profile_enabled(cookbook_name, pathname, name, path) - start_line("enabled profile #{cookbook_name}::#{pathname}", :red) + # puts_line(" * FIXME", :cyan) end - # Called when a compliance wavier is enabled (by include_waiver) + # Called when a compliance waiver is enabled (by include_waiver) def compliance_waiver_enabled(cookbook_name, pathname, path) - start_line("enabled waiver #{cookbook_name}::#{pathname}", :red) + # puts_line(" * FIXME", :cyan) end # (see Base#deprecation) diff --git a/lib/chef/resource/inspec_input.rb b/lib/chef/resource/inspec_input.rb new file mode 100644 index 0000000000..ec26591b8a --- /dev/null +++ b/lib/chef/resource/inspec_input.rb @@ -0,0 +1,89 @@ +# +# Copyright:: Copyright (c) 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. +# + +require_relative "../resource" + +class Chef + class Resource + class InspecInput < Chef::Resource + provides :inspec_input + unified_mode true + + description "Use the **inspec_waiver** resource to add a waiver to the Compliance Phase." + introduced "17.4" + examples <<~DOC + **Add an InSpec waiver to the Compliance Phase**: + + ```ruby + inspec_waiver 'Add waiver entry for control' do + control 'my_inspec_control_01' + run_test false + justification "The subject of this control is not managed by #{ChefUtils::Dist::Infra::PRODUCT} on the systems in policy group \#{node['policy_group']}" + expiration '2022-01-01' + action :add + end + ``` + + **Add an InSpec waiver to the Compliance Phase using the 'name' property to identify the control**: + + ```ruby + inspec_waiver 'my_inspec_control_01' do + justification "The subject of this control is not managed by #{ChefUtils::Dist::Infra::PRODUCT} on the systems in policy group \#{node['policy_group']}" + action :add + end + ``` + DOC + + property :control, String, + name_property: true, + description: "The name of the control being waived" + + property :expiration, String, + description: "The expiration date of the waiver - provided in YYYY-MM-DD format", + callbacks: { + "Expiration date should be a valid calendar date and match the following format: YYYY-MM-DD" => proc { |e| + re = Regexp.new('\d{4}-\d{2}-\d{2}$').freeze + if re.match?(e) + Date.valid_date?(*e.split("-").map(&:to_i)) + else + e.nil? + end + }, + } + + property :run_test, [true, false], + description: "If present and true, the control will run and be reported, but failures in it won’t make the overall run fail. If absent or false, the control will not be run." + + property :justification, String, + description: "Can be any text you want and might include a reason for the waiver as well as who signed off on the waiver." + + action :add do + if new_resource.justification.nil? || new_resource.justification == "" + raise Chef::Exceptions::ValidationFailed, "Entries for an InSpec waiver must have a justification given, this parameter must have a value." + end + + control_hash = {} + control_hash["expiration_date"] = new_resource.expiration.to_s unless new_resource.expiration.nil? + control_hash["run"] = new_resource.run_test unless new_resource.run_test.nil? + control_hash["justification"] = new_resource.justification.to_s + + waiver_hash = { new_resource.control => control_hash } + + include_waiver(waiver_hash) + end + end + end +end diff --git a/lib/chef/resource/inspec_waiver.rb b/lib/chef/resource/inspec_waiver.rb new file mode 100644 index 0000000000..ee1631d584 --- /dev/null +++ b/lib/chef/resource/inspec_waiver.rb @@ -0,0 +1,118 @@ +# +# Copyright:: Copyright (c) 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. +# + +require_relative "../resource" + +class Chef + class Resource + class InspecWaiver < Chef::Resource + provides :inspec_waiver + unified_mode true + + description "Use the **inspec_waiver** resource to add a waiver to the Compliance Phase." + introduced "17.4" + examples <<~DOC + **Add an InSpec waiver to the Compliance Phase**: + + ```ruby + inspec_waiver 'Add waiver entry for control' do + control 'my_inspec_control_01' + run_test false + justification "The subject of this control is not managed by #{ChefUtils::Dist::Infra::PRODUCT} on the systems in policy group \#{node['policy_group']}" + expiration '2022-01-01' + action :add + end + ``` + + **Add an InSpec waiver to the Compliance Phase using the 'name' property to identify the control**: + + ```ruby + inspec_waiver 'my_inspec_control_01' do + justification "The subject of this control is not managed by #{ChefUtils::Dist::Infra::PRODUCT} on the systems in policy group \#{node['policy_group']}" + action :add + end + ``` + DOC + + property :control, String, + name_property: true, + description: "The name of the control being waived" + + property :expiration, String, + description: "The expiration date of the waiver - provided in YYYY-MM-DD format", + callbacks: { + "Expiration date should be a valid calendar date and match the following format: YYYY-MM-DD" => proc { |e| + re = Regexp.new('\d{4}-\d{2}-\d{2}$').freeze + if re.match?(e) + Date.valid_date?(*e.split("-").map(&:to_i)) + else + e.nil? + end + }, + } + + property :run_test, [true, false], + description: "If present and true, the control will run and be reported, but failures in it won’t make the overall run fail. If absent or false, the control will not be run." + + property :justification, String, + description: "Can be any text you want and might include a reason for the waiver as well as who signed off on the waiver." + + property :source, [ Hash, String ] + + action :add do + include_waiver(waiver_hash) + end + + action_class do + # If the source is nil and the control / name_property contains a file separator and is a string of a + # file that exists, then use that as the file (similar to the package provider automatic source property). Otherwise + # just return the source. + # + # @api private + def source + @source ||= + begin + return new_resource.source unless new_resource.source.nil? + return nil unless new_resource.control.count(::File::SEPARATOR) > 0 || new_resource.control.count(::File::ALT_SEPARATOR) > 0 + return nil unless File.exist?(new_resource.control) + + new_resource.control + end + end + + def waiver_hash + case source + when Hash + source + when String + parse_file(source) + when nil + if new_resource.justification.nil? || new_resource.justification == "" + raise Chef::Exceptions::ValidationFailed, "Entries for an InSpec waiver must have a justification given, this parameter must have a value." + end + + control_hash = {} + control_hash["expiration_date"] = new_resource.expiration.to_s unless new_resource.expiration.nil? + control_hash["run"] = new_resource.run_test unless new_resource.run_test.nil? + control_hash["justification"] = new_resource.justification.to_s + + { new_resource.control => control_hash } + end + end + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 6b30a58aff..ac5ec5d8e0 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -73,6 +73,8 @@ require_relative "resource/homebrew_package" require_relative "resource/homebrew_tap" require_relative "resource/homebrew_update" require_relative "resource/ifconfig" +require_relative "resource/inspec_input" +require_relative "resource/inspec_waiver" require_relative "resource/inspec_waiver_file_entry" require_relative "resource/kernel_module" require_relative "resource/ksh" diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index d1184167d4..94f8a316e0 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -25,6 +25,7 @@ require_relative "log" require_relative "recipe" require_relative "run_context/cookbook_compiler" require_relative "event_dispatch/events_output_stream" +require_relative "compliance/input_collection" require_relative "compliance/waiver_collection" require_relative "compliance/profile_collection" require_relative "train_transport" @@ -138,6 +139,12 @@ class Chef # attr_accessor :waiver_collection + # Handle to the global input_collection of inspec input files for the compliance phase + # + # @return [Chef::Compliance::inputCollection] + # + attr_accessor :input_collection + # Pointer back to the Chef::Runner that created this # attr_accessor :runner @@ -212,6 +219,7 @@ class Chef @loaded_attributes_hash = {} @reboot_info = {} @cookbook_compiler = nil + @input_collection = Chef::Compliance::InputCollection.new(events) @waiver_collection = Chef::Compliance::WaiverCollection.new(events) @profile_collection = Chef::Compliance::ProfileCollection.new(events) @@ -690,6 +698,8 @@ class Chef events= has_cookbook_file_in_cookbook? has_template_in_cookbook? + input_collection + input_collection= load loaded_attribute loaded_attributes diff --git a/lib/chef/run_context/cookbook_compiler.rb b/lib/chef/run_context/cookbook_compiler.rb index 6f0a0bc70e..b31323b684 100644 --- a/lib/chef/run_context/cookbook_compiler.rb +++ b/lib/chef/run_context/cookbook_compiler.rb @@ -67,6 +67,15 @@ class Chef run_context.waiver_collection end + # The global input_collection hanging off of the run_context, used by + # compile_compliance and the compliance phase that runs inspec + # + # @returns [Chef::Compliance::inputCollection] + # + def input_collection + run_context.input_collection + end + # The global profile_collection hanging off of the run_context, used by # compile_compliance and the compliance phase that runs inspec # @@ -161,9 +170,21 @@ class Chef # def compile_compliance @events.compliance_load_start + @events.profiles_load_start + cookbook_order.each do |cookbook| + load_profiles_from_cookbook(cookbook) + end + @events.profiles_load_complete + @events.inputs_load_start cookbook_order.each do |cookbook| - load_compliance_from_cookbook(cookbook) + load_inputs_from_cookbook(cookbook) end + @events.inputs_load_complete + @events.waivers_load_start + cookbook_order.each do |cookbook| + load_waivers_from_cookbook(cookbook) + end + @events.waivers_load_complete @events.compliance_load_complete end @@ -323,23 +344,34 @@ class Chef # Load the compliance segment files from a single cookbook # - def load_compliance_from_cookbook(cookbook_name) + def load_profiles_from_cookbook(cookbook_name) # This identifies profiles by their inspec.yml file, we recurse into subdirs so the profiles may be deeply # nested in a subdir structure for organization. You could have profiles inside of profiles but # since that is not coherently defined, you should not. # - each_file_in_cookbook_by_segment(cookbook_name, :compliance, [ "profiles/**/inspec.yml", "profiles/**/inspec.yaml" ]) do |filename| + each_file_in_cookbook_by_segment(cookbook_name, :compliance, [ "profiles/**/inspec.{yml,yaml}" ]) do |filename| profile_collection.from_file(filename, cookbook_name) end + end + def load_waivers_from_cookbook(cookbook_name) # This identifies waiver files as any yaml files under the waivers subdir. We recurse into subdirs as well # so that waivers may be nested in subdirs for organization. Any other files are ignored. # - each_file_in_cookbook_by_segment(cookbook_name, :compliance, [ "waivers/**/*.yml", "waivers/**/*.yaml" ]) do |filename| + each_file_in_cookbook_by_segment(cookbook_name, :compliance, [ "waivers/**/*.{yml,yaml}" ]) do |filename| waiver_collection.from_file(filename, cookbook_name) end end + def load_inputs_from_cookbook(cookbook_name) + # This identifies input files as any yaml files under the inputs subdir. We recurse into subdirs as well + # so that inputs may be nested in subdirs for organization. Any other files are ignored. + # + each_file_in_cookbook_by_segment(cookbook_name, :compliance, [ "inputs/**/*.{yml,yaml}" ]) do |filename| + input_collection.from_file(filename, cookbook_name) + end + end + def load_resource_definitions_from_cookbook(cookbook_name) files_in_cookbook_by_segment(cookbook_name, :definitions).each do |filename| next unless File.extname(filename) == ".rb" |