Skip to content

Commit

Permalink
Merge pull request #134 from chef-cookbooks/vj/fix-reporting-files
Browse files Browse the repository at this point in the history
fix reporting files
  • Loading branch information
chris-rock authored Nov 1, 2016
2 parents 0fae37c + ad07cfa commit 5628f4e
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 114 deletions.
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,44 @@ audit: {
}
```

## Write to file on disk

To write the report to a file on disk, simply set the collector to 'json-file' like so:

```ruby
audit: {
collector: 'json-file',
profiles: [
{
'name': 'admin/ssh2',
'path': '/some/base_ssh.tar.gz'
}
]
}
```

## Multiple reporters

To enable multiple reporters, simply define multiple reporters with all the necessary information
for each one. For example, to report to chef-compliance and write to json file on disk:

```ruby
"audit": {
"collector": [ "chef-compliance", "json-file" ]
"server": "https://compliance-fqdn/api",
"owner": "my-comp-org",
"refresh_token": "5/4T...g==",
"insecure": false,
"profiles": [
{
"name": "windows",
"compliance": "base/windows"
}
]
}
```


## Relationship with Chef Audit Mode

The following tables compares the [Chef Client audit mode](https://docs.chef.io/ctl_chef_client.html#run-in-audit-mode) with this `audit` cookbook.
Expand Down
6 changes: 1 addition & 5 deletions attributes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# inspec gem version to install(e.g. '1.1.0')
default['audit']['inspec_version'] = '1.2.0'

# collector possible values: chef-server, chef-compliance, chef-visibility
# collector possible values: chef-server, chef-compliance, chef-visibility, json-file
# chef-visibility requires inspec version 0.27.1 or above
default['audit']['collector'] = 'chef-server'

Expand Down Expand Up @@ -64,9 +64,5 @@
# 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

# inspec gem version to install(e.g. '1.1.0')
default['audit']['inspec_version'] = '1.2.0'
116 changes: 55 additions & 61 deletions files/default/audit_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,44 @@ class Handler
# Creates a compliance audit report
class AuditReport < ::Chef::Handler
def report
reporter = node['audit']['collector']
# ensure reporters is array
reporters = handle_reporters(node['audit']['collector'])

# collect attribute values
server = node['audit']['server']
user = node['audit']['owner']
token = node['audit']['token']
refresh_token = node['audit']['refresh_token']
interval = node['audit']['interval']
interval_enabled = node['audit']['interval']['enabled']
interval_time = node['audit']['interval']['time']
report_file = node['audit']['output']
profiles = node['audit']['profiles']
quiet = node['audit']['quiet']

# create a file with a timestamp to
create_timestamp_file if interval_enabled

# load inspec, supermarket bundle and compliance bundle
load_needed_dependencies

# ensure authentication for Chef Compliance is in place
login_to_compliance(server, user, token, refresh_token) if reporter == 'chef-compliance'
# 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'

if check_interval_settings(interval, interval_enabled, interval_time, report_file)
call(reporter, profiles)
send_report(reporter, server, user, profiles)
else
Chef::Log.error 'Please take a look at your interval settings'
# true if profile is due to run (see libraries/helper.rb)
if check_interval_settings(interval, interval_enabled, interval_time)
# 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)

# 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
end
end

Expand All @@ -44,55 +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, report_file)
# 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, report_file)
# 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}"
{ 'report' => true, 'format' => format, 'output' => output }
end

def call(reporter, profiles)
# run profiles and return report
def call(opts, profiles)
Chef::Log.debug 'Initialize InSpec'
format = reporter == 'chef-visibility' ? 'json' : 'json-min'
Chef::Log.warn "Format is #{format}"
# TODO: for now we need to store the report to a file we expect that to
# get from the runner
Chef::Log.warn "*********** Directory is #{node['audit']['output']}"
opts = { 'format' => format, 'output' => node['audit']['output'] }
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
runner.report.to_json
end

# extracts relevant node data
Expand All @@ -109,41 +98,46 @@ def gather_nodeinfo
}
end

def send_report(reporter, server, user, profiles)
# 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]).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']
url = construct_url(server, File.join('/owners', user, 'inspec'))
if server
# TODO: we should not send the profiles to the reporter, all the information
# should be available in inspec reports out-of-the-box
# TODO: Chef Compliance can only handle reports for profiles it knows
profiles = tests_for_runner(profiles).map { |profile| profile[:compliance] }.uniq
# TODO: raise warning when not a compliance-known profile
profiles = tests_for_runner(profiles).map { |profile| profile[:compliance] if profile[:compliance] }.uniq.compact
compliance_profiles = profiles.map { |profile|
owner, profile_id = profile.split('/')
{
owner: owner,
profile_id: profile_id,
}
}
Collector::ChefCompliance.new(url, gather_nodeinfo, raise_if_unreachable, compliance_profiles).send_report
Collector::ChefCompliance.new(url, gather_nodeinfo, raise_if_unreachable, compliance_profiles, report).send_report
else
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

elsif reporter == 'json-file'
timestamp = Time.now.utc.to_s.tr(' ', '_')
Collector::JsonFile.new(report, timestamp).send_report
else
Chef::Log.warn "#{reporter} is not a supported InSpec report collector"
end
Expand Down
45 changes: 38 additions & 7 deletions libraries/collector_classes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ class ChefVisibility
@entity_uuid = nil
@run_id = nil
@node_name = ''
@report = ''

def initialize(entity_uuid, run_id, node_name)
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

# Method used in order to send the inspec report to the data_collector server
Expand All @@ -26,7 +28,7 @@ def send_report
return false
end

content = results
content = @report
json_report = enriched_report(JSON.parse(content))

unless json_report
Expand All @@ -47,7 +49,7 @@ def send_report
end

begin
Chef::Log.info "Report to Chef Visibility: #{dc[:server_url]}"
Chef::Log.warn "Report to Chef Visibility: #{dc[:server_url]}"
Chef::Log.debug("POSTing the following message to #{dc[:server_url]}: #{json_report}")
http = Chef::HTTP.new(dc[:server_url])
http.post(nil, json_report, headers)
Expand Down Expand Up @@ -186,9 +188,9 @@ class ChefCompliance

@url = nil
@node_info = {}
@report = ''

# TODO: do not pass run_context in here, define a proper interface
def initialize(_url, node_info, raise_if_unreachable, compliance_profiles)
def initialize(_url, node_info, raise_if_unreachable, compliance_profiles, report)
@node_info = node_info
@config = Compliance::Configuration.new
Chef::Log.warn "Report to Chef Compliance: #{@config['user']}"
Expand All @@ -197,13 +199,14 @@ def initialize(_url, node_info, raise_if_unreachable, compliance_profiles)
@token = @config['token']
@raise_if_unreachable = raise_if_unreachable
@compliance_profiles = compliance_profiles
@report = report
end

def send_report
Chef::Log.info "Report to Chef Compliance: #{@token}"
req = Net::HTTP::Post.new(@url, { 'Authorization' => "Bearer #{@token}" })

content = results
content = @report
json_report = enriched_report(JSON.parse(content))
req.body = json_report

Expand Down Expand Up @@ -271,4 +274,32 @@ def send_report
end
end
end

#
# Used to write report to file on disk
#
class JsonFile
include ReportHelpers

@report = ''

def initialize(report, timestamp)
@report = report
@timestamp = timestamp
end

def send_report
Chef::Log.warn 'Writing report to file.'
write_to_file(@report, @timestamp)
end

def write_to_file(report, timestamp)
filename = 'inspec' << '-' << timestamp << '.json'
path = File.expand_path("../../#{filename}", __FILE__)
Chef::Log.warn "Filename is #{path}"
json_file = File.new(path, 'w')
json_file.puts(report)
json_file.close
end
end
end
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
Loading

0 comments on commit 5628f4e

Please sign in to comment.