From edbd9c94a00c0ab249b6730c017cd227b06468ea Mon Sep 17 00:00:00 2001 From: Christoph Hartmann Date: Tue, 2 May 2017 19:27:10 +0200 Subject: [PATCH] always use full json as inspec format Signed-off-by: Christoph Hartmann --- files/default/handler/audit_report.rb | 62 ++++++++----------- libraries/reporters/compliance.rb | 56 ++++++++++++++++-- libraries/reporters/cs_compliance.rb | 3 +- spec/unit/libraries/compliance_spec.rb | 72 +++++++++++++++++++---- spec/unit/libraries/cs_compliance_spec.rb | 53 ++++++++++++++--- spec/unit/report/audit_report_spec.rb | 24 ++------ 6 files changed, 191 insertions(+), 79 deletions(-) diff --git a/files/default/handler/audit_report.rb b/files/default/handler/audit_report.rb index a8f8c645..61a22f31 100644 --- a/files/default/handler/audit_report.rb +++ b/files/default/handler/audit_report.rb @@ -38,28 +38,32 @@ def report reporters.include?('chef-server-automate') || node['audit']['fetcher'] == 'chef-server' - # iterate through reporters - reporters.each do |reporter| - # ensure authentication for Chef Compliance is in place, see libraries/compliance.rb - login_to_compliance(server, user, token, refresh_token) if reporter == 'chef-compliance' + # ensure authentication for Chef Compliance is in place, see libraries/compliance.rb + login_to_compliance(server, user, token, refresh_token) if reporters.include?('chef-compliance') - # true if profile is due to run (see libraries/helper.rb) - if check_interval_settings(interval, interval_enabled, interval_time) + # true if profile is due to run (see libraries/helper.rb) + if check_interval_settings(interval, interval_enabled, interval_time) - # create a file with a timestamp to calculate interval timing - create_timestamp_file if interval_enabled + # create a file with a timestamp to calculate interval timing + create_timestamp_file if interval_enabled - # return hash of opts to be used by runner - opts = get_opts(reporter, quiet) + # return hash of opts to be used by runner + opts = get_opts('json', quiet) - # instantiate inspec runner with given options and run profiles; return report - report = call(opts, profiles) + # instantiate inspec runner with given options and run profiles; return report + report = call(opts, profiles) - # send report to the correct reporter (automate, compliance, chef-server) - send_report(reporter, server, user, profiles, report) + # send report to the correct reporter (automate, compliance, chef-server) + if !report.empty? + # iterate through reporters + reporters.each do |reporter| + send_report(reporter, server, user, profiles, report) + end else - Chef::Log.info 'Audit run skipped due to interval configuration' + Chef::Log.error 'Audit report was not generated properly, skipped reporting' end + else + Chef::Log.info 'Audit run skipped due to interval configuration' end end @@ -97,10 +101,8 @@ def load_chef_fetcher end # sets format to json-min when chef-compliance, json when chef-automate - def get_opts(reporter, quiet) - format = ['chef-visibility', 'chef-server-visibility', 'chef-automate', 'chef-server-automate'].include?(reporter) ? 'json' : 'json-min' + def get_opts(format, quiet) output = quiet ? ::File::NULL : $stdout - Chef::Log.warn "Format is #{format}" opts = { 'report' => true, @@ -176,24 +178,8 @@ def gather_nodeinfo } end - # this is a helper methods to extract the profiles we scan and hand this - # over to the reporter in addition to the `json-min` report. `json-min` - # reports do not include information about the source of the profiles - # TODO: should be available in inspec `json-min` reports out-of-the-box - # TODO: raise warning when not a compliance-known profile - def cc_profile_index(profiles) - cc_profiles = tests_for_runner(profiles).select { |profile| profile[:compliance] }.map { |profile| profile[:compliance] }.uniq.compact - cc_profiles.map { |profile| - owner, profile_id = profile.split('/') - { - owner: owner, - profile_id: profile_id, - } - } - end - # send InSpec report to the reporter (see libraries/reporters.rb) - def send_report(reporter, server, user, profiles, content) + def send_report(reporter, server, user, source_location, content) Chef::Log.info "Reporting to #{reporter}" # Set `insecure` here to avoid passing 6 aruguments to `AuditReport#send_report` @@ -208,6 +194,7 @@ def send_report(reporter, server, user, profiles, content) run_id: run_status.run_id, node_info: gather_nodeinfo, insecure: insecure, + source_location: source_location, } Reporter::ChefAutomate.new(opts).send_report(report) elsif reporter == 'chef-server-visibility' || reporter == 'chef-server-automate' @@ -221,6 +208,7 @@ def send_report(reporter, server, user, profiles, content) node_info: gather_nodeinfo, insecure: insecure, url: url, + source_location: source_location, } Reporter::ChefServerAutomate.new(opts).send_report(report) else @@ -240,8 +228,8 @@ def send_report(reporter, server, user, profiles, content) url: url, node_info: gather_nodeinfo, raise_if_unreachable: raise_if_unreachable, - profile_index: cc_profile_index(profiles), token: token, + source_location: source_location, } Reporter::ChefCompliance.new(opts).send_report(report) else @@ -256,7 +244,7 @@ def send_report(reporter, server, user, profiles, content) url: url, node_info: gather_nodeinfo, raise_if_unreachable: raise_if_unreachable, - profile_index: cc_profile_index(profiles), + source_location: source_location, } Reporter::ChefServer.new(opts).send_report(report) else diff --git a/libraries/reporters/compliance.rb b/libraries/reporters/compliance.rb index f868fda4..a48a41ba 100644 --- a/libraries/reporters/compliance.rb +++ b/libraries/reporters/compliance.rb @@ -13,16 +13,17 @@ 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] + @source_location = opts[:source_location] 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) + min_report = transform(report) + json_report = enriched_report(min_report, @source_location) req.body = json_report # TODO: use secure option @@ -44,7 +45,8 @@ def send_report(report) # TODO: add to docs that all profiles used in Chef Compliance, need to # be uploaded to Chef Compliance first - def enriched_report(report) + def enriched_report(report, source_location) + compliance_profiles = cc_profile_index(source_location) blob = @node_info.dup # extract profile names @@ -56,8 +58,8 @@ def enriched_report(report) 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 } + 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] @@ -71,5 +73,49 @@ def enriched_report(report) blob.to_json 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] + + # iterate over each profile and control + min_report['controls'] = [] + full_report[:profiles].each { |profile| + 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 + } + } + } + min_report['controls'].flatten! + min_report['statistics'] = { + 'duration' => full_report[:statistics][:duration], + } + min_report + end + + private + + # this is a helper methods to extract the profiles we scan and hand this + # over to the reporter in addition to the `json-min` report. `json-min` + # reports do not include information about the source of the profiles + # TODO: should be available in inspec `json-min` reports out-of-the-box + # TODO: raise warning when not a compliance-known profile + def cc_profile_index(profiles) + cc_profiles = tests_for_runner(profiles).select { |profile| profile[:compliance] }.map { |profile| profile[:compliance] }.uniq.compact + cc_profiles.map { |profile| + owner, profile_id = profile.split('/') + { + owner: owner, + profile_id: profile_id, + } + } + end end end diff --git a/libraries/reporters/cs_compliance.rb b/libraries/reporters/cs_compliance.rb index b786695d..3b275fed 100644 --- a/libraries/reporters/cs_compliance.rb +++ b/libraries/reporters/cs_compliance.rb @@ -9,7 +9,8 @@ module Reporter # class ChefServerCompliance < ChefCompliance def send_report(report) - json_report = enriched_report(report) + min_report = transform(report) + json_report = enriched_report(min_report, @source_location) # TODO: only disable if insecure option is set Chef::Config[:verify_api_cert] = false diff --git a/spec/unit/libraries/compliance_spec.rb b/spec/unit/libraries/compliance_spec.rb index 5b725a1f..4b86fe5a 100644 --- a/spec/unit/libraries/compliance_spec.rb +++ b/spec/unit/libraries/compliance_spec.rb @@ -12,25 +12,73 @@ 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 = { - "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} + + @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} + } + + @report_full = { + "version":"1.21.0", + "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":"", + "source_location":{"ref":"tmp_compliance_profile-master/controls/tmp.rb","line":3}, + "id":"tmp-1.0", + "results":[{"status":"passed","code_desc":"File /tmp should be directory","run_time":0.001562,"start_time":"2017-05-02 21:23:03 +0200"}] + },{ + "title":"/tmp directory is owned by the root user", + "desc":"The /tmp directory must be owned by the root user", + "impact":0.3,"refs":[],"tags":{},"code":"", + "source_location":{"ref":"tmp_compliance_profile-master/controls/tmp.rb","line":12}, + "id":"tmp-1.1", + "results":[{"status":"passed","code_desc":"File /tmp should be owned by \"root\"","run_time":0.023661,"start_time":"2017-05-02 21:23:03 +0200"}] + }], + "groups":[{ + "title":"/tmp Compliance Profile", + "controls":["tmp-1.0","tmp-1.1"], + "id":"controls/tmp.rb"}], + "attributes":[] + }], + "statistics":{"duration":0.028643} } - compliance_profiles = [{:owner=> 'admin', :profile_id=> 'ssh'}] + + # @compliance_profiles = [{:owner=> 'admin', :profile_id=> 'tmp_compliance_profile'}] + + @source_location = [{ + "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"=>{"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"} + "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"} } opts = { url: url, node_info: node_info, raise_if_unreachable: raise_if_unreachable, - compliance_profiles: compliance_profiles, - token: 1234 + token: 1234, + source_location: @source_location, } @chef_compliance = Reporter::ChefCompliance.new(opts) @@ -42,10 +90,14 @@ end it 'sends report successfully' do - expect(@chef_compliance.send_report(@report)).to eq(true) + expect(@chef_compliance.send_report(@report_full)).to eq(true) + end + + it 'transforms full json to min-json' do + expect(@chef_compliance.transform(@report_full)).to eq(@report_min) end it 'enriches the report correctly' do - expect(JSON.parse(@chef_compliance.enriched_report(@report))).to eq(@enriched_report_expected) + expect(JSON.parse(@chef_compliance.enriched_report(@report_min, @source_location))).to eq(@enriched_report_expected) end end diff --git a/spec/unit/libraries/cs_compliance_spec.rb b/spec/unit/libraries/cs_compliance_spec.rb index cfc389af..d1932784 100644 --- a/spec/unit/libraries/cs_compliance_spec.rb +++ b/spec/unit/libraries/cs_compliance_spec.rb @@ -13,24 +13,61 @@ 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} + "version":"1.21.0", + "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":"", + "source_location":{"ref":"tmp_compliance_profile-master/controls/tmp.rb","line":3}, + "id":"tmp-1.0", + "results":[{"status":"passed","code_desc":"File /tmp should be directory","run_time":0.001562,"start_time":"2017-05-02 21:23:03 +0200"}] + },{ + "title":"/tmp directory is owned by the root user", + "desc":"The /tmp directory must be owned by the root user", + "impact":0.3,"refs":[],"tags":{},"code":"", + "source_location":{"ref":"tmp_compliance_profile-master/controls/tmp.rb","line":12}, + "id":"tmp-1.1", + "results":[{"status":"passed","code_desc":"File /tmp should be owned by \"root\"","run_time":0.023661,"start_time":"2017-05-02 21:23:03 +0200"}] + }], + "groups":[{ + "title":"/tmp Compliance Profile", + "controls":["tmp-1.0","tmp-1.1"], + "id":"controls/tmp.rb"}], + "attributes":[] + }], + "statistics":{"duration":0.028643} } - compliance_profiles = [{:owner=> 'admin', :profile_id=> 'ssh'}] + @source_location = [{ + "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"=>{"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"} + "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"} } opts = { url: url, node_info: node_info, raise_if_unreachable: raise_if_unreachable, - compliance_profiles: compliance_profiles, - token: 1234 + token: 1234, + source_location: @source_location } Chef::Config[:client_key] = File.expand_path("../../chef-client.pem", File.dirname(__FILE__)) @@ -38,7 +75,7 @@ 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'=>/.+/}). + :headers => {'Accept'=>'application/json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Length'=>/.+/, '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) diff --git a/spec/unit/report/audit_report_spec.rb b/spec/unit/report/audit_report_spec.rb index 941f93ce..05601aa9 100644 --- a/spec/unit/report/audit_report_spec.rb +++ b/spec/unit/report/audit_report_spec.rb @@ -57,28 +57,16 @@ end describe 'get_opts method' do - it 'given chef-compliance sets the format to json-min' do - reporter = 'chef-compliance' + it 'sets the format to json-min' do + format = 'json-min' quiet = true - opts = @audit_report.get_opts(reporter, quiet) + opts = @audit_report.get_opts(format, quiet) expect(opts).to eq({'report' => true, 'format' => 'json-min', 'output' => '/dev/null', 'logger' => Chef::Log}) end - it 'given chef-server sets the format to json-min' do - reporter = 'chef-server' + it 'sets the format to json-min' do + format = 'json' quiet = true - opts = @audit_report.get_opts(reporter, quiet) - expect(opts).to eq({'report' => true, 'format' => 'json-min', 'output' => '/dev/null', 'logger' => Chef::Log}) - end - it 'given chef-visibility sets the format to json' do - reporter = 'chef-visibility' - quiet = true - opts = @audit_report.get_opts(reporter, quiet) - expect(opts).to eq({'report' => true, 'format' => 'json', 'output' => '/dev/null', 'logger' => Chef::Log}) - end - it 'given chef-server-visibility sets the format to json' do - reporter = 'chef-server-visibility' - quiet = true - opts = @audit_report.get_opts(reporter, quiet) + opts = @audit_report.get_opts(format, quiet) expect(opts).to eq({'report' => true, 'format' => 'json', 'output' => '/dev/null', 'logger' => Chef::Log}) end end