From 038cd9292a60fad2ce43a323ab7225f5223fd291 Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Thu, 27 Apr 2017 22:54:28 +0200 Subject: [PATCH] refactor reporting Signed-off-by: Christoph Hartmann --- Gemfile | 1 + files/default/handler/audit_report.rb | 65 +++- libraries/helper.rb | 16 +- libraries/reporters.rb | 291 ------------------ libraries/reporters/automate.rb | 139 +++++++++ libraries/reporters/compliance.rb | 75 +++++ libraries/reporters/cs_automate.rb | 36 +++ libraries/reporters/cs_compliance.rb | 27 ++ libraries/reporters/json_file.rb | 23 ++ spec/chef-client.pem | 27 ++ spec/spec_helper.rb | 6 + spec/unit/libraries/automate_spec.rb | 93 ++---- .../{reporters_spec.rb => compliance_spec.rb} | 26 +- spec/unit/libraries/cs_automate_spec.rb | 85 +++++ spec/unit/libraries/cs_compliance_spec.rb | 50 +++ spec/unit/libraries/helpers_spec.rb | 72 ++++- spec/unit/libraries/json_file_spec.rb | 6 +- 17 files changed, 645 insertions(+), 393 deletions(-) delete mode 100644 libraries/reporters.rb create mode 100644 libraries/reporters/automate.rb create mode 100644 libraries/reporters/compliance.rb create mode 100644 libraries/reporters/cs_automate.rb create mode 100644 libraries/reporters/cs_compliance.rb create mode 100644 libraries/reporters/json_file.rb create mode 100644 spec/chef-client.pem rename spec/unit/libraries/{reporters_spec.rb => compliance_spec.rb} (60%) create mode 100644 spec/unit/libraries/cs_automate_spec.rb create mode 100644 spec/unit/libraries/cs_compliance_spec.rb diff --git a/Gemfile b/Gemfile index a2f3566d..1d6f36a0 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ group :test do gem 'chefspec', '~> 7.0' gem 'coveralls', '~> 0.8.2', require: false gem 'rb-readline' + gem 'webmock' end group :integration do diff --git a/files/default/handler/audit_report.rb b/files/default/handler/audit_report.rb index bf64335a..a8f8c645 100644 --- a/files/default/handler/audit_report.rb +++ b/files/default/handler/audit_report.rb @@ -193,46 +193,81 @@ def cc_profile_index(profiles) end # send InSpec report to the reporter (see libraries/reporters.rb) - def send_report(reporter, server, user, profiles, report) + def send_report(reporter, server, user, profiles, content) Chef::Log.info "Reporting to #{reporter}" # Set `insecure` here to avoid passing 6 aruguments to `AuditReport#send_report` # See `cookstyle` Metrics/ParameterLists insecure = node['audit']['insecure'] + report = JSON.parse(content) # TODO: harmonize reporter interface if reporter == 'chef-visibility' || reporter == 'chef-automate' - Reporter::ChefAutomate.new(entity_uuid, run_id, gather_nodeinfo, insecure, report).send_report - - elsif reporter == 'chef-compliance' - raise_if_unreachable = node['audit']['raise_if_unreachable'] - url = construct_url(server, File.join('/owners', user, 'inspec')) - if server - Reporter::ChefCompliance.new(url, gather_nodeinfo, raise_if_unreachable, cc_profile_index(profiles), report).send_report - else - Chef::Log.warn "'server' and 'token' properties required by inspec report collector #{reporter}. Skipping..." - end + opts = { + entity_uuid: run_status.entity_uuid, + run_id: run_status.run_id, + node_info: gather_nodeinfo, + insecure: insecure, + } + Reporter::ChefAutomate.new(opts).send_report(report) elsif reporter == 'chef-server-visibility' || reporter == 'chef-server-automate' chef_url = server || base_chef_server_url chef_org = Chef::Config[:chef_server_url].split('/').last if chef_url url = construct_url(chef_url, File.join('organizations', chef_org, 'data-collector')) - Reporter::ChefServerAutomate.new(entity_uuid, run_id, gather_nodeinfo, insecure, report).send_report(url) + opts = { + entity_uuid: run_status.entity_uuid, + run_id: run_status.run_id, + node_info: gather_nodeinfo, + insecure: insecure, + url: url, + } + Reporter::ChefServerAutomate.new(opts).send_report(report) else Chef::Log.warn "unable to determine chef-server url required by inspec report collector '#{reporter}'. Skipping..." end - elsif reporter == 'chef-server-compliance' || reporter == 'chef-server' # chef-server is legacy reporter + elsif reporter == 'chef-compliance' + if server + raise_if_unreachable = node['audit']['raise_if_unreachable'] + url = construct_url(server, File.join('/owners', user, 'inspec')) + + # @config = Compliance::Configuration.new + # Chef::Log.info "Report to Chef Compliance: #{@config['server']}/owners/#{@config['user']}/inspec" + # @url = URI("#{@config['server']}/owners/#{@config['user']}/inspec") + token = @config['token'] + + opts = { + url: url, + node_info: gather_nodeinfo, + raise_if_unreachable: raise_if_unreachable, + profile_index: cc_profile_index(profiles), + token: token, + } + Reporter::ChefCompliance.new(opts).send_report(report) + else + Chef::Log.warn "'server' and 'token' properties required by inspec report collector #{reporter}. Skipping..." + end + elsif reporter == 'chef-server-compliance' || reporter == 'chef-server' chef_url = server || base_chef_server_url chef_org = Chef::Config[:chef_server_url].split('/').last if chef_url url = construct_url(chef_url + '/compliance/', File.join('organizations', chef_org, 'inspec')) - Reporter::ChefServer.new(url, gather_nodeinfo, raise_if_unreachable, cc_profile_index(profiles), report).send_report + opts = { + url: url, + node_info: gather_nodeinfo, + raise_if_unreachable: raise_if_unreachable, + profile_index: cc_profile_index(profiles), + } + Reporter::ChefServer.new(opts).send_report(report) else Chef::Log.warn "unable to determine chef-server url required by inspec report collector '#{reporter}'. Skipping..." end elsif reporter == 'json-file' timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S') - Reporter::JsonFile.new(report, timestamp).send_report + filename = 'inspec' << '-' << timestamp << '.json' + path = File.expand_path("../../../../#{filename}", __FILE__) + Chef::Log.info "Writing report to #{path}" + Reporter::JsonFile.new({ file: path }).send_report(report) else Chef::Log.warn "#{reporter} is not a supported InSpec report collector" end diff --git a/libraries/helper.rb b/libraries/helper.rb index 88fe8feb..bb0936bd 100644 --- a/libraries/helper.rb +++ b/libraries/helper.rb @@ -69,6 +69,14 @@ def base_chef_server_url cs.to_s end + # used for interval timing + def create_timestamp_file + timestamp = Time.now.utc + timestamp_file = File.new(report_timing_file, 'w') + timestamp_file.puts(timestamp) + timestamp_file.close + end + def report_timing_file # Will create and return the complete folder path for the chef cache location and the passed in value ::File.join(Chef::FileCache.create_cache_path('compliance'), 'report_timing.json') @@ -92,14 +100,6 @@ def check_interval_settings(interval, interval_enabled, interval_time) profile_overdue_to_run?(interval_seconds) end - # used for interval timing - def create_timestamp_file - timestamp = Time.now.utc - timestamp_file = File.new(report_timing_file, 'w') - timestamp_file.puts(timestamp) - timestamp_file.close - end - # takes value of reporters and returns array to ensure backwards-compatibility def handle_reporters(reporters) return reporters if reporters.is_a? Array diff --git a/libraries/reporters.rb b/libraries/reporters.rb deleted file mode 100644 index 04bed24a..00000000 --- a/libraries/reporters.rb +++ /dev/null @@ -1,291 +0,0 @@ -# encoding: utf-8 -require 'json' -require_relative 'helper' - -class Reporter - # - # Used to send inspec reports to Chef Automate via the data_collector service - # - class ChefAutomate - include ReportHelpers - - @entity_uuid = nil - @run_id = nil - @node_name = '' - @report = '' - - def initialize(entity_uuid, run_id, node_info, insecure, report) - @entity_uuid = entity_uuid - @run_id = run_id - @node_name = node_info[:node] - @insecure = insecure - @report = report - end - - # Method used in order to send the inspec report to the data_collector server - def send_report - unless @entity_uuid && @run_id - Chef::Log.error "entity_uuid(#{@entity_uuid}) or run_id(#{@run_id}) can't be nil, not sending report to Chef Automate" - return false - end - - content = @report - json_report = enriched_report(JSON.parse(content)) - - unless json_report - Chef::Log.warn 'Something went wrong, report can\'t be nil' - return false - end - if defined?(Chef) && - defined?(Chef::Config) && - Chef::Config[:data_collector] && - Chef::Config[:data_collector][:token] && - Chef::Config[:data_collector][:server_url] - - dc = Chef::Config[:data_collector] - headers = { 'Content-Type' => 'application/json' } - unless dc[:token].nil? - headers['x-data-collector-token'] = dc[:token] - headers['x-data-collector-auth'] = 'version=1.0' - end - - # Enable OpenSSL::SSL::VERIFY_NONE via `node['audit']['insecure']` - # See https://github.com/chef/chef/blob/master/lib/chef/http/ssl_policies.rb#L54 - if @insecure - Chef::Config[:verify_api_cert] = false - Chef::Config[:ssl_verify_mode] = :verify_none - end - - begin - Chef::Log.info "Report to Chef Automate: #{dc[:server_url]}" - Chef::Log.debug("POST the following message to #{dc[:server_url]}: #{json_report}") - http = Chef::HTTP.new(dc[:server_url]) - http.post(nil, json_report, headers) - return true - rescue => e - Chef::Log.error "send_inspec_report: POST to #{dc[:server_url]} returned: #{e.message}" - return false - end - else - Chef::Log.warn 'data_collector.token and data_collector.server_url must be defined in client.rb!' - Chef::Log.warn 'Further information: https://github.com/chef-cookbooks/audit#direct-reporting-to-chef-automate' - return false - end - end - - # Some document stores like ElasticSearch don't like values that change type - # This function converts all profile attribute defaults to string and - # adds a 'type' key to store the original type - def typed_attributes(profiles) - return profiles unless profiles.class == Array && !profiles.empty? - profiles.each { |profile| - next unless profile['attributes'].class == Array && !profile['attributes'].empty? - profile['attributes'].map { |attrib| - case attrib['options']['default'].class.to_s - when 'String' - attrib['options']['type'] = 'string' - when 'FalseClass' - attrib['options']['type'] = 'boolean' - attrib['options']['default'] = attrib['options']['default'].to_s - when 'Fixnum' - attrib['options']['type'] = 'int' - attrib['options']['default'] = attrib['options']['default'].to_s - when 'Float' - attrib['options']['type'] = 'float' - attrib['options']['default'] = attrib['options']['default'].to_s - else - Chef::Log.warn "enriched_report: unsupported data type(#{attrib['options']['default'].class}) for attribute #{attrib['options']['name']}" - attrib['options']['type'] = 'unknown' - end - } - } - end - - # *************************************************************************************** - # TODO: We could likely simplify/remove alot of the extra logic we have here with a small - # revamp of the Automate expected input. - # *************************************************************************************** - - def enriched_report(content) - return nil unless content.is_a?(Hash) - final_report = {} - total_duration = content['statistics']['duration'] - inspec_version = content['version'] - - # strip the report to leave only the profiles - final_report['profiles'] = content['profiles'] - - # remove nil profiles if any - final_report['profiles'].select! { |p| p } - - # set types for profile attributes - final_report['profiles'] = typed_attributes(final_report['profiles']) - - # add some additional fields to ease report parsing - final_report['event_type'] = 'inspec' - final_report['event_action'] = 'exec' - final_report['compliance_summary'] = count_controls(final_report['profiles']) - final_report['compliance_summary']['status'] = compliance_status(final_report['compliance_summary']) - final_report['compliance_summary']['node_name'] = @node_name - final_report['compliance_summary']['end_time'] = DateTime.now.iso8601 - final_report['compliance_summary']['duration'] = total_duration - final_report['compliance_summary']['inspec_version'] = inspec_version - final_report['entity_uuid'] = @entity_uuid - final_report['run_id'] = @run_id - Chef::Log.info "Compliance Summary #{final_report['compliance_summary']}" - final_report.to_json - end - end - - # - # Used to send inspec reports to a Chef Compliance server - # - class ChefCompliance - include ReportHelpers - - @url = nil - @node_info = {} - @report = '' - - def initialize(_url, node_info, raise_if_unreachable, compliance_profiles, report) - @node_info = node_info - @config = Compliance::Configuration.new - Chef::Log.warn "Report to Chef Compliance: #{@config['user']}" - Chef::Log.warn "#{@config['server']}/owners/#{@config['user']}/inspec" - @url = URI("#{@config['server']}/owners/#{@config['user']}/inspec") - @token = @config['token'] - @raise_if_unreachable = raise_if_unreachable - @compliance_profiles = compliance_profiles - @report = report - end - - def send_report - Chef::Log.info "Report to Chef Compliance: #{@token}" - req = Net::HTTP::Post.new(@url, { 'Authorization' => "Bearer #{@token}" }) - - content = @report - json_report = enriched_report(JSON.parse(content)) - req.body = json_report - - Chef::Log.info "Report to Chef Compliance: #{@url}" - - # TODO: use secure option - opts = { use_ssl: @url.scheme == 'https', - verify_mode: OpenSSL::SSL::VERIFY_NONE, - } - Net::HTTP.start(@url.host, @url.port, opts) do |http| - with_http_rescue do - http.request(req) - end - end - end - - # TODO: add to docs that all profiles used in Chef Compliance, need to - # be uploaded to Chef Compliance first - def enriched_report(report) - blob = @node_info.dup - - # extract profile names - profiles = report['controls'].collect { |control| control['profile_id'] }.uniq - - # build report for chef compliance, it includes node data - blob[:reports] = {} - blob[:profiles] = {} - Chef::Log.info "Control Profile: #{profiles}" - profiles.each { |profile| - Chef::Log.info "Control Profile: #{profile}" - Chef::Log.info "Compliance Profiles: #{@compliance_profiles}" - namespace = @compliance_profiles.select { |entry| entry[:profile_id] == profile } - unless namespace.nil? && namespace.empty? - Chef::Log.debug "Namespace for #{profile} is #{namespace[0][:owner]}" - blob[:profiles][profile] = namespace[0][:owner] - blob[:reports][profile] = report.dup - # filter controls by profile_id - blob[:reports][profile]['controls'] = blob[:reports][profile]['controls'].select { |control| control['profile_id'] == profile } - else - Chef::Log.warn "Could not determine compliance namespace for #{profile}" - end - } - - blob.to_json - end - end - - # - # Used to send inspec reports to a Chef Compliance server via Chef Server - # - class ChefServer < ChefCompliance - @url = nil - - def initialize(url, node_info, raise_if_unreachable, compliance_profiles, report) - @url = url - @node_info = node_info - @raise_if_unreachable = raise_if_unreachable - @compliance_profiles = compliance_profiles - @report = report - end - - def send_report - content = @report - json_report = enriched_report(JSON.parse(content)) - - # TODO: only disable if insecure option is set - Chef::Config[:verify_api_cert] = false - Chef::Config[:ssl_verify_mode] = :verify_none - - Chef::Log.info "Report to Chef Server: #{@url}" - rest = Chef::ServerAPI.new(@url, Chef::Config) - with_http_rescue do - rest.post(@url, JSON.parse(json_report)) - end - end - end - - # - # Used to send inspec reports to Chef Automate server via Chef Server - # - class ChefServerAutomate < ChefAutomate - def send_report(url) - content = @report - json_report = enriched_report(JSON.parse(content)) - - if @insecure - Chef::Config[:verify_api_cert] = false - Chef::Config[:ssl_verify_mode] = :verify_none - end - - Chef::Log.info "Report to Chef Automate via Chef Server: #{url}" - rest = Chef::ServerAPI.new(url, Chef::Config) - with_http_rescue do - rest.post(url, JSON.parse(json_report)) - end - end - end - - # - # Used to write report to file on disk - # - class JsonFile - include ReportHelpers - - @report = '' - - def initialize(report, timestamp) - @report = report - @timestamp = timestamp - end - - def send_report - write_to_file(@report, @timestamp) - end - - def write_to_file(report, timestamp) - filename = 'inspec' << '-' << timestamp << '.json' - path = File.expand_path("../../#{filename}", __FILE__) - Chef::Log.info "Writing report to #{path}" - json_file = File.new(path, 'w') - json_file.puts(report) - json_file.close - end - end -end diff --git a/libraries/reporters/automate.rb b/libraries/reporters/automate.rb new file mode 100644 index 00000000..b5cea61d --- /dev/null +++ b/libraries/reporters/automate.rb @@ -0,0 +1,139 @@ +# encoding: utf-8 +require 'json' +require_relative '../helper' + +module Reporter + # + # Used to send inspec reports to Chef Automate via the data_collector service + # + class ChefAutomate + include ReportHelpers + + def initialize(opts) + @entity_uuid = opts[:entity_uuid] + @run_id = opts[:run_id] + @node_name = opts[:node_info][:node] + @insecure = opts[:insecure] + + if defined?(Chef) && + defined?(Chef::Config) && + Chef::Config[:data_collector] && + Chef::Config[:data_collector][:token] && + Chef::Config[:data_collector][:server_url] + + dc = Chef::Config[:data_collector] + @url = dc[:server_url] + @token = dc[:token] + end + end + + # Method used in order to send the inspec report to the data_collector server + def send_report(report) + unless @entity_uuid && @run_id + Chef::Log.error "entity_uuid(#{@entity_uuid}) or run_id(#{@run_id}) can't be nil, not sending report to Chef Automate" + return false + end + + json_report = enriched_report(report) + + unless json_report + Chef::Log.warn 'Something went wrong, report can\'t be nil' + return false + end + + if defined?(Chef) && + defined?(Chef::Config) + + headers = { 'Content-Type' => 'application/json' } + unless @token.nil? + headers['x-data-collector-token'] = @token + headers['x-data-collector-auth'] = 'version=1.0' + end + + # Enable OpenSSL::SSL::VERIFY_NONE via `node['audit']['insecure']` + # See https://github.com/chef/chef/blob/master/lib/chef/http/ssl_policies.rb#L54 + if @insecure + Chef::Config[:verify_api_cert] = false + Chef::Config[:ssl_verify_mode] = :verify_none + end + + begin + Chef::Log.warn "Report to Chef Automate: #{@url}" + http = Chef::HTTP.new(@url) + http.post(nil, json_report, headers) + return true + rescue => e + Chef::Log.error "send_report: POST to #{@url} returned: #{e.message}" + return false + end + else + Chef::Log.warn 'data_collector.token and data_collector.server_url must be defined in client.rb!' + Chef::Log.warn 'Further information: https://github.com/chef-cookbooks/audit#direct-reporting-to-chef-automate' + return false + end + end + + # Some document stores like ElasticSearch don't like values that change type + # This function converts all profile attribute defaults to string and + # adds a 'type' key to store the original type + def typed_attributes(profiles) + return profiles unless profiles.class == Array && !profiles.empty? + profiles.each { |profile| + next unless profile['attributes'].class == Array && !profile['attributes'].empty? + profile['attributes'].map { |attrib| + case attrib['options']['default'].class.to_s + when 'String' + attrib['options']['type'] = 'string' + when 'FalseClass' + attrib['options']['type'] = 'boolean' + attrib['options']['default'] = attrib['options']['default'].to_s + when 'Fixnum' + attrib['options']['type'] = 'int' + attrib['options']['default'] = attrib['options']['default'].to_s + when 'Float' + attrib['options']['type'] = 'float' + attrib['options']['default'] = attrib['options']['default'].to_s + else + Chef::Log.warn "enriched_report: unsupported data type(#{attrib['options']['default'].class}) for attribute #{attrib['options']['name']}" + attrib['options']['type'] = 'unknown' + end + } + } + end + + # *************************************************************************************** + # TODO: We could likely simplify/remove alot of the extra logic we have here with a small + # revamp of the Automate expected input. + # *************************************************************************************** + + def enriched_report(content) + return nil unless content.is_a?(Hash) + final_report = {} + total_duration = content['statistics']['duration'] + inspec_version = content['version'] + + # strip the report to leave only the profiles + final_report['profiles'] = content['profiles'] + + # remove nil profiles if any + final_report['profiles'].select! { |p| p } + + # set types for profile attributes + final_report['profiles'] = typed_attributes(final_report['profiles']) + + # add some additional fields to ease report parsing + final_report['event_type'] = 'inspec' + final_report['event_action'] = 'exec' + final_report['compliance_summary'] = count_controls(final_report['profiles']) + final_report['compliance_summary']['status'] = compliance_status(final_report['compliance_summary']) + final_report['compliance_summary']['node_name'] = @node_name + final_report['compliance_summary']['end_time'] = DateTime.now.iso8601 + final_report['compliance_summary']['duration'] = total_duration + final_report['compliance_summary']['inspec_version'] = inspec_version + final_report['entity_uuid'] = @entity_uuid + final_report['run_id'] = @run_id + Chef::Log.info "Compliance Summary #{final_report['compliance_summary']}" + final_report.to_json + end + end +end diff --git a/libraries/reporters/compliance.rb b/libraries/reporters/compliance.rb new file mode 100644 index 00000000..f868fda4 --- /dev/null +++ b/libraries/reporters/compliance.rb @@ -0,0 +1,75 @@ +# encoding: utf-8 +require 'json' +require_relative '../helper' + +module Reporter + # + # Used to send inspec reports to a Chef Compliance server + # + class ChefCompliance + include ReportHelpers + + def initialize(opts) + @node_info = opts[:node_info] + @url = opts[:url] + @raise_if_unreachable = opts[:raise_if_unreachable] + @compliance_profiles = opts[:compliance_profiles] + @insecure = opts[:insecure] + @token = opts[:token] + end + + def send_report(report) + Chef::Log.info "Report to Chef Compliance: #{@url} with #{@token}" + req = Net::HTTP::Post.new(@url, { 'Authorization' => "Bearer #{@token}" }) + + json_report = enriched_report(report) + req.body = json_report + + # TODO: use secure option + uri = URI(@url) + opts = { + use_ssl: uri.scheme == 'https', + verify_mode: OpenSSL::SSL::VERIFY_NONE, + } + Net::HTTP.start(uri.host, uri.port, opts) do |http| + with_http_rescue do + http.request(req) + end + end + return true + rescue => e + Chef::Log.error "send_report: POST to #{@url} returned: #{e.message}" + return false + end + + # TODO: add to docs that all profiles used in Chef Compliance, need to + # be uploaded to Chef Compliance first + def enriched_report(report) + blob = @node_info.dup + + # extract profile names + profiles = report['controls'].collect { |control| control['profile_id'] }.uniq + + # build report for chef compliance, it includes node data + blob[:reports] = {} + blob[:profiles] = {} + Chef::Log.info "Control Profile: #{profiles}" + profiles.each { |profile| + Chef::Log.info "Control Profile: #{profile}" + Chef::Log.info "Compliance Profiles: #{@compliance_profiles}" + namespace = @compliance_profiles.select { |entry| entry[:profile_id] == profile } + unless namespace.nil? && namespace.empty? + Chef::Log.debug "Namespace for #{profile} is #{namespace[0][:owner]}" + blob[:profiles][profile] = namespace[0][:owner] + blob[:reports][profile] = report.dup + # filter controls by profile_id + blob[:reports][profile]['controls'] = blob[:reports][profile]['controls'].select { |control| control['profile_id'] == profile } + else + Chef::Log.warn "Could not determine compliance namespace for #{profile}" + end + } + + blob.to_json + end + end +end diff --git a/libraries/reporters/cs_automate.rb b/libraries/reporters/cs_automate.rb new file mode 100644 index 00000000..0096fcb6 --- /dev/null +++ b/libraries/reporters/cs_automate.rb @@ -0,0 +1,36 @@ +# encoding: utf-8 +require 'json' +require_relative '../helper' +require_relative './automate' + +module Reporter + # + # Used to send inspec reports to Chef Automate server via Chef Server + # + class ChefServerAutomate < ChefAutomate + def initialize(opts) + @entity_uuid = opts[:entity_uuid] + @run_id = opts[:run_id] + @node_name = opts[:node_info][:node] + @insecure = opts[:insecure] + @url = opts[:url] + end + + def send_report(report) + json_report = enriched_report(report) + + if @insecure + Chef::Config[:verify_api_cert] = false + Chef::Config[:ssl_verify_mode] = :verify_none + end + + Chef::Log.info "Report to Chef Automate via Chef Server: #{@url}" + rest = Chef::ServerAPI.new(@url, Chef::Config) + with_http_rescue do + rest.post(@url, JSON.parse(json_report)) + return true + end + false + end + end +end diff --git a/libraries/reporters/cs_compliance.rb b/libraries/reporters/cs_compliance.rb new file mode 100644 index 00000000..b786695d --- /dev/null +++ b/libraries/reporters/cs_compliance.rb @@ -0,0 +1,27 @@ +# encoding: utf-8 +require 'json' +require_relative '../helper' +require_relative './compliance' + +module Reporter + # + # Used to send inspec reports to a Chef Compliance server via Chef Server + # + class ChefServerCompliance < ChefCompliance + def send_report(report) + json_report = enriched_report(report) + + # TODO: only disable if insecure option is set + Chef::Config[:verify_api_cert] = false + Chef::Config[:ssl_verify_mode] = :verify_none + + Chef::Log.info "Report to Chef Compliance via Chef Server: #{@url}" + rest = Chef::ServerAPI.new(@url, Chef::Config) + with_http_rescue do + rest.post(@url, JSON.parse(json_report)) + return true + end + false + end + end +end diff --git a/libraries/reporters/json_file.rb b/libraries/reporters/json_file.rb new file mode 100644 index 00000000..5ce172a7 --- /dev/null +++ b/libraries/reporters/json_file.rb @@ -0,0 +1,23 @@ +# encoding: utf-8 +require 'json' + +module Reporter + # + # Used to write report to file on disk + # + class JsonFile + def initialize(opts) + @opts = opts + end + + def send_report(report) + write_to_file(report, @opts[:file]) + end + + def write_to_file(report, path) + json_file = File.new(path, 'w') + json_file.puts(report) + json_file.close + end + end +end diff --git a/spec/chef-client.pem b/spec/chef-client.pem new file mode 100644 index 00000000..ecf6a195 --- /dev/null +++ b/spec/chef-client.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA6daLaGZK27W5f1Pe4UXZ5/a0/FMAxPDznOapVUpMFZS2rOyt +3Lc+/038ZzsDORLW6FUAefjAp1H3G0IxcjODAlLOaUVNuso9XLDRU45FFGW0h9oz +Le7NIq53PQ/btDoFA74xeQhEHtN9cRc4zb7r40sBe49HScJ5VkMsncy738+tP89+ +sWJfSW41vUCI+Xb28AqdANiIynsgDJyoLDXoUbYi/AV1JxYJECeTdo2XzqwvjZfQ +pGYtF4o19ivnMEj6/EQXhF9BnXpFB7LosCAOTuscW5B5BTq6kdqdnC2Uh5yJSRuz +3yhOfTR929dmsMUokEhBcB30gROCT70jNloUSQIDAQABAoIBAGxi1oFQkLggFlgP +XwqZ3vPm5WLjckLW0IRUYf63jmaeZMHofnoEsf2Sf0C2GLtWoShVZgAjLeEgW+JV +nyeo+ruT+DrRNcMzxJd3Gb+Z/SkEL1ac7AYJXyoJJhm2hQaXsgVXHgVUsIZ9TvKh +aeHr8diLxqcn9UoaCzXRsxd9c0O8U2cpoO9Sz37PpCvL3v+wT838Ulrpir8kTT2v +4HtpcQw3apFF1MC2IAIkLOnpjh5XT84KmgyYiTD3omtXMlwJuEzMDJMLTRQ0r0qr +3/aljmgOYFf8v7MGU8hsaBLIpcwpEbF3iYIx/kPpySpj7CeQB7yId/ayypFO8cH4 +h/wBlSUCgYEA9b1RZlZJgH4vFaNnHREq3ZFGpYYXK83/IIHsArmOy4hD4W74Dl8x +PVQjcPWaBNXXcIWTBoAF9hkSAXvCOPA6CK6w7UZesMvUPHYQYJH9BcSE4PZ0YzHN +5sB7enzOw32qG1dxsB2ovlsJ5e/kvcDhOHKETwrrdODNr3VOu326arMCgYEA85oD +ZBXKyIPW/wlW8P0X8i4c5gQSnWzQKwtTRwH2O9P33/BOp6Rl/8WFT1hnNOOZRcqW +dyOAvPGaMq7vj7Ll9ggyhCDceFDuFXOQfd2WOBDvx/hG8P3Swj+vYQU48FTeMkpG +zqPYq7ecCvaIMQ2FCL6xoT8F6CI+om4RpzvzsxMCgYEAtNNvr491HME9on2QJdp5 +IXuCcdC/AjPeNayE3+htRCXsVVmT3Pd9QzTDs552jHJSyvDvpIvWVyZRkpff7ogP +HE530NHEYfJLJYZ3PKiQeIsIgIW6VTfT3KXs9tAaUc4Ju37YIJFil1hkazfgqSTi +VegmpgdSBbpagG8g1WSKJXMCgYEAt5ge8CKgd6kts39lgDEwB/2LGCx/nxgweBCM +DhtDamniCmwBy8VSfoduZpOZDTpv/TKnXllqoHxym7pOoP3S5S/easidgSx1k8NK +ZiJIIi9ZmFvdk6mpW29GDZgzBqbf5AUpAnpoRVsXhwexM08eMa4PEBkAqaiNjjvo +oCLGE/MCgYEArv+F/lnt9Tdd6UbTGWxkKjhlb+TqJ6wYSFoKyiLN4uvDO4d6gL+f +B8hk1m6pQt/58jeTVgVFYTaBzO4ygkaJv66CpuTEZTb/LajxQE1PlE9tjQjRI+Lb +8am0IjK7GBgWTicDONCeBAKdsQ7RC2LFlRm/kUMc98DGMBjD1qy4HcY= +-----END RSA PRIVATE KEY----- diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index f058059d..e606f881 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,6 +1,12 @@ # encoding: utf-8 require 'chefspec' require 'chefspec/berkshelf' +require 'webmock/rspec' + +# Berkshelf needs to connect to internet +WebMock.disable_net_connect!( + allow: [/supermarket.chef.io/, /127.0.0.1:8889/, /s3.amazonaws.com\/community-files.opscode.com/] +) RSpec.configure do |config| config.file_cache_path = Chef::Config[:file_cache_path] diff --git a/spec/unit/libraries/automate_spec.rb b/spec/unit/libraries/automate_spec.rb index 15a31f0f..24a51e88 100644 --- a/spec/unit/libraries/automate_spec.rb +++ b/spec/unit/libraries/automate_spec.rb @@ -18,7 +18,7 @@ # limitations under the License. require 'spec_helper' -require_relative '../../../libraries/reporters' +require_relative '../../../libraries/reporters/automate' require_relative '../../data/mock.rb' describe 'Reporter::ChefAutomate methods' do @@ -26,7 +26,6 @@ entity_uuid = 'aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz' run_id = '3f0536f7-3361-4bca-ae53-b45118dceb5d' insecure = false - report = MockData.inspec_results @enriched_report_expected = { "profiles"=> [{"name"=>"tmp_compliance_profile", "title"=>"/tmp Compliance Profile", @@ -73,59 +72,42 @@ "inspec_version"=>"1.2.1"}, "entity_uuid"=>"aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz", "run_id"=>"3f0536f7-3361-4bca-ae53-b45118dceb5d"} - @viz = Reporter::ChefAutomate.new(entity_uuid, run_id, MockData.node_info, insecure, report) - end - it 'returns the correct control status' do - expect(@viz.control_status(nil)).to eq(nil) - expect(@viz.control_status([{"status"=>"failed"}])).to eq('failed') - expect(@viz.control_status([{"status"=>"passed"}])).to eq('passed') - expect(@viz.control_status([{"status"=>"passed"},{"status"=>"failed"}])).to eq('failed') - expect(@viz.control_status([{"status"=>"failed"},{"status"=>"passed"}])).to eq('failed') - expect(@viz.control_status([{"status"=>"passed"},{"status"=>"skipped"}])).to eq('skipped') - expect(@viz.control_status([{"status"=>"skipped"},{"status"=>"failed"}])).to eq('failed') + opts = { + entity_uuid: entity_uuid, + run_id: run_id, + node_info: MockData.node_info, + insecure: insecure, + } + # set data_collector + Chef::Config[:data_collector] = {token: 'dctoken', server_url: 'https://automate.test/data_collector' } + stub_request(:post, 'https://automate.test/data_collector'). + with(:body => @enriched_report_expected.to_json, + :headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'identity', 'Content-Length'=>'2818', 'Content-Type'=>'application/json', 'Host'=>'automate.test', 'User-Agent'=>/.+/, 'X-Chef-Version'=>/.+/, 'X-Data-Collector-Auth'=>'version=1.0', 'X-Data-Collector-Token'=>'dctoken'}). + to_return(:status => 200, :body => "", :headers => {}) + + @automate = Reporter::ChefAutomate.new(opts) end - it 'reports impact criticality correctly' do - expect(@viz.impact_to_s(0)).to eq('minor') - expect(@viz.impact_to_s(0.1234)).to eq('minor') - expect(@viz.impact_to_s(0.4)).to eq('major') - expect(@viz.impact_to_s(0.69)).to eq('major') - expect(@viz.impact_to_s(0.7)).to eq('critical') - expect(@viz.impact_to_s(1.0)).to eq('critical') + it 'sends report successfully' do + allow(DateTime).to receive(:now).and_return(DateTime.parse('2016-07-19T19:19:19+01:00')) + expect(@automate.send_report(MockData.inspec_results)).to eq(true) end - it 'reports compliance status like a compliance officer' do - passed = {"total"=>5, "passed"=>{"total"=>3}, "skipped"=>{"total"=>2}, "failed"=>{"total"=>0, "minor"=>0, "major"=>0, "critical"=>0}} - failed = {"total"=>5, "passed"=>{"total"=>1}, "skipped"=>{"total"=>1}, "failed"=>{"total"=>3, "minor"=>1, "major"=>1, "critical"=>1}} - skipped = {"total"=>5, "passed"=>{"total"=>0}, "skipped"=>{"total"=>5}, "failed"=>{"total"=>0, "minor"=>0, "major"=>0, "critical"=>0}} - expect(@viz.compliance_status(nil)).to eq('unknown') - expect(@viz.compliance_status(failed)).to eq('failed') - expect(@viz.compliance_status(passed)).to eq('passed') - expect(@viz.compliance_status(skipped)).to eq('skipped') + it 'enriches report correctly with the most test coverage' do + allow(DateTime).to receive(:now).and_return(DateTime.parse('2016-07-19T19:19:19+01:00')) + expect(JSON.parse(@automate.enriched_report(MockData.inspec_results))).to eq(@enriched_report_expected) end - it 'counts controls like an accountant' do - profi = [{"name"=>"test-profile", - "controls"=> - [{"id"=>"Checking /etc/missing1.rb existance", - "impact"=>0, - "results"=>[{"status"=>"failed"}]}, - {"id"=>"Checking /etc/missing2.rb existance", - "impact"=>0.5, - "results"=>[{"status"=>"failed"}]}, - {"id"=>"Checking /etc/missing3.rb existance", - "impact"=>0.8, - "results"=>[{"status"=>"failed"}]}, - {"id"=>"Checking /etc/passwd existance", - "impact"=>0.88, - "results"=>[{"status"=>"passed"}]}, - {"id"=>"Checking /etc/something existance", - "impact"=>1.0, - "results"=>[{"status"=>"skipped"}]}] - }] - expected_count = {"total"=>5, "passed"=>{"total"=>1}, "skipped"=>{"total"=>1}, "failed"=>{"total"=>3, "minor"=>1, "major"=>1, "critical"=>1}} - expect(@viz.count_controls(profi)).to eq(expected_count) + it 'is not sending report when entity_uuid is missing' do + opts = { + entity_uuid: nil, + run_id: '3f0536f7-3361-4bca-ae53-b45118dceb5d', + node_info: MockData.node_info, + insecure: false, + } + viz2 = Reporter::ChefAutomate.new(opts) + expect(viz2.send_report(MockData.inspec_results)).to eq(false) end it 'sets the attribute types like TypeScript' do @@ -155,19 +137,6 @@ types_profiles[0]['attributes'][3]['options']['type'] = 'float' types_profiles[0]['attributes'][3]['options']['default'] = '0.8' types_profiles[0]['attributes'][4]['options']['type'] = 'unknown' - expect(@viz.typed_attributes(profiles)).to eq(types_profiles) - end - - it 'enriches report correctly with the most test coverage' do - allow(DateTime).to receive(:now).and_return(DateTime.parse('2016-07-19T19:19:19+01:00')) - expect(JSON.parse(@viz.enriched_report(MockData.inspec_results))).to eq(@enriched_report_expected) - end - - it 'is not sending report when entity_uuid is missing' do - entity_uuid = nil - run_id = '3f0536f7-3361-4bca-ae53-b45118dceb5d' - insecure = false - viz2 = Reporter::ChefAutomate.new(entity_uuid, run_id, {}, insecure, MockData.inspec_results) - expect(viz2.send_report).to eq(false) + expect(@automate.typed_attributes(profiles)).to eq(types_profiles) end end diff --git a/spec/unit/libraries/reporters_spec.rb b/spec/unit/libraries/compliance_spec.rb similarity index 60% rename from spec/unit/libraries/reporters_spec.rb rename to spec/unit/libraries/compliance_spec.rb index 7684e5ee..5b725a1f 100644 --- a/spec/unit/libraries/reporters_spec.rb +++ b/spec/unit/libraries/compliance_spec.rb @@ -1,15 +1,15 @@ # encoding: utf-8 # # Cookbook Name:: audit -# Spec:: reporters +# Spec:: compliance_spec require 'spec_helper' -require_relative '../../../libraries/reporters' +require_relative '../../../libraries/reporters/compliance' describe 'Reporter::ChefCompliance methods' do before :each do require 'bundles/inspec-compliance/configuration' - url = 'https://192.168.33.201/api' + url = 'https://192.168.33.201/api/owners/admin/inspec' node_info = {:node=>"default-ubuntu-1404", :os=>{:release=>"14.04", :family=>"ubuntu"}, :environment=>"_default"} raise_if_unreachable = true @report = { @@ -24,7 +24,25 @@ "reports"=>{"ssh"=>{"version"=>"1.2.1", "controls"=>[{"id"=>"basic-4", "status"=>"passed", "code_desc"=>"File /etc/ssh/sshd_config should be owned by \"root\"", "profile_id"=>"ssh"}], "statistics"=>{"duration"=>0.355784812}}}, "profiles"=>{"ssh"=>"admin"} } - @chef_compliance = Reporter::ChefCompliance.new(url, node_info, raise_if_unreachable, compliance_profiles, @report) + + opts = { + url: url, + node_info: node_info, + raise_if_unreachable: raise_if_unreachable, + compliance_profiles: compliance_profiles, + token: 1234 + } + + @chef_compliance = Reporter::ChefCompliance.new(opts) + + stub_request(:post, url). + with(:body => @enriched_report_expected.to_json, + :headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization'=>'Bearer 1234', 'User-Agent'=>/.+/}). + to_return(:status => 200, :body => "", :headers => {}) + end + + it 'sends report successfully' do + expect(@chef_compliance.send_report(@report)).to eq(true) end it 'enriches the report correctly' do diff --git a/spec/unit/libraries/cs_automate_spec.rb b/spec/unit/libraries/cs_automate_spec.rb new file mode 100644 index 00000000..1f8bf3e4 --- /dev/null +++ b/spec/unit/libraries/cs_automate_spec.rb @@ -0,0 +1,85 @@ +# encoding: utf-8 +# +# Cookbook Name:: audit +# Spec:: cs_automate_spec + +require 'spec_helper' +require_relative '../../../libraries/reporters/cs_automate' +require_relative '../../data/mock.rb' + +describe 'Reporter::ChefServerAutomate methods' do + before :each do + entity_uuid = 'aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz' + run_id = '3f0536f7-3361-4bca-ae53-b45118dceb5d' + insecure = false + @enriched_report_expected = { "profiles"=> + [{"name"=>"tmp_compliance_profile", + "title"=>"/tmp Compliance Profile", + "summary"=>"An Example Compliance Profile", + "version"=>"0.1.1", + "maintainer"=>"Nathen Harvey ", + "license"=>"Apache 2.0 License", + "copyright"=>"Nathen Harvey ", + "supports"=>[], + "controls"=> + [ {"title"=>"A /tmp directory must exist", + "desc"=>"A /tmp directory must exist", + "impact"=>0.3, + "refs"=>[], + "tags"=>{}, + "code"=> + "control 'tmp-1.0' do\n impact 0.3\n title 'A /tmp directory must exist'\n desc 'A /tmp directory must exist'\n describe file '/tmp' do\n it { should be_directory }\n end\nend\n", + "source_location"=>{"ref"=>"/Users/vjeffrey/code/delivery/insights/data_generator/chef-client/cache/cookbooks/test-cookbook/recipes/../files/default/compliance_profiles/tmp_compliance_profile/controls/tmp.rb", "line"=>3}, + "id"=>"tmp-1.0", + "results"=>[{"status"=>"passed", "code_desc"=>"File /tmp should be directory", "run_time"=>0.002312, "start_time"=>"2016-10-19 11:09:43 -0400"}]}, + {"title"=>"/tmp directory is owned by the root user", + "desc"=>"The /tmp directory must be owned by the root user", + "impact"=>0.3, + "refs"=>[{"url"=>"https://pages.chef.io/rs/255-VFB-268/images/compliance-at-velocity2015.pdf", "ref"=>"Compliance Whitepaper"}], + "tags"=>{"production"=>nil, "development"=>nil, "identifier"=>"value", "remediation"=>"https://github.com/chef-cookbooks/audit"}, + "code"=> + "control 'tmp-1.1' do\n impact 0.3\n title '/tmp directory is owned by the root user'\n desc 'The /tmp directory must be owned by the root user'\n tag 'production','development'\n tag identifier: 'value'\n tag remediation: 'https://github.com/chef-cookbooks/audit'\n ref 'Compliance Whitepaper', url: 'https://pages.chef.io/rs/255-VFB-268/images/compliance-at-velocity2015.pdf'\n describe file '/tmp' do\n it { should be_owned_by 'root' }\n end\nend\n", + "source_location"=>{"ref"=>"/Users/vjeffrey/code/delivery/insights/data_generator/chef-client/cache/cookbooks/test-cookbook/recipes/../files/default/compliance_profiles/tmp_compliance_profile/controls/tmp.rb", "line"=>12}, + "id"=>"tmp-1.1", + "results"=>[{"status"=>"passed", "code_desc"=>"File /tmp should be owned by \"root\"", "run_time"=>0.028845, "start_time"=>"2016-10-19 11:09:43 -0400"}]}], + "groups"=>[{"title"=>"/tmp Compliance Profile", "controls"=>["tmp-1.0", "tmp-1.1"], "id"=>"controls/tmp.rb"}], + "attributes"=>[{"name"=>"syslog_pkg", "options"=>{"default"=>"rsyslog", "description"=>"syslog package...", "type"=>"string"}}]}], + "event_type"=>"inspec", + "event_action"=>"exec", + "compliance_summary"=>{ + "total"=>2, + "passed"=>{"total"=>2}, + "skipped"=>{"total"=>0}, + "failed"=>{"total"=>0, "minor"=>0, "major"=>0, "critical"=>0}, + "status"=>"passed", + "node_name"=>"chef-client.solo", + "end_time"=>"2016-07-19T19:19:19+01:00", + "duration"=>0.032332, + "inspec_version"=>"1.2.1"}, + "entity_uuid"=>"aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz", + "run_id"=>"3f0536f7-3361-4bca-ae53-b45118dceb5d"} + + opts = { + entity_uuid: entity_uuid, + run_id: run_id, + node_info: MockData.node_info, + insecure: insecure, + url: "https://chef.server/data_collector" + } + + Chef::Config[:client_key] = File.expand_path("../../chef-client.pem", File.dirname(__FILE__)) + Chef::Config[:node_name] = 'spec-node' + + # set data_collector + stub_request(:post, 'https://chef.server/data_collector'). + with(:body => @enriched_report_expected.to_json, + :headers => {'Accept'=>'application/json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Length'=>'2818', 'Content-Type'=>'application/json', 'Host'=>/.+/, 'User-Agent'=>/.+/, 'X-Chef-Version'=>/.+/, 'X-Ops-Authorization-1'=>/.+/, 'X-Ops-Authorization-2'=>/.+/, 'X-Ops-Authorization-3'=>/.+/, 'X-Ops-Authorization-4' => /.+/, 'X-Ops-Authorization-5'=>/.+/, 'X-Ops-Authorization-6'=>/.+/, 'X-Ops-Content-Hash'=>/.+/, 'X-Ops-Server-Api-Version'=>'1', 'X-Ops-Sign'=>'algorithm=sha1;version=1.1;', 'X-Ops-Timestamp'=>/.+/, 'X-Ops-Userid'=>'spec-node', 'X-Remote-Request-Id'=>/.+/}). + to_return(:status => 200, :body => "", :headers => {}) + @automate = Reporter::ChefServerAutomate.new(opts) + end + + it 'sends report successfully' do + allow(DateTime).to receive(:now).and_return(DateTime.parse('2016-07-19T19:19:19+01:00')) + expect(@automate.send_report(MockData.inspec_results)).to eq(true) + end +end diff --git a/spec/unit/libraries/cs_compliance_spec.rb b/spec/unit/libraries/cs_compliance_spec.rb new file mode 100644 index 00000000..cfc389af --- /dev/null +++ b/spec/unit/libraries/cs_compliance_spec.rb @@ -0,0 +1,50 @@ +# encoding: utf-8 +# +# Cookbook Name:: audit +# Spec:: cs_compliance_spec + +require 'spec_helper' +require_relative '../../../libraries/reporters/cs_compliance' + +describe 'Reporter::ChefServerCompliance methods' do + before :each do + require 'bundles/inspec-compliance/configuration' + url = 'https://chef-server/compliance/inspec' + node_info = {:node=>"default-ubuntu-1404", :os=>{:release=>"14.04", :family=>"ubuntu"}, :environment=>"_default"} + raise_if_unreachable = true + @report = { + "version"=>"1.2.1", + "controls"=>[{"id"=>"basic-4", "status"=>"passed", "code_desc"=>"File /etc/ssh/sshd_config should be owned by \"root\"", "profile_id"=>"ssh"}], "statistics"=>{"duration"=>0.355784812} + } + compliance_profiles = [{:owner=> 'admin', :profile_id=> 'ssh'}] + @enriched_report_expected = { + "node"=>"default-ubuntu-1404", + "os"=>{"release"=>"14.04", "family"=>"ubuntu"}, + "environment"=>"_default", + "reports"=>{"ssh"=>{"version"=>"1.2.1", "controls"=>[{"id"=>"basic-4", "status"=>"passed", "code_desc"=>"File /etc/ssh/sshd_config should be owned by \"root\"", "profile_id"=>"ssh"}], "statistics"=>{"duration"=>0.355784812}}}, + "profiles"=>{"ssh"=>"admin"} + } + + opts = { + url: url, + node_info: node_info, + raise_if_unreachable: raise_if_unreachable, + compliance_profiles: compliance_profiles, + token: 1234 + } + + Chef::Config[:client_key] = File.expand_path("../../chef-client.pem", File.dirname(__FILE__)) + Chef::Config[:node_name] = 'spec-node' + + stub_request(:post, url). + with(:body => @enriched_report_expected.to_json, + :headers => {'Accept'=>'application/json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Length'=>'336', 'Content-Type'=>'application/json', 'Host'=>/.+/, 'User-Agent'=>/.+/, 'X-Chef-Version'=>/.+/, 'X-Ops-Authorization-1'=>/.+/, 'X-Ops-Authorization-2'=>/.+/, 'X-Ops-Authorization-3'=>/.+/, 'X-Ops-Authorization-4'=>/.+/, 'X-Ops-Authorization-5'=>/.+/, 'X-Ops-Authorization-6'=>/.+/, 'X-Ops-Content-Hash'=>/.+/, 'X-Ops-Server-Api-Version'=>'1', 'X-Ops-Sign'=>'algorithm=sha1;version=1.1;', 'X-Ops-Timestamp'=>/.+/, 'X-Ops-Userid'=>'spec-node', 'X-Remote-Request-Id'=>/.+/}). + to_return(:status => 200, :body => "", :headers => {}) + + @chef_compliance = Reporter::ChefServerCompliance.new(opts) + end + + it 'sends report successfully' do + expect(@chef_compliance.send_report(@report)).to eq(true) + end +end diff --git a/spec/unit/libraries/helpers_spec.rb b/spec/unit/libraries/helpers_spec.rb index 102649ab..c0583549 100644 --- a/spec/unit/libraries/helpers_spec.rb +++ b/spec/unit/libraries/helpers_spec.rb @@ -4,7 +4,7 @@ # Spec:: helpers require 'spec_helper' -require_relative '../../../libraries/reporters' +require_relative '../../../libraries/helper' describe ReportHelpers do let(:helpers) { Class.new { extend ReportHelpers } } @@ -16,23 +16,75 @@ end it 'report_timing_file returns where the report timing file is located' do - expect(@report.report_timing_file).to eq("#{Chef::Config[:file_cache_path]}/compliance/report_timing.json") + expect(@helpers.report_timing_file).to eq("#{Chef::Config[:file_cache_path]}/compliance/report_timing.json") + end + + it 'handle_reporters returns array of reporters when given array' do + reporters = ['chef-compliance', 'json-file'] + expect(@helpers.handle_reporters(reporters)).to eq(['chef-compliance', 'json-file']) + end + + it 'handle_reporters returns array of reporters when given string' do + reporters = 'chef-compliance' + expect(@helpers.handle_reporters(reporters)).to eq(['chef-compliance']) end it 'create_timestamp_file creates a new file' do - expected_file_path = @report.report_timing_file - @report.create_timestamp_file + expected_file_path = @helpers.report_timing_file + @helpers.create_timestamp_file expect(File).to exist("#{expected_file_path}") File.delete("#{expected_file_path}") end - it 'handle_reporters returns array of reporters when given array' do - reporters = ['chef-compliance', 'json-file'] - expect(@report.handle_reporters(reporters)).to eq(['chef-compliance', 'json-file']) + it 'returns the correct control status' do + expect(@helpers.control_status(nil)).to eq(nil) + expect(@helpers.control_status([{"status"=>"failed"}])).to eq('failed') + expect(@helpers.control_status([{"status"=>"passed"}])).to eq('passed') + expect(@helpers.control_status([{"status"=>"passed"},{"status"=>"failed"}])).to eq('failed') + expect(@helpers.control_status([{"status"=>"failed"},{"status"=>"passed"}])).to eq('failed') + expect(@helpers.control_status([{"status"=>"passed"},{"status"=>"skipped"}])).to eq('skipped') + expect(@helpers.control_status([{"status"=>"skipped"},{"status"=>"failed"}])).to eq('failed') end - it 'handle_reporters returns array of reporters when given string' do - reporters = 'chef-compliance' - expect(@report.handle_reporters(reporters)).to eq(['chef-compliance']) + it 'reports impact criticality correctly' do + expect(@helpers.impact_to_s(0)).to eq('minor') + expect(@helpers.impact_to_s(0.1234)).to eq('minor') + expect(@helpers.impact_to_s(0.4)).to eq('major') + expect(@helpers.impact_to_s(0.69)).to eq('major') + expect(@helpers.impact_to_s(0.7)).to eq('critical') + expect(@helpers.impact_to_s(1.0)).to eq('critical') + end + + it 'reports compliance status like a compliance officer' do + passed = {"total"=>5, "passed"=>{"total"=>3}, "skipped"=>{"total"=>2}, "failed"=>{"total"=>0, "minor"=>0, "major"=>0, "critical"=>0}} + failed = {"total"=>5, "passed"=>{"total"=>1}, "skipped"=>{"total"=>1}, "failed"=>{"total"=>3, "minor"=>1, "major"=>1, "critical"=>1}} + skipped = {"total"=>5, "passed"=>{"total"=>0}, "skipped"=>{"total"=>5}, "failed"=>{"total"=>0, "minor"=>0, "major"=>0, "critical"=>0}} + expect(@helpers.compliance_status(nil)).to eq('unknown') + expect(@helpers.compliance_status(failed)).to eq('failed') + expect(@helpers.compliance_status(passed)).to eq('passed') + expect(@helpers.compliance_status(skipped)).to eq('skipped') + end + + it 'counts controls like an accountant' do + profi = [{"name"=>"test-profile", + "controls"=> + [{"id"=>"Checking /etc/missing1.rb existance", + "impact"=>0, + "results"=>[{"status"=>"failed"}]}, + {"id"=>"Checking /etc/missing2.rb existance", + "impact"=>0.5, + "results"=>[{"status"=>"failed"}]}, + {"id"=>"Checking /etc/missing3.rb existance", + "impact"=>0.8, + "results"=>[{"status"=>"failed"}]}, + {"id"=>"Checking /etc/passwd existance", + "impact"=>0.88, + "results"=>[{"status"=>"passed"}]}, + {"id"=>"Checking /etc/something existance", + "impact"=>1.0, + "results"=>[{"status"=>"skipped"}]}] + }] + expected_count = {"total"=>5, "passed"=>{"total"=>1}, "skipped"=>{"total"=>1}, "failed"=>{"total"=>3, "minor"=>1, "major"=>1, "critical"=>1}} + expect(@helpers.count_controls(profi)).to eq(expected_count) end end diff --git a/spec/unit/libraries/json_file_spec.rb b/spec/unit/libraries/json_file_spec.rb index 39a58595..e2b4ffc5 100644 --- a/spec/unit/libraries/json_file_spec.rb +++ b/spec/unit/libraries/json_file_spec.rb @@ -4,14 +4,14 @@ # Spec:: json-file require 'spec_helper' -require_relative '../../../libraries/reporters' +require_relative '../../../libraries/reporters/json_file' describe 'Reporter::JsonFile methods' do it 'writes the report to a file on disk' do report = 'some info' timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S') - @jsonfile = Reporter::JsonFile.new(report, timestamp).send_report - expected_file_path = File.expand_path("../../../../inspec-#{timestamp}.json", __FILE__) + expected_file_path = File.expand_path("inspec-#{timestamp}.json", File.dirname(__FILE__)) + @jsonfile = Reporter::JsonFile.new({file: expected_file_path}).send_report(report) expect(File).to exist("#{expected_file_path}") File.delete("#{expected_file_path}") end