diff --git a/files/default/handler/audit_report.rb b/files/default/handler/audit_report.rb index 61a22f31..6e84a935 100644 --- a/files/default/handler/audit_report.rb +++ b/files/default/handler/audit_report.rb @@ -144,16 +144,16 @@ def call(opts, profiles) r.delete(:controls) # calculate statistics - stats = count_controls(JSON.parse(r[:profiles].to_json)) + stats = count_controls(r[:profiles]) time = 0 time = r[:statistics][:duration] unless r[:statistics].nil? # count controls - Chef::Log.info "Summary #{stats['total']} controls: #{stats['passed']['total']} successful, #{stats['failed']['total']} failures, #{stats['skipped']['total']} skipped in #{time} s" + Chef::Log.info "Summary #{stats[:total]} controls: #{stats[:passed][:total]} successful, #{stats[:failed][:total]} failures, #{stats[:skipped][:total]} skipped in #{time} s" end - - r.to_json + Chef::Log.debug "Audit Report #{r}" + r else Chef::Log.warn 'No audit tests are defined.' {} @@ -179,13 +179,12 @@ def gather_nodeinfo end # send InSpec report to the reporter (see libraries/reporters.rb) - def send_report(reporter, server, user, source_location, content) + def send_report(reporter, server, user, source_location, report) 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' diff --git a/libraries/helper.rb b/libraries/helper.rb index bb0936bd..0de1b9dd 100644 --- a/libraries/helper.rb +++ b/libraries/helper.rb @@ -142,37 +142,37 @@ def get_reporters(audit) # e.g. minor: 10, major: 15, critical: 25 def count_controls(profiles) count = { - 'total' => 0, - 'passed' => { - 'total' => 0, + total: 0, + passed: { + total: 0, }, - 'skipped' => { - 'total' => 0, + skipped: { + total: 0, }, - 'failed' => { - 'total' => 0, - 'minor' => 0, - 'major' => 0, - 'critical' => 0, + failed: { + total: 0, + minor: 0, + major: 0, + critical: 0, }, } return count unless profiles.is_a?(Array) profiles.each do |profile| - next unless profile && profile['controls'].is_a?(Array) - profile['controls'].each do |control| - count['total'] += 1 + next unless profile && profile[:controls].is_a?(Array) + profile[:controls].each do |control| + count[:total] += 1 # ensure all impacts are float - control['impact'] = control['impact'].to_f - case control_status(control['results']) + control[:impact] = control[:impact].to_f + case control_status(control[:results]) when 'passed' - count['passed']['total'] += 1 + count[:passed][:total] += 1 when 'skipped' - count['skipped']['total'] += 1 + count[:skipped][:total] += 1 when 'failed' - count['failed']['total'] += 1 - criticality = impact_to_s(control['impact']) - count['failed'][criticality] += 1 unless criticality.nil? + count[:failed][:total] += 1 + criticality = impact_to_s(control[:impact]) + count[:failed][criticality.to_sym] += 1 unless criticality.nil? end end end @@ -182,11 +182,11 @@ def count_controls(profiles) # Returns a complince status string based on the passed/failed/skipped controls def compliance_status(counts) return 'unknown' unless counts.is_a?(Hash) && - counts['failed'].is_a?(Hash) && - counts['skipped'].is_a?(Hash) - if counts['failed']['total'] > 0 + counts[:failed].is_a?(Hash) && + counts[:skipped].is_a?(Hash) + if counts[:failed][:total] > 0 'failed' - elsif counts['total'] == counts['skipped']['total'] + elsif counts[:total] == counts[:skipped][:total] 'skipped' else 'passed' @@ -210,8 +210,8 @@ def control_status(results) return unless results.is_a?(Array) status = 'passed' results.each do |result| - return 'failed' if result['status'] == 'failed' - status = 'skipped' if result['status'] == 'skipped' + return 'failed' if result[:status] == 'failed' + status = 'skipped' if result[:status] == 'skipped' end status end diff --git a/libraries/reporters/automate.rb b/libraries/reporters/automate.rb index b5cea61d..15faffd0 100644 --- a/libraries/reporters/automate.rb +++ b/libraries/reporters/automate.rb @@ -34,7 +34,7 @@ def send_report(report) return false end - json_report = enriched_report(report) + json_report = enriched_report(report).to_json unless json_report Chef::Log.warn 'Something went wrong, report can\'t be nil' @@ -75,27 +75,27 @@ def send_report(report) # 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 + # 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 + 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' + attrib[:options][:type] = 'string' when 'FalseClass' - attrib['options']['type'] = 'boolean' - attrib['options']['default'] = attrib['options']['default'].to_s + 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 + 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 + 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' + Chef::Log.warn "enriched_report: unsupported data type(#{attrib[:options][:default].class}) for attribute #{attrib[:options]['name']}" + attrib[:options][:type] = 'unknown' end } } @@ -109,31 +109,31 @@ def typed_attributes(profiles) def enriched_report(content) return nil unless content.is_a?(Hash) final_report = {} - total_duration = content['statistics']['duration'] - inspec_version = content['version'] + total_duration = content[:statistics][:duration] + inspec_version = content[:version] # strip the report to leave only the profiles - final_report['profiles'] = content['profiles'] + final_report[:profiles] = content[:profiles] # remove nil profiles if any - final_report['profiles'].select! { |p| p } + final_report[:profiles].select! { |p| p } # set types for profile attributes - final_report['profiles'] = typed_attributes(final_report['profiles']) + 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 + 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 end end end diff --git a/libraries/reporters/compliance.rb b/libraries/reporters/compliance.rb index a48a41ba..56d70a80 100644 --- a/libraries/reporters/compliance.rb +++ b/libraries/reporters/compliance.rb @@ -24,7 +24,7 @@ def send_report(report) min_report = transform(report) json_report = enriched_report(min_report, @source_location) - req.body = json_report + req.body = json_report.to_json # TODO: use secure option uri = URI(@url) @@ -50,7 +50,7 @@ def enriched_report(report, source_location) blob = @node_info.dup # extract profile names - profiles = report['controls'].collect { |control| control['profile_id'] }.uniq + profiles = report[:controls].collect { |control| control[:profile_id] }.uniq # build report for chef compliance, it includes node data blob[:reports] = {} @@ -62,41 +62,39 @@ def enriched_report(report, source_location) 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 + blob[:profiles][profile.to_sym] = namespace[0][:owner] + blob[:reports][profile.to_sym] = report.dup # filter controls by profile_id - blob[:reports][profile]['controls'] = blob[:reports][profile]['controls'].select { |control| control['profile_id'] == profile } + blob[:reports][profile.to_sym][:controls] = blob[:reports][profile.to_sym][:controls].select { |control| control[:profile_id] == profile } else Chef::Log.warn "Could not determine compliance namespace for #{profile}" end } - blob.to_json + blob end # transforms a full InSpec json report to a min InSpec json report def transform(full_report) min_report = {} - min_report['version'] = full_report[:version] + min_report[:version] = full_report[:version] # iterate over each profile and control - min_report['controls'] = [] + min_report[:controls] = [] full_report[:profiles].each { |profile| - min_report['controls'] += profile[:controls].map { |control| + min_report[:controls] += profile[:controls].map { |control| control[:results].map { |result| c = {} - c['id'] = control[:id] - c['profile_id'] = profile[:name] - c['status'] = result[:status] - c['code_desc'] = result[:code_desc] + c[:id] = control[:id] + c[:profile_id] = profile[:name] + c[:status] = result[:status] + c[:code_desc] = result[:code_desc] c } } } - min_report['controls'].flatten! - min_report['statistics'] = { - 'duration' => full_report[:statistics][:duration], - } + min_report[:controls].flatten! + min_report[:statistics] = full_report[:statistics] min_report end diff --git a/libraries/reporters/cs_automate.rb b/libraries/reporters/cs_automate.rb index 0096fcb6..a11e91b1 100644 --- a/libraries/reporters/cs_automate.rb +++ b/libraries/reporters/cs_automate.rb @@ -17,7 +17,7 @@ def initialize(opts) end def send_report(report) - json_report = enriched_report(report) + automate_report = enriched_report(report) if @insecure Chef::Config[:verify_api_cert] = false @@ -27,7 +27,7 @@ def send_report(report) 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)) + rest.post(@url, automate_report) return true end false diff --git a/libraries/reporters/cs_compliance.rb b/libraries/reporters/cs_compliance.rb index 3b275fed..3657bf11 100644 --- a/libraries/reporters/cs_compliance.rb +++ b/libraries/reporters/cs_compliance.rb @@ -10,7 +10,7 @@ module Reporter class ChefServerCompliance < ChefCompliance def send_report(report) min_report = transform(report) - json_report = enriched_report(min_report, @source_location) + cc_report = enriched_report(min_report, @source_location) # TODO: only disable if insecure option is set Chef::Config[:verify_api_cert] = false @@ -19,7 +19,7 @@ def send_report(report) 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)) + rest.post(@url, cc_report) return true end false diff --git a/spec/data/mock.rb b/spec/data/mock.rb index 4cbea036..6cdd0c52 100644 --- a/spec/data/mock.rb +++ b/spec/data/mock.rb @@ -4,39 +4,39 @@ def self.node_info end def self.inspec_results - {"version"=>"1.2.1", - "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"=> + {"version":"1.2.1", + "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..."}}]}], - "other_checks"=>[], - "statistics"=>{"duration"=>0.032332}} + "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..."}}]}], + "other_checks":[], + "statistics":{"duration":0.032332}} end end diff --git a/spec/unit/libraries/automate_spec.rb b/spec/unit/libraries/automate_spec.rb index 24a51e88..9fb455ca 100644 --- a/spec/unit/libraries/automate_spec.rb +++ b/spec/unit/libraries/automate_spec.rb @@ -26,52 +26,52 @@ 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"=> + @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"=> + "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"} + "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, @@ -83,7 +83,7 @@ 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'}). + :headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'identity', 'Content-Length'=>/.+/, '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) @@ -96,7 +96,7 @@ 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) + expect(@automate.enriched_report(MockData.inspec_results)).to eq(@enriched_report_expected) end it 'is not sending report when entity_uuid is missing' do @@ -113,30 +113,30 @@ it 'sets the attribute types like TypeScript' do profiles = [ { - "attributes"=>[ - { "name"=>"syslog_pkg", - "options"=>{ "default"=>"rsyslog", "description"=>"a string" } }, - { "name"=>"sysctl_forwarding", - "options"=>{ "default"=>false, "description"=>"a boolean" } }, - { "name"=>"some_number", - "options"=>{ "default"=>0, "description"=>"a number" } }, - { "name"=>"some_float", - "options"=>{ "default"=>0.8, "description"=>"a bloody float" } }, - { "name"=>"some_array", - "options"=>{ "default"=>[], "description"=>"a bloody array" } } + "attributes":[ + { "name":"syslog_pkg", + "options":{ "default":"rsyslog", "description":"a string" } }, + { "name":"sysctl_forwarding", + "options":{ "default":false, "description":"a boolean" } }, + { "name":"some_number", + "options":{ "default":0, "description":"a number" } }, + { "name":"some_float", + "options":{ "default":0.8, "description":"a bloody float" } }, + { "name":"some_array", + "options":{ "default":[], "description":"a bloody array" } } ] } ] # poor man's deep clone - types_profiles = JSON.parse(profiles.to_json) - types_profiles[0]['attributes'][0]['options']['type'] = 'string' - types_profiles[0]['attributes'][1]['options']['type'] = 'boolean' - types_profiles[0]['attributes'][1]['options']['default'] = 'false' - types_profiles[0]['attributes'][2]['options']['type'] = 'int' - types_profiles[0]['attributes'][2]['options']['default'] = '0' - 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' + types_profiles = profiles.dup + types_profiles[0][:attributes][0][:options][:type] = 'string' + types_profiles[0][:attributes][1][:options][:type] = 'boolean' + types_profiles[0][:attributes][1][:options][:default] = 'false' + types_profiles[0][:attributes][2][:options][:type] = 'int' + types_profiles[0][:attributes][2][:options][:default] = '0' + 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(@automate.typed_attributes(profiles)).to eq(types_profiles) end end diff --git a/spec/unit/libraries/compliance_spec.rb b/spec/unit/libraries/compliance_spec.rb index 4b86fe5a..3e3be24b 100644 --- a/spec/unit/libraries/compliance_spec.rb +++ b/spec/unit/libraries/compliance_spec.rb @@ -10,13 +10,13 @@ before :each do require 'bundles/inspec-compliance/configuration' url = 'https://192.168.33.201/api/owners/admin/inspec' - node_info = {:node=>"default-ubuntu-1404", :os=>{:release=>"14.04", :family=>"ubuntu"}, :environment=>"_default"} + node_info = {node:"default-ubuntu-1404", os:{release:"14.04", family:"ubuntu"}, environment:"_default"} raise_if_unreachable = true @report_min = { - "version" => "1.21.0", - "controls" => [{"id"=>"tmp-1.0","profile_id"=>"tmp_compliance_profile","status"=>"passed","code_desc"=>"File /tmp should be directory"},{"id"=>"tmp-1.1","profile_id"=>"tmp_compliance_profile","status"=>"passed","code_desc"=>"File /tmp should be owned by \"root\""}], - "statistics" => {"duration"=>0.028643} + "version":"1.21.0", + "controls":[{"id":"tmp-1.0","profile_id":"tmp_compliance_profile","status":"passed","code_desc":"File /tmp should be directory"},{"id":"tmp-1.1","profile_id":"tmp_compliance_profile","status":"passed","code_desc":"File /tmp should be owned by \"root\""}], + "statistics":{"duration":0.028643} } @report_full = { @@ -54,23 +54,21 @@ "statistics":{"duration":0.028643} } - # @compliance_profiles = [{:owner=> 'admin', :profile_id=> 'tmp_compliance_profile'}] - @source_location = [{ - "name": "tmp_compliance_profile", - "compliance": "admin/tmp_compliance_profile" + "name":"tmp_compliance_profile", + "compliance":"admin/tmp_compliance_profile" }] @enriched_report_expected = { - "node"=>"default-ubuntu-1404", - "os"=>{"release"=>"14.04", "family"=>"ubuntu"}, - "environment"=>"_default", - "reports"=>{"tmp_compliance_profile"=>{ - "version" => "1.21.0", - "controls" => [{"id"=>"tmp-1.0","profile_id"=>"tmp_compliance_profile","status"=>"passed","code_desc"=>"File /tmp should be directory"},{"id"=>"tmp-1.1","profile_id"=>"tmp_compliance_profile","status"=>"passed","code_desc"=>"File /tmp should be owned by \"root\""}], - "statistics" => {"duration"=>0.028643} + "node":"default-ubuntu-1404", + "os":{"release":"14.04", "family":"ubuntu"}, + "environment":"_default", + "reports":{"tmp_compliance_profile":{ + "version":"1.21.0", + "controls":[{"id":"tmp-1.0","profile_id":"tmp_compliance_profile","status":"passed","code_desc":"File /tmp should be directory"},{"id":"tmp-1.1","profile_id":"tmp_compliance_profile","status":"passed","code_desc":"File /tmp should be owned by \"root\""}], + "statistics":{"duration":0.028643} }}, - "profiles"=>{"tmp_compliance_profile"=>"admin"} + "profiles":{"tmp_compliance_profile":"admin"} } opts = { @@ -98,6 +96,6 @@ end it 'enriches the report correctly' do - expect(JSON.parse(@chef_compliance.enriched_report(@report_min, @source_location))).to eq(@enriched_report_expected) + expect(@chef_compliance.enriched_report(@report_min, @source_location)).to eq(@enriched_report_expected) end end diff --git a/spec/unit/libraries/cs_automate_spec.rb b/spec/unit/libraries/cs_automate_spec.rb index 1f8bf3e4..42429992 100644 --- a/spec/unit/libraries/cs_automate_spec.rb +++ b/spec/unit/libraries/cs_automate_spec.rb @@ -12,52 +12,52 @@ 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"=> + @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"=> + "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"} + "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, diff --git a/spec/unit/libraries/cs_compliance_spec.rb b/spec/unit/libraries/cs_compliance_spec.rb index d1932784..205635dd 100644 --- a/spec/unit/libraries/cs_compliance_spec.rb +++ b/spec/unit/libraries/cs_compliance_spec.rb @@ -51,15 +51,15 @@ "compliance": "admin/tmp_compliance_profile" }] @enriched_report_expected = { - "node"=>"default-ubuntu-1404", - "os"=>{"release"=>"14.04", "family"=>"ubuntu"}, - "environment"=>"_default", - "reports"=>{"tmp_compliance_profile"=>{ - "version" => "1.21.0", - "controls" => [{"id"=>"tmp-1.0","profile_id"=>"tmp_compliance_profile","status"=>"passed","code_desc"=>"File /tmp should be directory"},{"id"=>"tmp-1.1","profile_id"=>"tmp_compliance_profile","status"=>"passed","code_desc"=>"File /tmp should be owned by \"root\""}], - "statistics" => {"duration"=>0.028643} + "node": "default-ubuntu-1404", + "os": {"release": "14.04", "family": "ubuntu"}, + "environment": "_default", + "reports": {"tmp_compliance_profile": { + "version": "1.21.0", + "controls": [{"id": "tmp-1.0","profile_id": "tmp_compliance_profile","status": "passed","code_desc": "File /tmp should be directory"},{"id": "tmp-1.1","profile_id": "tmp_compliance_profile","status": "passed","code_desc": "File /tmp should be owned by \"root\""}], + "statistics": {"duration": 0.028643} }}, - "profiles"=>{"tmp_compliance_profile"=>"admin"} + "profiles": {"tmp_compliance_profile"=>"admin"} } opts = { diff --git a/spec/unit/libraries/helpers_spec.rb b/spec/unit/libraries/helpers_spec.rb index c0583549..06366523 100644 --- a/spec/unit/libraries/helpers_spec.rb +++ b/spec/unit/libraries/helpers_spec.rb @@ -38,12 +38,12 @@ 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') + 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 'reports impact criticality correctly' do @@ -56,9 +56,9 @@ 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}} + 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') @@ -66,25 +66,25 @@ 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"}]}] + profil = [{"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) + expected_count = {"total":5, "passed":{"total":1}, "skipped":{"total":1}, "failed":{"total":3, "minor":1, "major":1, "critical":1}} + expect(@helpers.count_controls(profil)).to eq(expected_count) end end diff --git a/spec/unit/report/audit_report_spec.rb b/spec/unit/report/audit_report_spec.rb index 05601aa9..5e164372 100644 --- a/spec/unit/report/audit_report_spec.rb +++ b/spec/unit/report/audit_report_spec.rb @@ -19,6 +19,7 @@ require 'spec_helper' require 'json' +require_relative '../../../libraries/helper' require_relative '../../../files/default/handler/audit_report' require_relative '../../data/mock.rb' @@ -73,21 +74,24 @@ describe 'call' do it 'given a profile, returns a json report' do - require 'inspec' opts = {'report' => true, 'format' => 'json', 'output' => '/dev/null'} path = File.expand_path('../../../data/mock_profile.rb', __FILE__) profiles = [{'name': 'example', 'path': path }] + # we cirumwent the default load mechanisms, therefore we have to require inspec + require 'inspec' report = @audit_report.call(opts, profiles) expected_report = /^.*version.*profiles.*controls.*statistics.*duration.*/ - expect(report).to match(expected_report) + expect(report.to_json).to match(expected_report) end it 'given a profile, returns a json-min report' do require 'inspec' opts = {'report' => true, 'format' => 'json-min', 'output' => '/dev/null'} path = File.expand_path('../../../data/mock_profile.rb', __FILE__) profiles = [{'name': 'example', 'path': path }] - report = JSON.parse(@audit_report.call(opts, profiles)) - expect(report['controls'].length).to eq(2) + # we cirumwent the default load mechanisms, therefore we have to require inspec + require 'inspec' + report = @audit_report.call(opts, profiles) + expect(report[:controls].length).to eq(2) end end end