Skip to content

Commit

Permalink
Merge pull request #124 from chef-cookbooks/vj/integrate-with-compliance
Browse files Browse the repository at this point in the history
Integrate with Chef Compliance
  • Loading branch information
chris-rock authored Oct 27, 2016
2 parents 3296f21 + 348145e commit 2d769f4
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@ suites:
profiles:
- name: linux
compliance: base/linux
- name: brewinc/ssh-hardening
supermarket: hardening/ssh-hardening
- name: ssh
compliance: base/ssh
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
86 changes: 72 additions & 14 deletions files/default/audit_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ class Handler
# Creates a compliance audit report
class AuditReport < ::Chef::Handler
def report
reporter = node['audit']['collector']
server = node['audit']['server']
user = node['audit']['owner']
token = node['audit']['token']
refresh_token = node['audit']['refresh_token']

load_needed_dependencies
call
send_report

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

call(reporter)
send_report(reporter, server, user)
end

def load_needed_dependencies
Expand All @@ -25,35 +35,83 @@ def load_needed_dependencies
require 'bundles/inspec-compliance/target'
end

def set_json_format
reporter = node['audit']['collector']
if reporter == 'chef-visibility'
format = 'json'
# 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
format = 'json-min'
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
format
end

def call
def call(reporter)
Chef::Log.debug 'Initialize InSpec'
format = set_json_format
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'] }
runner = ::Inspec::Runner.new(opts)

tests = tests_for_runner
tests.each { |target| runner.add_target(target, opts) }

Chef::Log.debug 'Running tests from: #{tests.inspect}'
Chef::Log.info "Running tests from: #{tests.inspect}"
runner.run
end

def send_report
reporter = node['audit']['collector']
Chef::Log.debug 'Reporting to #{reporter}'
def send_report(reporter, server, user)
Chef::Log.info "Reporting to #{reporter}"

# TODO: harmonize reporter interface
if reporter == 'chef-visibility'
Collector::ChefVisibility.new(entity_uuid, run_id, run_context.node.name).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.map { |profile| profile[:compliance] }.uniq
compliance_profiles = profiles.map { |profile|
owner, profile_id = profile.split('/')
{
owner: owner,
profile_id: profile_id,
}
}
Collector::ChefCompliance.new(url, run_context, raise_if_unreachable, compliance_profiles).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
else
Chef::Log.warn "#{reporter} is not a supported InSpec report collector"
end
end
end
Expand Down
117 changes: 110 additions & 7 deletions libraries/collector_classes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Collector
# Used to send inspec reports to Chef Visibility via the data_collector service
#
class ChefVisibility
include ReportHelpers

@entity_uuid = nil
@run_id = nil
@node_name = ''
Expand All @@ -24,13 +26,7 @@ def send_report
return false
end

# get file contents where inspec results were saved
result_path = File.expand_path('../../inspec_results.json', __FILE__)
file = File.open(result_path, 'rb')
content = file.read
file.close

# parse that string of contents into json
content = results
json_report = enriched_report(JSON.parse(content))

unless json_report
Expand Down Expand Up @@ -181,4 +177,111 @@ def control_status(results)
status
end
end

#
# Used to send inspec reports to a Chef Compliance server
#
class ChefCompliance
include ReportHelpers

@url = nil
@node_info = {}

# TODO: do not pass run_context in here, define a proper interface
def initialize(_url, run_context, raise_if_unreachable, compliance_profiles)
@run_context = run_context
@config = Compliance::Configuration.new
Chef::Log.warn "Report to Chef Compliance: #{@config['user']}"
Chef::Log.warn "#{@config['server']}/owners/#{@config['user']}/inspec"
@url = URI("#{@config['server']}/owners/#{@config['user']}/inspec")
@token = @config['token']
@raise_if_unreachable = raise_if_unreachable
@compliance_profiles = compliance_profiles
end

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

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

Chef::Log.info "Report to Chef Compliance: #{@url}"

# TODO: use insecure option
opts = { use_ssl: @url.scheme == 'https',
verify_mode: OpenSSL::SSL::VERIFY_NONE,
}
Net::HTTP.start(@url.host, @url.port, opts) do |http|
with_http_rescue do
http.request(req)
end
end
end

# TODO: add to docs that all profiles used in Chef Compliance, need to
# be uploaded to Chef Compliance first
def enriched_report(report)
blob = node_info

# extract profile names
profiles = report['controls'].collect { |control| control['profile_id'] }.uniq

# build report for chef compliance, it includes node data
blob[:reports] = {}
blob[:profiles] = {}
profiles.each { |profile|
namespace = @compliance_profiles.select { |entry| entry[:profile_id] == 'ssh' }
unless namespace.nil? && namespace.empty?
Chef::Log.debug "Namespace for #{profile} is #{namespace[0][:owner]}"
blob[:profiles][profile] = namespace[0][:owner]
blob[:reports][profile] = report.dup
# filter controls by profile_id
blob[:reports][profile]['controls'] = blob[:reports][profile]['controls'].select { |control| control['profile_id'] == profile }
else
Chef::Log.warn "Could not determine compliance namespace for #{profile}"
end
}

blob.to_json
end

def node_info
n = @run_context.node
{
node: n.name,
os: {
# arch: n['arch'],
release: n['platform_version'],
family: n['platform'],
},
environment: n.environment,
}
end
end

#
# Used to send inspec reports to a Chef Compliance server via Chef Server
#
class ChefServer
include ReportHelpers

@url = nil

def initialize(url)
@url = url
end

def send_report
content = results
Chef::Config[:verify_api_cert] = false
Chef::Config[:ssl_verify_mode] = :verify_none
Chef::Log.info "Report to Chef Server: #{@url}"
rest = Chef::ServerAPI.new(@url, Chef::Config)
with_http_rescue do
rest.post(@url, content)
end
end
end
end
48 changes: 48 additions & 0 deletions libraries/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,52 @@ def tests_for_runner
tests_for_runner = tests.map { |test| Hash[test.map { |k, v| [k.to_sym, v] }] }
tests_for_runner
end

def construct_url(server, path)
# sanitize inputs
server << '/' unless server =~ %r{/\z}
path.sub!(%r{^/}, '')
server = URI(server)
server.path = server.path + path if path
server
end

def with_http_rescue(*)
response = yield
if response.respond_to?(:code)
# handle non 200 error codes, they are not raised as Net::HTTPServerException
handle_http_error_code(response.code) if response.code.to_i >= 300
end
return response
rescue Net::HTTPServerException => e
Chef::Log.error e
handle_http_error_code(e.response.code)
end

def handle_http_error_code(code)
case code
when /401|403/
Chef::Log.error 'Auth issue: see audit cookbook TROUBLESHOOTING.md'
when /404/
Chef::Log.error 'Object does not exist on remote server.'
end
msg = "Received HTTP error #{code}"
Chef::Log.error msg
raise msg if @raise_if_unreachable
end

def base_chef_server_url
cs = URI(Chef::Config[:chef_server_url])
cs.path = ''
cs.to_s
end

# get file contents where inspec results were saved
def results
result_path = File.expand_path('../../inspec_results.json', __FILE__)
file = File.open(result_path, 'rb')
content = file.read
file.close
content
end
end
27 changes: 27 additions & 0 deletions libraries/matchers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# encoding: utf-8

# used by ChefSpec
if defined?(ChefSpec)

ChefSpec.define_matcher :compliance_profile

def create_compliance_token(resource_name)
ChefSpec::Matchers::ResourceMatcher.new(:compliance_token, :create, resource_name)
end

def fetch_compliance_profile(resource_name)
ChefSpec::Matchers::ResourceMatcher.new(:compliance_profile, :fetch, resource_name)
end

def upload_compliance_profile(resource_name)
ChefSpec::Matchers::ResourceMatcher.new(:compliance_profile, :upload, resource_name)
end

def execute_compliance_profile(resource_name)
ChefSpec::Matchers::ResourceMatcher.new(:compliance_profile, :execute, resource_name)
end

def execute_compliance_report(resource_name)
ChefSpec::Matchers::ResourceMatcher.new(:compliance_report, :execute, resource_name)
end
end

0 comments on commit 2d769f4

Please sign in to comment.