Skip to content

Commit

Permalink
always use full json as inspec format
Browse files Browse the repository at this point in the history
Signed-off-by: Christoph Hartmann <chris@lollyrock.com>
  • Loading branch information
chris-rock committed May 3, 2017
1 parent e4e7fe8 commit edbd9c9
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 79 deletions.
62 changes: 25 additions & 37 deletions files/default/handler/audit_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand All @@ -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'
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
56 changes: 51 additions & 5 deletions libraries/reporters/compliance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]
Expand All @@ -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
3 changes: 2 additions & 1 deletion libraries/reporters/cs_compliance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 62 additions & 10 deletions spec/unit/libraries/compliance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <nharvey@chef.io>",
"license":"Apache 2.0 License",
"copyright":"Nathen Harvey <nharvey@chef.io>",
"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)
Expand All @@ -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
53 changes: 45 additions & 8 deletions spec/unit/libraries/cs_compliance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,69 @@
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 <nharvey@chef.io>",
"license":"Apache 2.0 License",
"copyright":"Nathen Harvey <nharvey@chef.io>",
"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__))
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'=>/.+/}).
: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)
Expand Down
Loading

0 comments on commit edbd9c9

Please sign in to comment.