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

Reduce report enrichment, bump cookbook to version 4.0.0 #224

Merged
merged 2 commits into from
May 12, 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
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,13 @@ This method is sending the report using the `data_collector.server_url` and `dat

If you are using a self-signed certificate, please also read [how to add the Chef Automate certificate to the trusted_certs directory](https://docs.chef.io/setup_visibility_chef_automate.html#add-chef-automate-certificate-to-trusted-certs-directory)

Version compatibility matrix:

| Automate version | InSpec version | Audit Cookbook version |
|--------------------|------------------|--------------------------|
| < 0.8.0 | ≤ 1.23.0 | ≤ 3.1.0 |
| ≥ 0.8.0 | ≥ 1.24.0 | ≥ 4.0.0 |

## Profile Upload to Compliance Server

In order to support build cookbook mode, the `compliance_profile` resource has an `upload` action that allows uploading a compressed
Expand Down Expand Up @@ -464,11 +471,11 @@ Please note that all dependencies to the `inspec` gem must also be hosted in thi

Using the `inspec_version` attribute, please use the following `InSpec` version based on your Chef Compliance version:

| Chef Compliance version | InSpec version | Audit Cookbook version |
|----------------------------|----------------------------|----------------------------|
| Less or equal to 1.1.23 | 0.20.1 | 0.7.0 |
| Greater than 1.1.23 | Greater or equal to 0.22.1 | 0.8.0 |
| Greater or equal to 1.6.8 | Greater or equal to 1.2.0 | 1.0.2 |
| Chef Compliance version | InSpec version | Audit Cookbook version |
|----------------------------|-------------------|---------------------------|
| 1.1.23 | = 0.20.1 | = 0.7.0 |
| > 1.1.23 | ≥ 0.22.1 | = 0.8.0 |
| 1.6.8 | ≥ 1.2.0 | = 1.0.2 |

You can see all publicly available InSpec versions [here](https://rubygems.org/gems/inspec/versions)

Expand Down
14 changes: 0 additions & 14 deletions files/default/handler/audit_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,6 @@ def call(opts, profiles)
Chef::Log.info "Summary: #{passed_controls} successful, #{failed_controls} failures, #{skipped_controls} skipped in #{time} s"
end

if !r.nil? && 'json' == opts['format']
# ensure controls are never stored or shipped, since this was an accidential
# addition in InSpec and will be remove in our next major release
r.delete(:controls)

# calculate statistics
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"
end
Chef::Log.debug "Audit Report #{r}"
r
else
Expand Down
84 changes: 0 additions & 84 deletions libraries/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,90 +133,6 @@ def get_reporters(audit)
end
handle_reporters(audit['reporter'])
end

# Returns a hash with the counted controls based on their status and criticality
# This working on full-json reports
# total: count for all controls in the report, e.g. 100
# successful: count for all controls that executed successfully, e.g. 40
# skipped: count for all skipped controls, e.g. 10
# failed: count for all failed controls, e.g. 50
# minor, major, critical: split the failed count in 3 buckets based on the criticality,
# e.g. minor: 10, major: 15, critical: 25
def count_controls(profiles)
count = {
total: 0,
passed: {
total: 0,
},
skipped: {
total: 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
# ensure all impacts are float
control[:impact] = control[:impact].to_f
case control_status(control[:results])
when 'passed'
count[:passed][:total] += 1
when 'skipped'
count[:skipped][:total] += 1
when 'failed'
count[:failed][:total] += 1
criticality = impact_to_s(control[:impact])
count[:failed][criticality.to_sym] += 1 unless criticality.nil?
end
end
end
count
end

# 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
'failed'
elsif counts[:total] == counts[:skipped][:total]
'skipped'
else
'passed'
end
end

# Returns a string with the control criticality based on the impact value
def impact_to_s(impact)
if impact < 0.4
'minor'
elsif impact < 0.7
'major'
else
'critical'
end
end

# A control can have multiple tests. Returns 'passed' unless any
# of the results has a status different than 'passed'
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'
end
status
end
end

::Chef::Recipe.send(:include, ReportHelpers)
39 changes: 17 additions & 22 deletions libraries/reporters/automate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(opts)
@entity_uuid = opts[:entity_uuid]
@run_id = opts[:run_id]
@node_name = opts[:node_info][:node]
@environment = opts[:node_info][:environment]
@insecure = opts[:insecure]

if defined?(Chef) &&
Expand Down Expand Up @@ -59,6 +60,7 @@ def send_report(report)

begin
Chef::Log.warn "Report to Chef Automate: #{@url}"
Chef::Log.debug "Audit Report: #{json_report}"
http = Chef::HTTP.new(@url)
http.post(nil, json_report, headers)
return true
Expand Down Expand Up @@ -106,33 +108,26 @@ def typed_attributes(profiles)
# revamp of the Automate expected input.
# ***************************************************************************************

def enriched_report(content)
return nil unless content.is_a?(Hash)
final_report = {}
total_duration = content[:statistics][:duration]
inspec_version = content[:version]
def enriched_report(final_report)
return nil unless final_report.is_a?(Hash)

# strip the report to leave only the profiles
final_report[:profiles] = content[:profiles]

# remove nil profiles if any
# Remove nil profiles if any
final_report[:profiles].select! { |p| p }

# set types for profile attributes
# Set types for profile attributes
final_report[:profiles] = typed_attributes(final_report[:profiles])

# add some additional fields to ease report parsing
final_report[:event_type] = 'inspec'
final_report[:event_action] = 'exec'
final_report[:compliance_summary] = count_controls(final_report[:profiles])
final_report[:compliance_summary][:status] = compliance_status(final_report[:compliance_summary])
final_report[:compliance_summary][:node_name] = @node_name
final_report[:compliance_summary][:end_time] = DateTime.now.iso8601
final_report[:compliance_summary][:duration] = total_duration
final_report[:compliance_summary][:inspec_version] = inspec_version
final_report[:entity_uuid] = @entity_uuid
final_report[:run_id] = @run_id
Chef::Log.info "Compliance Summary #{final_report[:compliance_summary]}"
# Label this content as an inspec_report
final_report[:type] = 'inspec_report'

# Ensure controls are never stored or shipped, since this was an accidential
# addition in InSpec and will be remove in the next inspec major release
final_report.delete(:controls)
final_report[:node_name] = @node_name
final_report[:end_time] = DateTime.now.iso8601
final_report[:node_uuid] = @entity_uuid
final_report[:environment] = @environment
final_report[:report_uuid] = @run_id
final_report
end
end
Expand Down
1 change: 1 addition & 0 deletions libraries/reporters/cs_automate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def initialize(opts)
@run_id = opts[:run_id]
@node_name = opts[:node_info][:node]
@insecure = opts[:insecure]
@environment = opts[:node_info][:environment]
@url = opts[:url]
end

Expand Down
2 changes: 1 addition & 1 deletion metadata.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
license 'Apache-2.0'
description 'Allows for fetching and executing compliance profiles, and reporting its results'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version '3.1.0'
version '4.0.0'

source_url 'https://github.com/chef-cookbooks/audit'
issues_url 'https://github.com/chef-cookbooks/audit/issues'
Expand Down
3 changes: 2 additions & 1 deletion spec/data/mock.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class MockData
def self.node_info
{ node: "chef-client.solo" }
{ node: "chef-client.solo",
environment: 'My Prod Env' }
end

def self.inspec_results
Expand Down
28 changes: 12 additions & 16 deletions spec/unit/libraries/automate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
entity_uuid = 'aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz'
run_id = '3f0536f7-3361-4bca-ae53-b45118dceb5d'
insecure = false
@enriched_report_expected = { "profiles":
@enriched_report_expected = {"version":"1.2.1",
"profiles":
[{"name":"tmp_compliance_profile",
"title":"/tmp Compliance Profile",
"summary":"An Example Compliance Profile",
Expand Down Expand Up @@ -58,20 +59,15 @@
"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"}
"other_checks": [],
"statistics":{"duration":0.032332},
"type":"inspec_report",
"node_name":"chef-client.solo",
"end_time":"2016-07-19T19:19:19+01:00",
"node_uuid":"aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz",
"environment":"My Prod Env",
"report_uuid":"3f0536f7-3361-4bca-ae53-b45118dceb5d"
}

opts = {
entity_uuid: entity_uuid,
Expand All @@ -89,7 +85,7 @@
@automate = Reporter::ChefAutomate.new(opts)
end

it 'sends report successfully' do
it 'sends report successfully to ChefAutomate' do
allow(DateTime).to receive(:now).and_return(DateTime.parse('2016-07-19T19:19:19+01:00'))
expect(@automate.send_report(MockData.inspec_results)).to eq(true)
end
Expand Down
30 changes: 13 additions & 17 deletions spec/unit/libraries/cs_automate_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
entity_uuid = 'aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz'
run_id = '3f0536f7-3361-4bca-ae53-b45118dceb5d'
insecure = false
@enriched_report_expected = { "profiles":
@enriched_report_expected = { "version": "1.2.1",
"profiles":
[{"name": "tmp_compliance_profile",
"title": "/tmp Compliance Profile",
"summary": "An Example Compliance Profile",
Expand Down Expand Up @@ -44,20 +45,15 @@
"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"}
"other_checks": [],
"statistics":{"duration":0.032332},
"type": "inspec_report",
"node_name": "chef-client.solo",
"end_time": "2016-07-19T19:19:19+01:00",
"node_uuid": "aaaaaaaa-709a-475d-bef5-zzzzzzzzzzzz",
"environment": "My Prod Env",
"report_uuid": "3f0536f7-3361-4bca-ae53-b45118dceb5d"
}

opts = {
entity_uuid: entity_uuid,
Expand All @@ -73,12 +69,12 @@
# set data_collector
stub_request(:post, 'https://chef.server/data_collector').
with(:body => @enriched_report_expected.to_json,
:headers => {'Accept'=>'application/json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Length'=>'2818', 'Content-Type'=>'application/json', 'Host'=>/.+/, 'User-Agent'=>/.+/, 'X-Chef-Version'=>/.+/, 'X-Ops-Authorization-1'=>/.+/, 'X-Ops-Authorization-2'=>/.+/, 'X-Ops-Authorization-3'=>/.+/, 'X-Ops-Authorization-4' => /.+/, 'X-Ops-Authorization-5'=>/.+/, 'X-Ops-Authorization-6'=>/.+/, 'X-Ops-Content-Hash'=>/.+/, 'X-Ops-Server-Api-Version'=>'1', 'X-Ops-Sign'=>'algorithm=sha1;version=1.1;', 'X-Ops-Timestamp'=>/.+/, 'X-Ops-Userid'=>'spec-node', 'X-Remote-Request-Id'=>/.+/}).
:headers => {'Accept'=>'application/json', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Content-Length'=>'2706', 'Content-Type'=>'application/json', 'Host'=>/.+/, 'User-Agent'=>/.+/, 'X-Chef-Version'=>/.+/, 'X-Ops-Authorization-1'=>/.+/, 'X-Ops-Authorization-2'=>/.+/, 'X-Ops-Authorization-3'=>/.+/, 'X-Ops-Authorization-4' => /.+/, 'X-Ops-Authorization-5'=>/.+/, 'X-Ops-Authorization-6'=>/.+/, 'X-Ops-Content-Hash'=>/.+/, 'X-Ops-Server-Api-Version'=>'1', 'X-Ops-Sign'=>'algorithm=sha1;version=1.1;', 'X-Ops-Timestamp'=>/.+/, 'X-Ops-Userid'=>'spec-node', 'X-Remote-Request-Id'=>/.+/}).
to_return(:status => 200, :body => "", :headers => {})
@automate = Reporter::ChefServerAutomate.new(opts)
end

it 'sends report successfully' do
it 'sends report successfully to ChefServerAutomate' do
allow(DateTime).to receive(:now).and_return(DateTime.parse('2016-07-19T19:19:19+01:00'))
expect(@automate.send_report(MockData.inspec_results)).to eq(true)
end
Expand Down
52 changes: 0 additions & 52 deletions spec/unit/libraries/helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,56 +35,4 @@
expect(File).to exist("#{expected_file_path}")
File.delete("#{expected_file_path}")
end

it 'returns the correct control status' do
expect(@helpers.control_status(nil)).to eq(nil)
expect(@helpers.control_status([{"status":"failed"}])).to eq('failed')
expect(@helpers.control_status([{"status":"passed"}])).to eq('passed')
expect(@helpers.control_status([{"status":"passed"},{"status":"failed"}])).to eq('failed')
expect(@helpers.control_status([{"status":"failed"},{"status":"passed"}])).to eq('failed')
expect(@helpers.control_status([{"status":"passed"},{"status":"skipped"}])).to eq('skipped')
expect(@helpers.control_status([{"status":"skipped"},{"status":"failed"}])).to eq('failed')
end

it 'reports impact criticality correctly' do
expect(@helpers.impact_to_s(0)).to eq('minor')
expect(@helpers.impact_to_s(0.1234)).to eq('minor')
expect(@helpers.impact_to_s(0.4)).to eq('major')
expect(@helpers.impact_to_s(0.69)).to eq('major')
expect(@helpers.impact_to_s(0.7)).to eq('critical')
expect(@helpers.impact_to_s(1.0)).to eq('critical')
end

it 'reports compliance status like a compliance officer' do
passed = {"total":5, "passed":{"total":3}, "skipped":{"total":2}, "failed":{"total":0, "minor":0, "major":0, "critical":0}}
failed = {"total":5, "passed":{"total":1}, "skipped":{"total":1}, "failed":{"total":3, "minor":1, "major":1, "critical":1}}
skipped = {"total":5, "passed":{"total":0}, "skipped":{"total":5}, "failed":{"total":0, "minor":0, "major":0, "critical":0}}
expect(@helpers.compliance_status(nil)).to eq('unknown')
expect(@helpers.compliance_status(failed)).to eq('failed')
expect(@helpers.compliance_status(passed)).to eq('passed')
expect(@helpers.compliance_status(skipped)).to eq('skipped')
end

it 'counts controls like an accountant' do
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(profil)).to eq(expected_count)
end
end