summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2021-08-16 21:36:46 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2021-08-16 21:36:46 -0700
commit28d4c2d010b3545a0092d9ad4136e092b8872f9a (patch)
tree7832ba96747ff5ea3035f5b15dd1f70855be7093
parent9657ee55c8d9a86c741dfacca9ff7b4996dcc352 (diff)
downloadchef-28d4c2d010b3545a0092d9ad4136e092b8872f9a.tar.gz
WIP
Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
-rw-r--r--lib/chef/compliance/input.rb108
-rw-r--r--lib/chef/compliance/input_collection.rb107
-rw-r--r--lib/chef/compliance/profile.rb13
-rw-r--r--lib/chef/compliance/profile_collection.rb2
-rw-r--r--lib/chef/compliance/runner.rb22
-rw-r--r--lib/chef/compliance/waiver_collection.rb10
-rw-r--r--lib/chef/dsl/compliance.rb5
-rw-r--r--lib/chef/dsl/reader_helpers.rb44
-rw-r--r--lib/chef/dsl/universal.rb2
-rw-r--r--lib/chef/event_dispatch/base.rb26
-rw-r--r--lib/chef/formatters/doc.rb35
-rw-r--r--lib/chef/resource/inspec_input.rb89
-rw-r--r--lib/chef/resource/inspec_waiver.rb118
-rw-r--r--lib/chef/resources.rb2
-rw-r--r--lib/chef/run_context.rb10
-rw-r--r--lib/chef/run_context/cookbook_compiler.rb40
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"