Skip to content

Commit

Permalink
tests and cleanup
Browse files Browse the repository at this point in the history
Signed-off-by: Victoria Jeffrey <vjeffrey@chef.io>
  • Loading branch information
Victoria Jeffrey committed Oct 31, 2016
1 parent 2c5d191 commit d544e1e
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 91 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,31 @@ You can enable the interval and set the interval time, along with your desired p

```

## Write to file

If you would like to write the json report to a file on disk, you can enable the write to file attribute.
Note: If write to file is enabled, interval timing may not be enabled. This is because when we write
the file to disk, the file is named with a timestamp. When we do interval timing, we write a file
with a simple name of the profiles, to enable easy checking for existence.

```json

"audit": {
"profiles": [
{
"name": "ssh",
"compliance": "base/ssh"
},
{
"name": "linux",
"compliance": "base/linux"
}
],
"write_to_file": true
}

```

## Troubleshooting

Please refer to TROUBLESHOOTING.md.
Expand Down
5 changes: 2 additions & 3 deletions attributes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,8 @@
# set profiles to empty array as default
default['audit']['profiles'] = []

# output for inspec results
result_path = File.expand_path('../../inspec_results.json', __FILE__)
default['audit']['output'] = result_path
# write json report to file on disk
default['audit']['write_to_file'] = false

# inspec gem version to install(e.g. '1.1.0')
default['audit']['inspec_version'] = '1.2.0'
91 changes: 40 additions & 51 deletions files/default/audit_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,32 @@ def report
interval_time = node['audit']['interval']['time']
profiles = node['audit']['profiles']
quiet = node['audit']['quiet']
write_to_file = node['audit']['write_to_file']

# used to ensure there are no conflicting attributes (see libraries/helper.rb)
if check_attributes(write_to_file, interval_enabled) == false
Chef::Log.error 'Please have a look at your attributes. Only one each of interval enabled and write to file may be set to true for filename writing purposes.'
end

# load inspec, supermarket bundle and compliance bundle
load_needed_dependencies

# ensure authentication for Chef Compliance is in place
# ensure authentication for Chef Compliance is in place, see libraries/compliance.rb
login_to_compliance(server, user, token, refresh_token) if reporter == 'chef-compliance'

# true if profile is due to run (see libraries/helper.rb)
if check_interval_settings(interval, interval_enabled, interval_time, profiles)
call(reporter, quiet, server, user, profiles)
# return hash of opts to be used by runner
opts = get_opts(reporter, quiet)

# instantiate inspec runner with given options and run profiles; return report
report = call(opts, profiles)

# creates file on disk if interval reporting is enabled or write_to_file is enabled (see libraries/helper.rb)
write_to_file(report, profiles, interval_enabled, write_to_file) if interval_enabled || write_to_file

# send report to the correct reporter (visibility, compliance, chef-server)
send_report(reporter, server, user, profiles, report)
else
Chef::Log.error 'Please take a look at your interval settings'
end
Expand All @@ -43,57 +61,27 @@ def load_needed_dependencies
require 'bundles/inspec-compliance/target'
end

# TODO: temporary, we should not use this
# TODO: harmonize with CLI login_refreshtoken method
def login_to_compliance(server, user, access_token, refresh_token)
if !refresh_token.nil?
success, msg, access_token = Compliance::API.get_token_via_refresh_token(server, refresh_token, true)
else
success = true
end

if success
config = Compliance::Configuration.new
config['user'] = user
config['server'] = server
config['token'] = access_token
config['insecure'] = true
config['version'] = Compliance::API.version(server, true)
config.store
else
Chef::Log.error msg
raise('Could not store authentication token')
end
end

def check_interval_settings(interval, interval_enabled, interval_time, profiles)
# handle intervals
interval_seconds = 0 # always run this by default, unless interval is defined
if !interval.nil? && interval_enabled
interval_seconds = interval_time * 60 # seconds in interval
Chef::Log.debug "Auditing this machine every #{interval_seconds} seconds "
end
# returns true if profile is overdue to run
profile_overdue_to_run?(interval_seconds, profiles)
end

def call(reporter, quiet, server, user, profiles)
Chef::Log.debug 'Initialize InSpec'
# sets format to json-min when chef-compliance, json when chef-visibility
def get_opts(reporter, quiet)
format = reporter == 'chef-visibility' ? 'json' : 'json-min'
output = quiet ? ::File::NULL : $stdout
Chef::Log.warn "Format is #{format}"
opts = { 'report' => true, 'format' => format, 'output' => output }
{ 'report' => true, 'format' => format, 'output' => output }
end

# run profiles and return report
def call(opts, profiles)
Chef::Log.debug 'Initialize InSpec'
Chef::Log.debug "Options are set to: #{opts}"
runner = ::Inspec::Runner.new(opts)

# parse profile hashes for runner, see libraries/helper.rb
tests = tests_for_runner(profiles)
tests.each { |target| runner.add_target(target, opts) }

Chef::Log.info "Running tests from: #{tests.inspect}"
runner.run
report = runner.report.to_json
write_to_file(report, profiles)
send_report(reporter, server, user, profiles, report)
runner.report.to_json
end

# extracts relevant node data
Expand All @@ -110,12 +98,13 @@ def gather_nodeinfo
}
end

# send report to the collector (see libraries/collector_classes.rb)
def send_report(reporter, server, user, profiles, report)
Chef::Log.info "Reporting to #{reporter}"

# TODO: harmonize reporter interface
if reporter == 'chef-visibility'
Collector::ChefVisibility.new(entity_uuid, run_id, gather_nodeinfo[:node], report).send_report
Collector::ChefVisibility.new(entity_uuid, run_id, gather_nodeinfo, report).send_report

elsif reporter == 'chef-compliance'
raise_if_unreachable = node['audit']['raise_if_unreachable']
Expand All @@ -137,14 +126,14 @@ def send_report(reporter, server, user, profiles, report)
Chef::Log.warn "'server' and 'token' properties required by inspec report collector #{reporter}. Skipping..."
end

elsif reporter == 'chef-server'
chef_url = server || base_chef_server_url
if chef_url
url = construct_url(chef_url + '/compliance/', File.join('organizations', user, 'inspec'))
Collector::ChefServer.new(url).send_report
else
Chef::Log.warn "unable to determine chef-server url required by inspec report collector '#{reporter}'. Skipping..."
end
# elsif reporter == 'chef-server'
# chef_url = server || base_chef_server_url
# if chef_url
# url = construct_url(chef_url + '/compliance/', File.join('organizations', user, 'inspec'))
# Collector::ChefServer.new(url).send_report
# else
# Chef::Log.warn "unable to determine chef-server url required by inspec report collector '#{reporter}'. Skipping..."
# end
else
Chef::Log.warn "#{reporter} is not a supported InSpec report collector"
end
Expand Down
4 changes: 2 additions & 2 deletions libraries/collector_classes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ class ChefVisibility
@node_name = ''
@report = ''

def initialize(entity_uuid, run_id, node_name, report)
def initialize(entity_uuid, run_id, node_info, report)
@entity_uuid = entity_uuid
@run_id = run_id
@node_name = node_name
@node_name = node_info[:node]
@report = report
end

Expand Down
23 changes: 23 additions & 0 deletions libraries/compliance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,26 @@ def check_existence(config, path)
def upload_profile(config, owner, profile_name, path)
Compliance::API.upload(config, owner, profile_name, path)
end

# TODO: temporary, we should not use this
# TODO: harmonize with CLI login_refreshtoken method
def login_to_compliance(server, user, access_token, refresh_token)
if !refresh_token.nil?
success, msg, access_token = Compliance::API.get_token_via_refresh_token(server, refresh_token, true)
else
success = true
end

if success
config = Compliance::Configuration.new
config['user'] = user
config['server'] = server
config['token'] = access_token
config['insecure'] = true
config['version'] = Compliance::API.version(server, true)
config.store
else
Chef::Log.error msg
raise('Could not store authentication token')
end
end
32 changes: 26 additions & 6 deletions libraries/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,18 @@ def base_chef_server_url
# returns string of profile names separated with underscore
def extract_profile_names(profiles)
string = ''
profiles.each { |profile| string << profile["name"] << '_'}
profiles.each do |profile|
name = 'unknown'
name = profile['name'] unless profile['name'].nil?
string << name << '_'
end
string
end

# write to json file for interval calculations
def write_to_file(report, profiles)
names = extract_profile_names(profiles)
names << '-' << Time.now.utc.to_s.gsub(" ", "_") << '.json'
def write_to_file(report, profiles, interval_enabled, write_to_file)
names = extract_profile_names(profiles) << '.json' if interval_enabled
names = extract_profile_names(profiles) << '-' << Time.now.utc.to_s.tr(' ', '_') << '.json' if write_to_file
path = File.expand_path("../../#{names}", __FILE__)
json_file = File.new(path, 'w')
json_file.puts(report)
Expand All @@ -89,10 +93,26 @@ def write_to_file(report, profiles)
def profile_overdue_to_run?(interval, profiles)
# Calculate when a report was last created so we delay the next report if necessary
names = extract_profile_names(profiles)
filename = /#{names}.*json/
report_file = File.expand_path("../../#{filename}", __FILE__)
report_file = File.expand_path("../../#{names}.json", __FILE__)
return true unless ::File.exist?(report_file)
seconds_since_last_run = Time.now - ::File.mtime(report_file)
seconds_since_last_run > interval
end

def check_interval_settings(interval, interval_enabled, interval_time, profiles)
# handle intervals
interval_seconds = 0 # always run this by default, unless interval is defined
if !interval.nil? && interval_enabled
interval_seconds = interval_time * 60 # seconds in interval
Chef::Log.debug "Auditing this machine every #{interval_seconds} seconds "
end
# returns true if profile is overdue to run
profile_overdue_to_run?(interval_seconds, profiles)
end

# write_to_file and interval_enabled cannot both be set to true, for file naming purposes
def check_attributes(write_to_file, interval_enabled)
return false if write_to_file && interval_enabled
true
end
end
12 changes: 6 additions & 6 deletions spec/data/mock.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
class MockData
def self.node_info
"chef-client.solo"
end
def self.node_info
{ node: "chef-client.solo" }
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 <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\":\"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\":null,\"development\":null,\"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\":[]}],\"other_checks\":[],\"statistics\":{\"duration\":0.032332}}"
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 <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\":\"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\":null,\"development\":null,\"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\":[]}],\"other_checks\":[],\"statistics\":{\"duration\":0.032332}}"
end
end
24 changes: 24 additions & 0 deletions spec/data/mock_profile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# encoding: utf-8
# copyright: 2015, Chef Software, Inc.
# license: All rights reserved

title '/tmp profile'

# you add controls here
control "tmp-1.0" do # A unique ID for this control
impact 0.7 # The criticality, if this control fails.
title "Create /tmp directory" # A human-readable title
desc "An optional description..." # Describe why this is needed
tag data: "temp data" # A tag allows you to associate key information
tag "security" # to the test
ref "Document A-12", url: 'http://...' # Additional references

describe file('/tmp') do # The actual test
it { should be_directory }
end
end

# you can also use plain tests
describe file('/tmp') do
it { should be_directory }
end
6 changes: 3 additions & 3 deletions spec/unit/libraries/compliance_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@
url = 'https://192.168.33.201/api'
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}}
compliance_profiles = [{:owner=> 'admin', :profile_id=> 'ssh'}]
@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\"}}"
@chef_compliance = Collector::ChefCompliance.new(url, node_info, raise_if_unreachable, compliance_profiles)
@chef_compliance = Collector::ChefCompliance.new(url, node_info, raise_if_unreachable, compliance_profiles, @report)
end

it 'enriches the report correctly' do
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}}
expect(@chef_compliance.enriched_report(report)).to eq(@enriched_report_expected)
expect(@chef_compliance.enriched_report(@report)).to eq(@enriched_report_expected)
end
end
Loading

0 comments on commit d544e1e

Please sign in to comment.