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

Migrate to chef_handler from resource-based audit #71

Closed
wants to merge 19 commits into from
Closed
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
37 changes: 37 additions & 0 deletions .kitchen.vagrant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
driver:
name: vagrant
chef_version: 12.7.2

provisioner:
name: chef_zero

verifier:
name: inspec
sudo: false

platforms:
- name: centos-7.2

suites:
# - name: default # compliance reporting via chef-server
# run_list:
# - recipe[audit::default]
# attributes:
# audit:
# server: <%= ENV['CHEF_SERVER_URL'] %>
# profiles: &profiles
# base/ssh: true
# base/linux: true
- name: default # compliance direct reporting
run_list:
- recipe[audit::default]
attributes:
audit:
server: <%= ENV['COMPLIANCE_API'] %>
token: <%= ENV['COMPLIANCE_ACCESSTOKEN'] %>
owner: admin
profiles: &profiles
base/ssh: true
base/linux: true
# profiles: *profiles
2 changes: 1 addition & 1 deletion .kitchen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@ suites:
server: <%= ENV['COMPLIANCE_API'] %>
token: <%= ENV['COMPLIANCE_ACCESSTOKEN'] %>
owner: admin
profiles: *profiles
profiles: *profiles
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,5 @@ Style/SpaceAroundOperators:
Enabled: false
Style/IfUnlessModifier:
Enabled: false
Style/SignalException:
Enabled: false
21 changes: 7 additions & 14 deletions attributes/default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,23 @@
# limitations under the License.
#

# Compliance server if you want to report directly to it
default['audit']['server'] = nil
# Compliance token for accessing compliance, used only when reporting directly to compliance
default['audit']['token'] = nil
# Compliance refresh token, used when reporting directly to the compliance server
default['audit']['refresh_token'] = nil
# The owner of the scanned node when reporting directly to the compliance server
default['audit']['owner'] = nil
default['audit']['quiet'] = nil
# Whether to silence report results
default['audit']['quiet'] = false
# The profiles to scan
default['audit']['profiles'] = {}

# raise exception if Compliance API endpoint is unreachable
# while fetching profiles or posting report
default['audit']['raise_if_unreachable'] = true

# fail converge if downloaded profile is not present
default['audit']['fail_if_not_present'] = false

# fail converge after posting report if any audits have failed
default['audit']['fail_if_any_audits_failed'] = false

# inspec gem version to install(e.g. '0.22.1') or 'latest'
default['audit']['inspec_version'] = '0.22.1'

# by default run audit every time
default['audit']['interval']['enabled'] = false
# by default run compliance once a day
default['audit']['interval']['time'] = 1440

# quiet mode, on by default because this is testing, resources aren't converged in the normal chef sense
default['audit']['quiet'] = true
128 changes: 128 additions & 0 deletions files/default/audit_report.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# encoding: utf-8

require 'chef/handler'

class Chef
class Handler
# Creates a compliance audit report
class AuditReport < ::Chef::Handler
def report
return if audit_scheduler.should_skip_audit?

ensure_inspec_installed
report_results = initialize_report_results
# fetch first so dependencies can be used
compliance_profiles.each(&:fetch)

# add results of executed profiles to final report results
compliance_profiles.each do |profile|
report_results[:reports][profile.name] = profile.execute
end
report_results[:profile] = profile_owners_by_profile
server_connection.report report_results
audit_scheduler.record_completed_run
end

def audit_scheduler
@audit_scheduler ||= ::Audit::AuditScheduler.new(node['audit']['interval']['enabled'], node['audit']['interval']['time'])
end

def initialize_report_results
environment = node['environment']
environment = '_default' if environment.nil?
value = {
node: node['fqdn'],
os: {
release: node['platform_version'],
family: node['platform'],
},
environment: environment,
reports: {},
profile: {},
}
Chef::Log.debug "Initialized report results on node #{value['node']} and environment #{value['environment']}"
value
end

def inspec_version
node['audit']['inspec_version']
end

def ensure_inspec_installed
Chef::Log.debug "Ensuring inspec #{inspec_version} is installed"
require 'inspec'
# load the supermarket plugin
require 'bundles/inspec-supermarket/api'
require 'bundles/inspec-supermarket/target'

# load the compliance api plugin
require 'bundles/inspec-compliance/api'

if Inspec::VERSION != inspec_version && inspec_version != 'latest'
Chef::Log.warn "Wrong version of inspec (#{Inspec::VERSION}), please "\
'remove old versions (/opt/chef/embedded/bin/gem uninstall inspec).'
else
Chef::Log.warn "Using inspec version: (#{Inspec::VERSION})"
end
end

def compliance_profiles
@compliance_profiles ||= initialize_compliance_profiles
end

def profile_owners_by_profile
owners_by_profile = {}
compliance_profiles.each do |profile|
owners_by_profile[profile.name] = profile.owner
end
owners_by_profile
end

def initialize_compliance_profiles
profiles = []
node['audit']['profiles'].each do |owner_profile, value|
case value
when Hash
enabled = !value['disabled']
path = value['source']
else
enabled = value
end
fail "Invalid profile name '#{owner_profile}'. "\
"Must contain /, e.g. 'john/ssh'" if owner_profile !~ %r{\/}
owner, name = owner_profile.split('/').last(2)
platform_windows = node['platform'] == 'windows'
quiet = node['audit']['quiet']
profiles.push ::Audit::ComplianceProfile.new(owner, name, enabled, path, server_connection, platform_windows, quiet)
end
profiles
end

def server_connection
@server_connection ||= initialize_server_connection
end

def initialize_server_connection
token = node['audit']['token']
server = node['audit']['server']
unless token.nil?
Chef::Log.info "Connecting to compliance server #{server} with provided token"
org = node['audit']['owner']
org = parse_org(server) if org.nil?
::Audit::ComplianceServerConnection.new(server, org, token, node['audit']['refresh_token'])
else
server = Chef::Config[:chef_server_url] if server.nil?
org = parse_org(server)
base_server = server
base_server.slice!("/organizations/#{org}")
Chef::Log.info "Connecting to chef server #{server}"
::Audit::ChefServerConnection.new(base_server, org)
end
end

def parse_org(url)
url.split('/').last
end
end
end
end
52 changes: 52 additions & 0 deletions libraries/audit_scheduler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# encoding: utf-8
module Audit
class AuditScheduler
def initialize(interval_enabled, interval_time)
@interval_enabled = interval_enabled
@interval_time = interval_time
end

def should_skip_audit?
if should_run_on_schedule?
if scheduled_to_run?
Chef::Log.warn 'Running the compliance audit of this node since it is set to run '\
"every #{schedule_interval} seconds and last ran on #{last_run_time}."
else
Chef::Log.warn 'Skipping The compliance audit of this node since it is set to run '\
"every #{schedule_interval} seconds and is scheduled to run during a chef-client "\
"run after #{next_scheduled_time}"
return true
end
else
Chef::Log.warn 'Running the compliance audit of this node every time chef-client runs.'\
'To change this, configure it to run on an interval'
end
false
end

def record_completed_run
FileUtils.touch schedule_file
end

def should_run_on_schedule?
@interval_enabled
end

def schedule_interval
@interval_time
end

def scheduled_to_run?
schedule_file_modified = ::File.mtime(schedule_file)
seconds_since_last_run = Time.now - schedule_file_modified
scheduled = schedule_interval < seconds_since_last_run
Chef::Log.debug "Run scheduled is #{scheduled} where interval is set to #{schedule_interval} "\
"and it has been #{seconds_since_last_run} seconds since #{schedule_file} last ran "\
"at #{schedule_file_modified}"
end

def schedule_file
::File.join(Chef::Config[:file_cache_path], 'compliance', 'schedule')
end
end
end
49 changes: 49 additions & 0 deletions libraries/chef_server_connection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# encoding: utf-8

require 'uri'

module Audit
class ChefServerConnection
attr_accessor :server, :org
def initialize(server, org)
@server = server
@org = org
Chef::Log.debug "Creating chef server connection for server #{@server} and org #{@org}"
end

def fetch(profile)
reqpath ="organizations/#{org}/owners/#{profile.owner}/compliance/#{profile.name}/tar"
url = construct_url(server + '/compliance/', reqpath)
Chef::Log.info "Load profile from: #{url}"
Chef::Config[:verify_api_cert] = false # FIXME
Chef::Config[:ssl_verify_mode] = :verify_none # FIXME
rest = Chef::ServerAPI.new(url, Chef::Config)
tf = ::Audit::HttpProcessor.with_http_rescue do
Chef::Log.debug "Requesting profile from #{url}"
rest.binmode_streaming_request(url)
end
tf
end

def construct_url(server, path)
path.sub!(%r{^/}, '') # sanitize input
server = URI(server)
server.path = server.path + path if path
server
end

def report(report_results)
Chef::Config[:verify_api_cert] = false
Chef::Config[:ssl_verify_mode] = :verify_none

url = construct_url(server + '/compliance/', ::File.join('organizations', org, 'inspec'))
Chef::Log.info "Report to: #{url}"

rest = Chef::ServerAPI.new(url, Chef::Config)
::Audit::HttpProcessor.with_http_rescue do
Chef::Log.debug "Posting to #{url} results: #{report_results}"
rest.post(url, report_results)
end
end
end
end
Loading