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

fix reporting files #134

Merged
merged 5 commits into from
Nov 1, 2016
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
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|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it would be perfect to iterate only over send_report(reporter, server, user, profiles), but we need json-min for Chef Compliance ... so it looks like we need to iterate over the scan as well for now

# 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

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