Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always use json format for inspec report #212

Merged
merged 1 commit into from
May 3, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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