Move compliance to v2 plugin (#3423)

* Move compliance pluging to v2 system.
* Update kitchen-inspec to test.
* Add legacy require patsh.
* Fix unit test

Signed-off-by: Jared Quick <jquick@chef.io>
This commit is contained in:
Jared Quick 2018-10-04 14:31:39 -04:00 committed by GitHub
parent 92249898d9
commit cb12ada2fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1430 additions and 1483 deletions

View file

@ -58,7 +58,7 @@ Rake::TestTask.new do |t|
'test/unit/**/*_test.rb', 'test/unit/**/*_test.rb',
'lib/plugins/inspec-*/test/unit/**/*_test.rb', 'lib/plugins/inspec-*/test/unit/**/*_test.rb',
]) ])
t.warning = true t.warning = false
t.verbose = true t.verbose = true
t.ruby_opts = ['--dev'] if defined?(JRUBY_VERSION) t.ruby_opts = ['--dev'] if defined?(JRUBY_VERSION)
end end
@ -91,7 +91,6 @@ namespace :test do
'test/functional/inspec_exec_json_test.rb', 'test/functional/inspec_exec_json_test.rb',
'test/functional/inspec_detect_test.rb', 'test/functional/inspec_detect_test.rb',
'test/functional/inspec_vendor_test.rb', 'test/functional/inspec_vendor_test.rb',
'test/functional/inspec_compliance_test.rb',
'test/functional/inspec_check_test.rb', 'test/functional/inspec_check_test.rb',
'test/functional/filter_table_test.rb', 'test/functional/filter_table_test.rb',
] ]

View file

@ -1,16 +0,0 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
libdir = File.dirname(__FILE__)
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
module Compliance
autoload :Configuration, 'inspec-compliance/configuration'
autoload :HTTP, 'inspec-compliance/http'
autoload :Support, 'inspec-compliance/support'
autoload :API, 'inspec-compliance/api'
end
require 'inspec-compliance/cli'
require 'inspec-compliance/target'

View file

@ -1,20 +0,0 @@
---
driver:
name: vagrant
synced_folders:
- ['../../../', '/inspec']
network:
- ['private_network', {ip: '192.168.251.2'}]
provisioner:
name: shell
verifier:
name: inspec
platforms:
- name: ubuntu-14.04
suites:
- name: default
run_list:
attributes:

356
lib/bundles/inspec-compliance/api.rb Executable file → Normal file
View file

@ -1,354 +1,4 @@
# encoding: utf-8 # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
# author: Christoph Hartmann # TODO: Remove in inspec 4.0
# author: Dominik Richter
require 'net/http' require 'plugins/inspec-compliance/lib/inspec-compliance/api'
require 'uri'
require 'json'
require_relative 'api/login'
module Compliance
class ServerConfigurationMissing < StandardError; end
# API Implementation does not hold any state by itself,
# everything will be stored in local Configuration store
class API
extend Compliance::API::Login
# return all compliance profiles available for the user
# the user is either specified in the options hash or by default
# the username of the account is used that is logged in
def self.profiles(config, profile_filter = nil) # rubocop:disable PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
owner = config['owner'] || config['user']
# Chef Compliance
if is_compliance_server?(config)
url = "#{config['server']}/user/compliance"
# Chef Automate2
elsif is_automate2_server?(config)
url = "#{config['server']}/compliance/profiles/search"
# Chef Automate
elsif is_automate_server?(config)
url = "#{config['server']}/profiles/#{owner}"
else
raise ServerConfigurationMissing
end
headers = get_headers(config)
if profile_filter
_owner, id, ver = profile_split(profile_filter)
else
id, ver = nil
end
if is_automate2_server?(config)
body = { owner: owner, name: id }.to_json
response = Compliance::HTTP.post_with_headers(url, headers, body, config['insecure'])
else
response = Compliance::HTTP.get(url, headers, config['insecure'])
end
data = response.body
response_code = response.code
case response_code
when '200'
msg = 'success'
profiles = JSON.parse(data)
# iterate over profiles
if is_compliance_server?(config)
mapped_profiles = []
profiles.values.each { |org|
mapped_profiles += org.values
}
# Chef Automate pre 0.8.0
elsif is_automate_server_pre_080?(config)
mapped_profiles = profiles.values.flatten
elsif is_automate2_server?(config)
mapped_profiles = []
profiles['profiles'].each { |p|
mapped_profiles << p
}
else
mapped_profiles = profiles.map { |e|
e['owner_id'] = owner
e
}
end
# filter by name and version if they were specified in profile_filter
mapped_profiles.select! do |p|
(!ver || p['version'] == ver) && (!id || p['name'] == id)
end
return msg, mapped_profiles
when '401'
msg = '401 Unauthorized. Please check your token.'
return msg, []
else
msg = "An unexpected error occurred (HTTP #{response_code}): #{response.message}"
return msg, []
end
end
# return the server api version
# NB this method does not use Compliance::Configuration to allow for using
# it before we know the version (e.g. oidc or not)
def self.version(config)
url = config['server']
insecure = config['insecure']
raise ServerConfigurationMissing if url.nil?
headers = get_headers(config)
response = Compliance::HTTP.get(url+'/version', headers, insecure)
return {} if response.code == '404'
data = response.body
return {} if data.nil? || data.empty?
parsed = JSON.parse(data)
return {} unless parsed.key?('version') && !parsed['version'].empty?
parsed
end
# verifies that a profile exists
def self.exist?(config, profile)
_msg, profiles = Compliance::API.profiles(config, profile)
!profiles.empty?
end
def self.upload(config, owner, profile_name, archive_path)
# Chef Compliance
if is_compliance_server?(config)
url = "#{config['server']}/owners/#{owner}/compliance/#{profile_name}/tar"
# Chef Automate pre 0.8.0
elsif is_automate_server_pre_080?(config)
url = "#{config['server']}/#{owner}"
elsif is_automate2_server?(config)
url = "#{config['server']}/compliance/profiles?owner=#{owner}"
# Chef Automate
else
url = "#{config['server']}/profiles/#{owner}"
end
headers = get_headers(config)
if is_automate2_server?(config)
res = Compliance::HTTP.post_multipart_file(url, headers, archive_path, config['insecure'])
else
res = Compliance::HTTP.post_file(url, headers, archive_path, config['insecure'])
end
[res.is_a?(Net::HTTPSuccess), res.body]
end
# Use username and refresh_token to get an API access token
def self.get_token_via_refresh_token(url, refresh_token, insecure)
uri = URI.parse("#{url}/login")
req = Net::HTTP::Post.new(uri.path)
req.body = { token: refresh_token }.to_json
access_token = nil
response = Compliance::HTTP.send_request(uri, req, insecure)
data = response.body
if response.code == '200'
begin
tokendata = JSON.parse(data)
access_token = tokendata['access_token']
msg = 'Successfully fetched API access token'
success = true
rescue JSON::ParserError => e
success = false
msg = e.message
end
else
success = false
msg = "Failed to authenticate to #{url} \n\
Response code: #{response.code}\n Body: #{response.body}"
end
[success, msg, access_token]
end
# Use username and password to get an API access token
def self.get_token_via_password(url, username, password, insecure)
uri = URI.parse("#{url}/login")
req = Net::HTTP::Post.new(uri.path)
req.body = { userid: username, password: password }.to_json
access_token = nil
response = Compliance::HTTP.send_request(uri, req, insecure)
data = response.body
if response.code == '200'
access_token = data
msg = 'Successfully fetched an API access token valid for 12 hours'
success = true
else
success = false
msg = "Failed to authenticate to #{url} \n\
Response code: #{response.code}\n Body: #{response.body}"
end
[success, msg, access_token]
end
def self.get_headers(config)
token = get_token(config)
if is_automate_server?(config) || is_automate2_server?(config)
headers = { 'chef-delivery-enterprise' => config['automate']['ent'] }
if config['automate']['token_type'] == 'dctoken'
headers['x-data-collector-token'] = token
else
headers['chef-delivery-user'] = config['user']
headers['chef-delivery-token'] = token
end
else
headers = { 'Authorization' => "Bearer #{token}" }
end
headers
end
def self.get_token(config)
return config['token'] unless config['refresh_token']
_success, _msg, token = get_token_via_refresh_token(config['server'], config['refresh_token'], config['insecure'])
token
end
def self.target_url(config, profile)
owner, id, ver = profile_split(profile)
return "#{config['server']}/compliance/profiles/tar" if is_automate2_server?(config)
return "#{config['server']}/owners/#{owner}/compliance/#{id}/tar" unless is_automate_server?(config)
if ver.nil?
"#{config['server']}/profiles/#{owner}/#{id}/tar"
else
"#{config['server']}/profiles/#{owner}/#{id}/version/#{ver}/tar"
end
end
def self.profile_split(profile)
owner, id = profile.split('/')
id, version = id.split('#')
[owner, id, version]
end
# returns a parsed url for `admin/profile` or `compliance://admin/profile`
def self.sanitize_profile_name(profile)
if URI(profile).scheme == 'compliance'
uri = URI(profile)
else
uri = URI("compliance://#{profile}")
end
uri.to_s.sub(%r{^compliance:\/\/}, '')
end
def self.is_compliance_server?(config)
config['server_type'] == 'compliance'
end
def self.is_automate_server_pre_080?(config)
# Automate versions before 0.8.x do not have a valid version in the config
return false unless config['server_type'] == 'automate'
server_version_from_config(config).nil?
end
def self.is_automate_server_080_and_later?(config)
# Automate versions 0.8.x and later will have a "version" key in the config
# that is properly parsed out via server_version_from_config below
return false unless config['server_type'] == 'automate'
!server_version_from_config(config).nil?
end
def self.is_automate2_server?(config)
config['server_type'] == 'automate2'
end
def self.is_automate_server?(config)
config['server_type'] == 'automate'
end
def self.server_version_from_config(config)
# Automate versions 0.8.x and later will have a "version" key in the config
# that looks like: "version":{"api":"compliance","version":"0.8.24"}
return nil unless config.key?('version')
return nil unless config['version'].is_a?(Hash)
config['version']['version']
end
def self.determine_server_type(url, insecure)
if target_is_automate2_server?(url, insecure)
:automate2
elsif target_is_automate_server?(url, insecure)
:automate
elsif target_is_compliance_server?(url, insecure)
:compliance
else
Inspec::Log.debug('Could not determine server type using known endpoints')
nil
end
end
def self.target_is_automate2_server?(url, insecure)
automate_endpoint = '/dex/auth'
response = Compliance::HTTP.get(url + automate_endpoint, nil, insecure)
if response.code == '400'
Inspec::Log.debug(
"Received 400 from #{url}#{automate_endpoint} - " \
'assuming target is a Chef Automate2 instance',
)
true
else
false
end
end
def self.target_is_automate_server?(url, insecure)
automate_endpoint = '/compliance/version'
response = Compliance::HTTP.get(url + automate_endpoint, nil, insecure)
case response.code
when '401'
Inspec::Log.debug(
"Received 401 from #{url}#{automate_endpoint} - " \
'assuming target is a Chef Automate instance',
)
true
when '200'
# Chef Automate currently returns 401 for `/compliance/version` but some
# versions of OpsWorks Chef Automate return 200 and a Chef Manage page
# when unauthenticated requests are received.
if response.body.include?('Are You Looking For the Chef Server?')
Inspec::Log.debug(
"Received 200 from #{url}#{automate_endpoint} - " \
'assuming target is an OpsWorks Chef Automate instance',
)
true
else
Inspec::Log.debug(
"Received 200 from #{url}#{automate_endpoint} " \
'but did not receive the Chef Manage page - ' \
'assuming target is not a Chef Automate instance',
)
false
end
else
Inspec::Log.debug(
"Received unexpected status code #{response.code} " \
"from #{url}#{automate_endpoint} - " \
'assuming target is not a Chef Automate instance',
)
false
end
end
def self.target_is_compliance_server?(url, insecure)
# All versions of Chef Compliance return 200 for `/api/version`
compliance_endpoint = '/api/version'
response = Compliance::HTTP.get(url + compliance_endpoint, nil, insecure)
return false unless response.code == '200'
Inspec::Log.debug(
"Received 200 from #{url}#{compliance_endpoint} - " \
'assuming target is a Compliance server',
)
true
end
end
end

View file

@ -1,193 +0,0 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Ricter
# author: Jerry Aldrich
module Compliance
class API
module Login
class CannotDetermineServerType < StandardError; end
def login(options)
raise ArgumentError, 'Please specify a server using `inspec compliance login https://SERVER`' unless options['server']
options['server'] = URI("https://#{options['server']}").to_s if URI(options['server']).scheme.nil?
options['server_type'] = Compliance::API.determine_server_type(options['server'], options['insecure'])
case options['server_type']
when :automate2
Login::Automate2Server.login(options)
when :automate
Login::AutomateServer.login(options)
when :compliance
Login::ComplianceServer.login(options)
else
raise CannotDetermineServerType, "Unable to determine if #{options['server']} is a Chef Automate or Chef Compliance server"
end
end
module Automate2Server
def self.login(options)
verify_thor_options(options)
options['url'] = options['server'] + '/api/v0'
token = options['dctoken'] || options['token']
store_access_token(options, token)
end
def self.store_access_token(options, token)
config = Compliance::Configuration.new
config.clean
config['automate'] = {}
config['automate']['ent'] = 'automate'
config['automate']['token_type'] = 'dctoken'
config['server'] = options['url']
config['user'] = options['user']
config['owner'] = options['user']
config['insecure'] = options['insecure'] || false
config['server_type'] = options['server_type'].to_s
config['token'] = token
config['version'] = '0'
config.store
config
end
def self.verify_thor_options(o)
error_msg = []
error_msg.push('Please specify a user using `--user=\'USER\'`') if o['user'].nil?
if o['token'].nil? && o['dctoken'].nil?
error_msg.push('Please specify a token using `--token=\'APITOKEN\'`')
end
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
end
end
module AutomateServer
def self.login(options)
verify_thor_options(options)
options['url'] = options['server'] + '/compliance'
token = options['dctoken'] || options['token']
store_access_token(options, token)
end
def self.store_access_token(options, token)
token_type = if options['token']
'usertoken'
else
'dctoken'
end
config = Compliance::Configuration.new
config.clean
config['automate'] = {}
config['automate']['ent'] = options['ent']
config['automate']['token_type'] = token_type
config['server'] = options['url']
config['user'] = options['user']
config['insecure'] = options['insecure'] || false
config['server_type'] = options['server_type'].to_s
config['token'] = token
config['version'] = Compliance::API.version(config)
config.store
config
end
# Automate login requires `--ent`, `--user`, and either `--token` or `--dctoken`
def self.verify_thor_options(o)
error_msg = []
error_msg.push('Please specify a user using `--user=\'USER\'`') if o['user'].nil?
error_msg.push('Please specify an enterprise using `--ent=\'automate\'`') if o['ent'].nil?
if o['token'].nil? && o['dctoken'].nil?
error_msg.push('Please specify a token using `--token=\'AUTOMATE_TOKEN\'` or `--dctoken=\'DATA_COLLECTOR_TOKEN\'`')
end
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
end
end
module ComplianceServer
def self.login(options)
compliance_verify_thor_options(options)
options['url'] = options['server'] + '/api'
if options['user'] && options['token']
compliance_store_access_token(options, options['token'])
elsif options['user'] && options['password']
compliance_login_user_pass(options)
elsif options['refresh_token']
compliance_login_refresh_token(options)
end
end
def self.compliance_login_user_pass(options)
success, msg, token = Compliance::API.get_token_via_password(
options['url'],
options['user'],
options['password'],
options['insecure'],
)
raise msg unless success
compliance_store_access_token(options, token)
end
def self.compliance_login_refresh_token(options)
success, msg, token = Compliance::API.get_token_via_refresh_token(
options['url'],
options['refresh_token'],
options['insecure'],
)
raise msg unless success
compliance_store_access_token(options, token)
end
def self.compliance_store_access_token(options, token)
config = Compliance::Configuration.new
config.clean
config['user'] = options['user'] if options['user']
config['server'] = options['url']
config['insecure'] = options['insecure'] || false
config['server_type'] = options['server_type'].to_s
config['token'] = token
config['version'] = Compliance::API.version(config)
config.store
config
end
# Compliance login requires `--user` or `--refresh_token`
# If `--user` then either `--password`, `--token`, or `--refresh-token`, is required
def self.compliance_verify_thor_options(o)
error_msg = []
error_msg.push('Please specify a server using `inspec compliance login https://SERVER`') if o['server'].nil?
if o['user'].nil? && o['refresh_token'].nil?
error_msg.push('Please specify a `--user=\'USER\'` or a `--refresh-token=\'TOKEN\'`')
end
if o['user'] && o['password'].nil? && o['token'].nil? && o['refresh_token'].nil?
error_msg.push('Please specify either a `--password`, `--token`, or `--refresh-token`')
end
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
end
end
end
end
end

View file

@ -1,41 +0,0 @@
#!/bin/bash
echo "Installing Chef Compliance $deb"
# select latest package from cache directory
# deb=$(find /inspec/.cache -name '*.deb' | tail -1)
# sudo dpkg -i $deb
# use chef compliance package repository
sudo apt-get install -y apt-transport-https
sudo apt-get install wget
wget -qO - https://downloads.chef.io/packages-chef-io-public.key | sudo apt-key add -
CHANNEL=${CHANNEL:-stable}
DISTRIBUTION=$(lsb_release --codename | cut -f2)
echo "found $DISTRIBUTION"
echo "use $CHANNEL channel"
echo "deb https://packages.chef.io/$CHANNEL-apt $DISTRIBUTION main" > /etc/apt/sources.list.d/chef-$CHANNEL.list
sudo apt-get update
sudo apt-get install chef-compliance
sudo chef-compliance-ctl reconfigure --accept-license
sudo chef-compliance-ctl restart
# finalize setup
cd /
/opt/chef-compliance/embedded/service/core/bin/core setup --endpoint "http://127.0.0.1:10500/setup" --login "admin" --password "admin" --name "John Doe" --accept-eula
# wget --no-check-certificate http://127.0.0.1/api/version
# cat version
# install ruby 2.3
sudo apt-get install -y software-properties-common
sudo apt-add-repository -y ppa:brightbox/ruby-ng
sudo apt-get update
sudo apt-get install -y ruby2.3 ruby2.3-dev
ruby2.3 -v
# prepare the usage of bundler
sudo gem install bundler
cd /inspec
bundle install
BUNDLE_GEMFILE=/inspec/Gemfile bundle exec inspec version

View file

@ -1,276 +0,0 @@
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
require 'thor'
require 'erb'
require 'inspec/base_cli'
module Compliance
class ComplianceCLI < Inspec::BaseCLI
namespace 'compliance'
# TODO: find another solution, once https://github.com/erikhuda/thor/issues/261 is fixed
def self.banner(command, _namespace = nil, _subcommand = false)
"#{basename} #{subcommand_prefix} #{command.usage}"
end
def self.subcommand_prefix
namespace
end
desc "login https://SERVER --insecure --user='USER' --ent='ENTERPRISE' --token='TOKEN'", 'Log in to a Chef Compliance/Chef Automate SERVER'
long_desc <<-LONGDESC
`login` allows you to use InSpec with Chef Automate or a Chef Compliance Server
You need to a token for communication. More information about token retrieval
is available at:
https://docs.chef.io/api_automate.html#authentication-methods
https://docs.chef.io/api_compliance.html#obtaining-an-api-token
LONGDESC
option :insecure, aliases: :k, type: :boolean,
desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
option :user, type: :string, required: false,
desc: 'Username'
option :password, type: :string, required: false,
desc: 'Password (Chef Compliance Only)'
option :token, type: :string, required: false,
desc: 'Access token'
option :refresh_token, type: :string, required: false,
desc: 'Chef Compliance refresh token (Chef Compliance Only)'
option :dctoken, type: :string, required: false,
desc: 'Data Collector token (Chef Automate Only)'
option :ent, type: :string, required: false,
desc: 'Enterprise for Chef Automate reporting (Chef Automate Only)'
def login(server)
options['server'] = server
Compliance::API.login(options)
config = Compliance::Configuration.new
puts "Stored configuration for Chef #{config['server_type'].capitalize}: #{config['server']}' with user: '#{config['user']}'"
end
desc 'profiles', 'list all available profiles in Chef Compliance'
option :owner, type: :string, required: false,
desc: 'owner whose profiles to list'
def profiles
config = Compliance::Configuration.new
return if !loggedin(config)
# set owner to config
config['owner'] = options['owner'] || config['user']
msg, profiles = Compliance::API.profiles(config)
profiles.sort_by! { |hsh| hsh['title'] }
if !profiles.empty?
# iterate over profiles
headline('Available profiles:')
profiles.each { |profile|
owner = profile['owner_id'] || profile['owner']
li("#{profile['title']} v#{profile['version']} (#{mark_text(owner + '/' + profile['name'])})")
}
else
puts msg if msg != 'success'
puts 'Could not find any profiles'
exit 1
end
rescue Compliance::ServerConfigurationMissing
STDERR.puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
exit 1
end
desc 'exec PROFILE', 'executes a Chef Compliance profile'
exec_options
def exec(*tests)
config = Compliance::Configuration.new
return if !loggedin(config)
o = opts(:exec).dup
diagnose(o)
configure_logger(o)
# iterate over tests and add compliance scheme
tests = tests.map { |t| 'compliance://' + Compliance::API.sanitize_profile_name(t) }
runner = Inspec::Runner.new(o)
tests.each { |target| runner.add_target(target) }
exit runner.run
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
exit 1
end
desc 'download PROFILE', 'downloads a profile from Chef Compliance'
option :name, type: :string,
desc: 'Name of the archive filename (file type will be added)'
def download(profile_name)
o = options.dup
configure_logger(o)
config = Compliance::Configuration.new
return if !loggedin(config)
profile_name = Compliance::API.sanitize_profile_name(profile_name)
if Compliance::API.exist?(config, profile_name)
puts "Downloading `#{profile_name}`"
fetcher = Compliance::Fetcher.resolve(
{
compliance: profile_name,
},
)
# we provide a name, the fetcher adds the extension
_owner, id = profile_name.split('/')
file_name = fetcher.fetch(o.name || id)
puts "Profile stored to #{file_name}"
else
puts "Profile #{profile_name} is not available in Chef Compliance."
exit 1
end
end
desc 'upload PATH', 'uploads a local profile to Chef Compliance'
option :overwrite, type: :boolean, default: false,
desc: 'Overwrite existing profile on Server.'
option :owner, type: :string, required: false,
desc: 'Owner that should own the profile'
def upload(path) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, PerceivedComplexity, Metrics/CyclomaticComplexity
config = Compliance::Configuration.new
return if !loggedin(config)
# set owner to config
config['owner'] = options['owner'] || config['user']
unless File.exist?(path)
puts "Directory #{path} does not exist."
exit 1
end
vendor_deps(path, options) if File.directory?(path)
o = options.dup
configure_logger(o)
# only run against the mock backend, otherwise we run against the local system
o[:backend] = Inspec::Backend.create(target: 'mock://')
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
# check the profile, we only allow to upload valid profiles
profile = Inspec::Profile.for_target(path, o)
# start verification process
error_count = 0
error = lambda { |msg|
error_count += 1
puts msg
}
result = profile.check
unless result[:summary][:valid]
error.call('Profile check failed. Please fix the profile before upload.')
else
puts('Profile is valid')
end
# determine user information
if (config['token'].nil? && config['refresh_token'].nil?) || config['user'].nil?
error.call('Please login via `inspec compliance login`')
end
# read profile name from inspec.yml
profile_name = profile.params[:name]
# read profile version from inspec.yml
profile_version = profile.params[:version]
# check that the profile is not uploaded already,
# confirm upload to the user (overwrite with --force)
if Compliance::API.exist?(config, "#{config['owner']}/#{profile_name}##{profile_version}") && !options['overwrite']
error.call('Profile exists on the server, use --overwrite')
end
# abort if we found an error
if error_count > 0
puts "Found #{error_count} error(s)"
exit 1
end
# if it is a directory, tar it to tmp directory
generated = false
if File.directory?(path)
generated = true
archive_path = Dir::Tmpname.create([profile_name, '.tar.gz']) {}
puts "Generate temporary profile archive at #{archive_path}"
profile.archive({ output: archive_path, ignore_errors: false, overwrite: true })
else
archive_path = path
end
puts "Start upload to #{config['owner']}/#{profile_name}"
pname = ERB::Util.url_encode(profile_name)
if Compliance::API.is_automate_server?(config) || Compliance::API.is_automate2_server?(config)
puts 'Uploading to Chef Automate'
else
puts 'Uploading to Chef Compliance'
end
success, msg = Compliance::API.upload(config, config['owner'], pname, archive_path)
# delete temp file if it was temporary generated
File.delete(archive_path) if generated && File.exist?(archive_path)
if success
puts 'Successfully uploaded profile'
else
puts 'Error during profile upload:'
puts msg
exit 1
end
end
desc 'version', 'displays the version of the Chef Compliance server'
def version
config = Compliance::Configuration.new
info = Compliance::API.version(config)
if !info.nil? && info['version']
puts "Name: #{info['api']}"
puts "Version: #{info['version']}"
else
puts 'Could not determine server version.'
exit 1
end
rescue Compliance::ServerConfigurationMissing
puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
exit 1
end
desc 'logout', 'user logout from Chef Compliance'
def logout
config = Compliance::Configuration.new
unless config.supported?(:oidc) || config['token'].nil? || config['server_type'] == 'automate'
config = Compliance::Configuration.new
url = "#{config['server']}/logout"
Compliance::HTTP.post(url, config['token'], config['insecure'], !config.supported?(:oidc))
end
success = config.destroy
if success
puts 'Successfully logged out'
else
puts 'Could not log out'
end
end
private
def loggedin(config)
serverknown = !config['server'].nil?
puts 'You need to login first with `inspec compliance login`' if !serverknown
serverknown
end
end
# register the subcommand to Inspec CLI registry
Inspec::Plugins::CLI.add_subcommand(ComplianceCLI, 'compliance', 'compliance SUBCOMMAND ...', 'Chef Compliance commands', {})
end

View file

@ -1,103 +1,4 @@
# encoding: utf-8 # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
# author: Christoph Hartmann # TODO: Remove in inspec 4.0
# author: Dominik Richter
module Compliance require 'plugins/inspec-compliance/lib/inspec-compliance/configuration'
# stores configuration on local filesystem
class Configuration
def initialize
@config_path = File.join(Dir.home, '.inspec', 'compliance')
# ensure the directory is available
unless File.directory?(@config_path)
FileUtils.mkdir_p(@config_path)
end
# set config file path
@config_file = File.join(@config_path, '/config.json')
@config = {}
# load the data
get
end
# direct access to config
def [](key)
@config[key]
end
def []=(key, value)
@config[key] = value
end
def key?(key)
@config.key?(key)
end
def clean
@config = {}
end
# return the json data
def get
if File.exist?(@config_file)
file = File.read(@config_file)
@config = JSON.parse(file)
end
@config
end
# stores a hash to json
def store
File.open(@config_file, 'w') do |f|
f.chmod(0600)
f.write(@config.to_json)
end
end
# deletes data
def destroy
if File.exist?(@config_file)
File.delete(@config_file)
else
true
end
end
# return if the (stored) api version does not support a certain feature
def supported?(feature)
sup = version_with_support(feature)
# we do not know the version, therefore we do not know if its possible to use the feature
return if self['version'].nil? || self['version']['version'].nil?
if sup.is_a?(Array)
Gem::Version.new(self['version']['version']) >= sup[0] &&
Gem::Version.new(self['version']['version']) < sup[1]
else
Gem::Version.new(self['version']['version']) >= sup
end
end
# exit 1 if the version of compliance that we're working with doesn't support odic
def legacy_check!(feature)
return if supported?(feature)
puts "This feature (#{feature}) is not available for legacy installations."
puts 'Please upgrade to a recent version of Chef Compliance.'
exit 1
end
private
# for a feature, returns either:
# - a version v0: v supports v0 iff v0 <= v
# - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
def version_with_support(feature)
case feature.to_sym
when :oidc
Gem::Version.new('0.16.19')
else
Gem::Version.new('0.0.0')
end
end
end
end

View file

@ -1,116 +1,4 @@
# encoding: utf-8 # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
# author: Christoph Hartmann # TODO: Remove in inspec 4.0
# author: Dominik Richter
require 'net/http' require 'plugins/inspec-compliance/lib/inspec-compliance/http'
require 'net/http/post/multipart'
require 'uri'
module Compliance
# implements a simple http abstraction on top of Net::HTTP
class HTTP
# generic get requires
def self.get(url, headers = nil, insecure)
uri = _parse_url(url)
req = Net::HTTP::Get.new(uri.path)
headers&.each do |key, value|
req.add_field(key, value)
end
send_request(uri, req, insecure)
end
# generic post request
def self.post(url, token, insecure, basic_auth = false)
# form request
uri = _parse_url(url)
req = Net::HTTP::Post.new(uri.path)
if basic_auth
req.basic_auth token, ''
else
req['Authorization'] = "Bearer #{token}"
end
req.form_data={}
send_request(uri, req, insecure)
end
def self.post_with_headers(url, headers, body, insecure)
uri = _parse_url(url)
req = Net::HTTP::Post.new(uri.path)
req.body = body unless body.nil?
headers&.each do |key, value|
req.add_field(key, value)
end
send_request(uri, req, insecure)
end
# post a file
def self.post_file(url, headers, file_path, insecure)
uri = _parse_url(url)
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
http = Net::HTTP.new(uri.host, uri.port)
# set connection flags
http.use_ssl = (uri.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
req = Net::HTTP::Post.new(uri.path)
headers.each do |key, value|
req.add_field(key, value)
end
req.body_stream=File.open(file_path, 'rb')
req.add_field('Content-Length', File.size(file_path))
req.add_field('Content-Type', 'application/x-gzip')
boundary = 'INSPEC-PROFILE-UPLOAD'
req.add_field('session', boundary)
res=http.request(req)
res
end
def self.post_multipart_file(url, headers, file_path, insecure)
uri = _parse_url(url)
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
http = Net::HTTP.new(uri.host, uri.port)
# set connection flags
http.use_ssl = (uri.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
File.open(file_path) do |tar|
req = Net::HTTP::Post::Multipart.new(uri, 'file' => UploadIO.new(tar, 'application/x-gzip', File.basename(file_path)))
headers.each do |key, value|
req.add_field(key, value)
end
res = http.request(req)
return res
end
end
# sends a http requests
def self.send_request(uri, req, insecure)
opts = {
use_ssl: uri.scheme == 'https',
}
opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
raise "Unable to parse URI: #{uri}" if uri.nil? || uri.host.nil?
res = Net::HTTP.start(uri.host, uri.port, opts) { |http|
http.request(req)
}
res
rescue OpenSSL::SSL::SSLError => e
raise e unless e.message.include? 'certificate verify failed'
puts "Error: Failed to connect to #{uri}."
puts 'If the server uses a self-signed certificate, please re-run the login command with the --insecure option.'
exit 1
end
def self._parse_url(url)
url = "https://#{url}" if URI.parse(url).scheme.nil?
URI.parse(url)
end
end
end

View file

@ -1,36 +1,4 @@
# encoding: utf-8 # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
# author: Christoph Hartmann # TODO: Remove in inspec 4.0
# author: Dominik Richter
module Compliance require 'plugins/inspec-compliance/lib/inspec-compliance/support'
# is a helper that provides information which version of compliance supports
# which feature
class Support
# for a feature, returns either:
# - a version v0: v supports v0 iff v0 <= v
# - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
def self.version_with_support(feature)
case feature.to_sym
when :oidc # open id connect authentication
Gem::Version.new('0.16.19')
else
Gem::Version.new('0.0.0')
end
end
# determines if the given version support a certain feature
def self.supported?(feature, version)
sup = version_with_support(feature)
if sup.is_a?(Array)
Gem::Version.new(version) >= sup[0] &&
Gem::Version.new(version) < sup[1]
else
Gem::Version.new(version) >= sup
end
end
# we do not know the version, therefore we do not know if its possible to use the feature
# return if self['version'].nil? || self['version']['version'].nil?
end
end

View file

@ -1,143 +1,4 @@
# encoding: utf-8 # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
# author: Christoph Hartmann # TODO: Remove in inspec 4.0
# author: Dominik Richter
require 'uri' require 'plugins/inspec-compliance/lib/inspec-compliance/target'
require 'inspec/fetcher'
require 'inspec/errors'
# InSpec Target Helper for Chef Compliance
# reuses UrlHelper, but it knows the target server and the access token already
# similar to `inspec exec http://localhost:2134/owners/%base%/compliance/%ssh%/tar --user %token%`
module Compliance
class Fetcher < Fetchers::Url
name 'compliance'
priority 500
attr_reader :upstream_sha256
def initialize(target, opts)
super(target, opts)
@upstream_sha256 = ''
if target.is_a?(Hash) && target.key?(:url)
@target = target[:url]
@upstream_sha256 = target[:sha256]
elsif target.is_a?(String)
@target = target
end
end
def sha256
upstream_sha256.empty? ? super : upstream_sha256
end
def self.check_compliance_token(uri, config)
if config['token'].nil? && config['refresh_token'].nil?
if config['server_type'] == 'automate'
server = 'automate'
msg = 'inspec compliance login https://your_automate_server --user USER --ent ENT --dctoken DCTOKEN or --token USERTOKEN'
elsif config['server_type'] == 'automate2'
server = 'automate2'
msg = 'inspec compliance login https://your_automate2_server --user USER --token APITOKEN'
else
server = 'compliance'
msg = "inspec compliance login https://your_compliance_server --user admin --insecure --token 'PASTE TOKEN HERE' "
end
raise Inspec::FetcherFailure, <<~EOF
Cannot fetch #{uri} because your #{server} token has not been
configured.
Please login using
#{msg}
EOF
end
end
def self.get_target_uri(target)
if target.is_a?(String) && URI(target).scheme == 'compliance'
URI(target)
elsif target.respond_to?(:key?) && target.key?(:compliance)
URI("compliance://#{target[:compliance]}")
end
end
def self.resolve(target)
uri = get_target_uri(target)
return nil if uri.nil?
config = Compliance::Configuration.new
profile = Compliance::API.sanitize_profile_name(uri)
profile_fetch_url = Compliance::API.target_url(config, profile)
# we have detailed information available in our lockfile, no need to ask the server
if target.respond_to?(:key?) && target.key?(:sha256)
profile_checksum = target[:sha256]
else
check_compliance_token(uri, config)
# verifies that the target e.g base/ssh exists
# Call profiles directly instead of exist? to capture the results
# so we can access the upstream sha256 from the results.
_msg, profile_result = Compliance::API.profiles(config, profile)
if profile_result.empty?
raise Inspec::FetcherFailure, "The compliance profile #{profile} was not found on the configured compliance server"
else
# Guarantee sorting by verison and grab the latest.
# If version was specified, it will be the first and only result.
# Note we are calling the sha256 as a string, not a symbol since
# it was returned as json from the Compliance API.
profile_info = profile_result.sort_by { |x| Gem::Version.new(x['version']) }[0]
profile_checksum = profile_info.key?('sha256') ? profile_info['sha256'] : ''
end
end
# We need to pass the token to the fetcher
config['token'] = Compliance::API.get_token(config)
# Needed for automate2 post request
profile_stub = profile || target[:compliance]
config['profile'] = Compliance::API.profile_split(profile_stub)
new({ url: profile_fetch_url, sha256: profile_checksum }, config)
rescue URI::Error => _e
nil
end
# We want to save compliance: in the lockfile rather than url: to
# make sure we go back through the Compliance API handling.
def resolved_source
@resolved_source ||= {
compliance: compliance_profile_name,
url: @target,
sha256: sha256,
}
end
def to_s
'Chef Compliance Profile Loader'
end
private
# determine the owner_id and the profile name from the url
def compliance_profile_name
m = if Compliance::API.is_automate_server_pre_080?(@config)
%r{^#{@config['server']}/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
elsif Compliance::API.is_automate_server_080_and_later?(@config)
%r{^#{@config['server']}/profiles/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
else
%r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}
end.match(@target)
if Compliance::API.is_automate2_server?(@config)
m = {}
m[:owner] = @config['profile'][0]
m[:id] = @config['profile'][1]
end
raise 'Unable to determine compliance profile name. This can be caused by ' \
'an incorrect server in your configuration. Try to login to compliance ' \
'via the `inspec compliance login` command.' if m.nil?
"#{m[:owner]}/#{m[:id]}"
end
end
end

View file

@ -292,7 +292,10 @@ module Inspec
end end
# check for compliance settings # check for compliance settings
Compliance::API.login(o['compliance']) if o['compliance'] if o['compliance']
require 'plugins/inspec-compliance/lib/inspec-compliance/api'
InspecPlugins::Compliance::API.login(o['compliance'])
end
o o
end end

View file

@ -0,0 +1,12 @@
module InspecPlugins
module Compliance
class Plugin < Inspec.plugin(2)
plugin_name :'inspec-compliance'
cli_command :compliance do
require_relative 'inspec-compliance/cli'
InspecPlugins::Compliance::CLI
end
end
end
end

View file

@ -0,0 +1,358 @@
# encoding: utf-8
require 'net/http'
require 'uri'
require 'json'
require_relative 'api/login'
require_relative 'configuration'
require_relative 'http'
require_relative 'target'
require_relative 'support'
module InspecPlugins
module Compliance
class ServerConfigurationMissing < StandardError; end
# API Implementation does not hold any state by itself,
# everything will be stored in local Configuration store
class API
extend InspecPlugins::Compliance::API::Login
# return all compliance profiles available for the user
# the user is either specified in the options hash or by default
# the username of the account is used that is logged in
def self.profiles(config, profile_filter = nil) # rubocop:disable PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
owner = config['owner'] || config['user']
# Chef Compliance
if is_compliance_server?(config)
url = "#{config['server']}/user/compliance"
# Chef Automate2
elsif is_automate2_server?(config)
url = "#{config['server']}/compliance/profiles/search"
# Chef Automate
elsif is_automate_server?(config)
url = "#{config['server']}/profiles/#{owner}"
else
raise ServerConfigurationMissing
end
headers = get_headers(config)
if profile_filter
_owner, id, ver = profile_split(profile_filter)
else
id, ver = nil
end
if is_automate2_server?(config)
body = { owner: owner, name: id }.to_json
response = InspecPlugins::Compliance::HTTP.post_with_headers(url, headers, body, config['insecure'])
else
response = InspecPlugins::Compliance::HTTP.get(url, headers, config['insecure'])
end
data = response.body
response_code = response.code
case response_code
when '200'
msg = 'success'
profiles = JSON.parse(data)
# iterate over profiles
if is_compliance_server?(config)
mapped_profiles = []
profiles.values.each { |org|
mapped_profiles += org.values
}
# Chef Automate pre 0.8.0
elsif is_automate_server_pre_080?(config)
mapped_profiles = profiles.values.flatten
elsif is_automate2_server?(config)
mapped_profiles = []
profiles['profiles'].each { |p|
mapped_profiles << p
}
else
mapped_profiles = profiles.map { |e|
e['owner_id'] = owner
e
}
end
# filter by name and version if they were specified in profile_filter
mapped_profiles.select! do |p|
(!ver || p['version'] == ver) && (!id || p['name'] == id)
end
return msg, mapped_profiles
when '401'
msg = '401 Unauthorized. Please check your token.'
return msg, []
else
msg = "An unexpected error occurred (HTTP #{response_code}): #{response.message}"
return msg, []
end
end
# return the server api version
# NB this method does not use Compliance::Configuration to allow for using
# it before we know the version (e.g. oidc or not)
def self.version(config)
url = config['server']
insecure = config['insecure']
raise ServerConfigurationMissing if url.nil?
headers = get_headers(config)
response = InspecPlugins::Compliance::HTTP.get(url+'/version', headers, insecure)
return {} if response.code == '404'
data = response.body
return {} if data.nil? || data.empty?
parsed = JSON.parse(data)
return {} unless parsed.key?('version') && !parsed['version'].empty?
parsed
end
# verifies that a profile exists
def self.exist?(config, profile)
_msg, profiles = InspecPlugins::Compliance::API.profiles(config, profile)
!profiles.empty?
end
def self.upload(config, owner, profile_name, archive_path)
# Chef Compliance
if is_compliance_server?(config)
url = "#{config['server']}/owners/#{owner}/compliance/#{profile_name}/tar"
# Chef Automate pre 0.8.0
elsif is_automate_server_pre_080?(config)
url = "#{config['server']}/#{owner}"
elsif is_automate2_server?(config)
url = "#{config['server']}/compliance/profiles?owner=#{owner}"
# Chef Automate
else
url = "#{config['server']}/profiles/#{owner}"
end
headers = get_headers(config)
if is_automate2_server?(config)
res = InspecPlugins::Compliance::HTTP.post_multipart_file(url, headers, archive_path, config['insecure'])
else
res = InspecPlugins::Compliance::HTTP.post_file(url, headers, archive_path, config['insecure'])
end
[res.is_a?(Net::HTTPSuccess), res.body]
end
# Use username and refresh_token to get an API access token
def self.get_token_via_refresh_token(url, refresh_token, insecure)
uri = URI.parse("#{url}/login")
req = Net::HTTP::Post.new(uri.path)
req.body = { token: refresh_token }.to_json
access_token = nil
response = InspecPlugins::Compliance::HTTP.send_request(uri, req, insecure)
data = response.body
if response.code == '200'
begin
tokendata = JSON.parse(data)
access_token = tokendata['access_token']
msg = 'Successfully fetched API access token'
success = true
rescue JSON::ParserError => e
success = false
msg = e.message
end
else
success = false
msg = "Failed to authenticate to #{url} \n\
Response code: #{response.code}\n Body: #{response.body}"
end
[success, msg, access_token]
end
# Use username and password to get an API access token
def self.get_token_via_password(url, username, password, insecure)
uri = URI.parse("#{url}/login")
req = Net::HTTP::Post.new(uri.path)
req.body = { userid: username, password: password }.to_json
access_token = nil
response = InspecPlugins::Compliance::HTTP.send_request(uri, req, insecure)
data = response.body
if response.code == '200'
access_token = data
msg = 'Successfully fetched an API access token valid for 12 hours'
success = true
else
success = false
msg = "Failed to authenticate to #{url} \n\
Response code: #{response.code}\n Body: #{response.body}"
end
[success, msg, access_token]
end
def self.get_headers(config)
token = get_token(config)
if is_automate_server?(config) || is_automate2_server?(config)
headers = { 'chef-delivery-enterprise' => config['automate']['ent'] }
if config['automate']['token_type'] == 'dctoken'
headers['x-data-collector-token'] = token
else
headers['chef-delivery-user'] = config['user']
headers['chef-delivery-token'] = token
end
else
headers = { 'Authorization' => "Bearer #{token}" }
end
headers
end
def self.get_token(config)
return config['token'] unless config['refresh_token']
_success, _msg, token = get_token_via_refresh_token(config['server'], config['refresh_token'], config['insecure'])
token
end
def self.target_url(config, profile)
owner, id, ver = profile_split(profile)
return "#{config['server']}/compliance/profiles/tar" if is_automate2_server?(config)
return "#{config['server']}/owners/#{owner}/compliance/#{id}/tar" unless is_automate_server?(config)
if ver.nil?
"#{config['server']}/profiles/#{owner}/#{id}/tar"
else
"#{config['server']}/profiles/#{owner}/#{id}/version/#{ver}/tar"
end
end
def self.profile_split(profile)
owner, id = profile.split('/')
id, version = id.split('#')
[owner, id, version]
end
# returns a parsed url for `admin/profile` or `compliance://admin/profile`
def self.sanitize_profile_name(profile)
if URI(profile).scheme == 'compliance'
uri = URI(profile)
else
uri = URI("compliance://#{profile}")
end
uri.to_s.sub(%r{^compliance:\/\/}, '')
end
def self.is_compliance_server?(config)
config['server_type'] == 'compliance'
end
def self.is_automate_server_pre_080?(config)
# Automate versions before 0.8.x do not have a valid version in the config
return false unless config['server_type'] == 'automate'
server_version_from_config(config).nil?
end
def self.is_automate_server_080_and_later?(config)
# Automate versions 0.8.x and later will have a "version" key in the config
# that is properly parsed out via server_version_from_config below
return false unless config['server_type'] == 'automate'
!server_version_from_config(config).nil?
end
def self.is_automate2_server?(config)
config['server_type'] == 'automate2'
end
def self.is_automate_server?(config)
config['server_type'] == 'automate'
end
def self.server_version_from_config(config)
# Automate versions 0.8.x and later will have a "version" key in the config
# that looks like: "version":{"api":"compliance","version":"0.8.24"}
return nil unless config.key?('version')
return nil unless config['version'].is_a?(Hash)
config['version']['version']
end
def self.determine_server_type(url, insecure)
if target_is_automate2_server?(url, insecure)
:automate2
elsif target_is_automate_server?(url, insecure)
:automate
elsif target_is_compliance_server?(url, insecure)
:compliance
else
Inspec::Log.debug('Could not determine server type using known endpoints')
nil
end
end
def self.target_is_automate2_server?(url, insecure)
automate_endpoint = '/dex/auth'
response = InspecPlugins::Compliance::HTTP.get(url + automate_endpoint, nil, insecure)
if response.code == '400'
Inspec::Log.debug(
"Received 400 from #{url}#{automate_endpoint} - " \
'assuming target is a Chef Automate2 instance',
)
true
else
false
end
end
def self.target_is_automate_server?(url, insecure)
automate_endpoint = '/compliance/version'
response = InspecPlugins::Compliance::HTTP.get(url + automate_endpoint, nil, insecure)
case response.code
when '401'
Inspec::Log.debug(
"Received 401 from #{url}#{automate_endpoint} - " \
'assuming target is a Chef Automate instance',
)
true
when '200'
# Chef Automate currently returns 401 for `/compliance/version` but some
# versions of OpsWorks Chef Automate return 200 and a Chef Manage page
# when unauthenticated requests are received.
if response.body.include?('Are You Looking For the Chef Server?')
Inspec::Log.debug(
"Received 200 from #{url}#{automate_endpoint} - " \
'assuming target is an OpsWorks Chef Automate instance',
)
true
else
Inspec::Log.debug(
"Received 200 from #{url}#{automate_endpoint} " \
'but did not receive the Chef Manage page - ' \
'assuming target is not a Chef Automate instance',
)
false
end
else
Inspec::Log.debug(
"Received unexpected status code #{response.code} " \
"from #{url}#{automate_endpoint} - " \
'assuming target is not a Chef Automate instance',
)
false
end
end
def self.target_is_compliance_server?(url, insecure)
# All versions of Chef Compliance return 200 for `/api/version`
compliance_endpoint = '/api/version'
response = InspecPlugins::Compliance::HTTP.get(url + compliance_endpoint, nil, insecure)
return false unless response.code == '200'
Inspec::Log.debug(
"Received 200 from #{url}#{compliance_endpoint} - " \
'assuming target is a Compliance server',
)
true
end
end
end
end

View file

@ -0,0 +1,192 @@
# encoding: utf-8
module InspecPlugins
module Compliance
class API
module Login
class CannotDetermineServerType < StandardError; end
def login(options)
raise ArgumentError, 'Please specify a server using `inspec compliance login https://SERVER`' unless options['server']
options['server'] = URI("https://#{options['server']}").to_s if URI(options['server']).scheme.nil?
options['server_type'] = InspecPlugins::Compliance::API.determine_server_type(options['server'], options['insecure'])
case options['server_type']
when :automate2
Login::Automate2Server.login(options)
when :automate
Login::AutomateServer.login(options)
when :compliance
Login::ComplianceServer.login(options)
else
raise CannotDetermineServerType, "Unable to determine if #{options['server']} is a Chef Automate or Chef Compliance server"
end
end
module Automate2Server
def self.login(options)
verify_thor_options(options)
options['url'] = options['server'] + '/api/v0'
token = options['dctoken'] || options['token']
store_access_token(options, token)
end
def self.store_access_token(options, token)
config = InspecPlugins::Compliance::Configuration.new
config.clean
config['automate'] = {}
config['automate']['ent'] = 'automate'
config['automate']['token_type'] = 'dctoken'
config['server'] = options['url']
config['user'] = options['user']
config['owner'] = options['user']
config['insecure'] = options['insecure'] || false
config['server_type'] = options['server_type'].to_s
config['token'] = token
config['version'] = '0'
config.store
config
end
def self.verify_thor_options(o)
error_msg = []
error_msg.push('Please specify a user using `--user=\'USER\'`') if o['user'].nil?
if o['token'].nil? && o['dctoken'].nil?
error_msg.push('Please specify a token using `--token=\'APITOKEN\'`')
end
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
end
end
module AutomateServer
def self.login(options)
verify_thor_options(options)
options['url'] = options['server'] + '/compliance'
token = options['dctoken'] || options['token']
store_access_token(options, token)
end
def self.store_access_token(options, token)
token_type = if options['token']
'usertoken'
else
'dctoken'
end
config = InspecPlugins::Compliance::Configuration.new
config.clean
config['automate'] = {}
config['automate']['ent'] = options['ent']
config['automate']['token_type'] = token_type
config['server'] = options['url']
config['user'] = options['user']
config['insecure'] = options['insecure'] || false
config['server_type'] = options['server_type'].to_s
config['token'] = token
config['version'] = InspecPlugins::Compliance::API.version(config)
config.store
config
end
# Automate login requires `--ent`, `--user`, and either `--token` or `--dctoken`
def self.verify_thor_options(o)
error_msg = []
error_msg.push('Please specify a user using `--user=\'USER\'`') if o['user'].nil?
error_msg.push('Please specify an enterprise using `--ent=\'automate\'`') if o['ent'].nil?
if o['token'].nil? && o['dctoken'].nil?
error_msg.push('Please specify a token using `--token=\'AUTOMATE_TOKEN\'` or `--dctoken=\'DATA_COLLECTOR_TOKEN\'`')
end
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
end
end
module ComplianceServer
def self.login(options)
compliance_verify_thor_options(options)
options['url'] = options['server'] + '/api'
if options['user'] && options['token']
compliance_store_access_token(options, options['token'])
elsif options['user'] && options['password']
compliance_login_user_pass(options)
elsif options['refresh_token']
compliance_login_refresh_token(options)
end
end
def self.compliance_login_user_pass(options)
success, msg, token = InspecPlugins::Compliance::API.get_token_via_password(
options['url'],
options['user'],
options['password'],
options['insecure'],
)
raise msg unless success
compliance_store_access_token(options, token)
end
def self.compliance_login_refresh_token(options)
success, msg, token = InspecPlugins::Compliance::API.get_token_via_refresh_token(
options['url'],
options['refresh_token'],
options['insecure'],
)
raise msg unless success
compliance_store_access_token(options, token)
end
def self.compliance_store_access_token(options, token)
config = InspecPlugins::Compliance::Configuration.new
config.clean
config['user'] = options['user'] if options['user']
config['server'] = options['url']
config['insecure'] = options['insecure'] || false
config['server_type'] = options['server_type'].to_s
config['token'] = token
config['version'] = InspecPlugins::Compliance::API.version(config)
config.store
config
end
# Compliance login requires `--user` or `--refresh_token`
# If `--user` then either `--password`, `--token`, or `--refresh-token`, is required
def self.compliance_verify_thor_options(o)
error_msg = []
error_msg.push('Please specify a server using `inspec compliance login https://SERVER`') if o['server'].nil?
if o['user'].nil? && o['refresh_token'].nil?
error_msg.push('Please specify a `--user=\'USER\'` or a `--refresh-token=\'TOKEN\'`')
end
if o['user'] && o['password'].nil? && o['token'].nil? && o['refresh_token'].nil?
error_msg.push('Please specify either a `--password`, `--token`, or `--refresh-token`')
end
raise ArgumentError, error_msg.join("\n") unless error_msg.empty?
end
end
end
end
end
end

View file

@ -0,0 +1,266 @@
# encoding: utf-8
require_relative 'api'
module InspecPlugins
module Compliance
class CLI < Inspec.plugin(2, :cli_command)
subcommand_desc 'compliance SUBCOMMAND', 'Chef Compliance commands'
# desc "login https://SERVER --insecure --user='USER' --ent='ENTERPRISE' --token='TOKEN'", 'Log in to a Chef Compliance/Chef Automate SERVER'
desc 'login', 'Log in to a Chef Compliance/Chef Automate SERVER'
long_desc <<-LONGDESC
`login` allows you to use InSpec with Chef Automate or a Chef Compliance Server
You need to a token for communication. More information about token retrieval
is available at:
https://docs.chef.io/api_automate.html#authentication-methods
https://docs.chef.io/api_compliance.html#obtaining-an-api-token
LONGDESC
option :insecure, aliases: :k, type: :boolean,
desc: 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
option :user, type: :string, required: false,
desc: 'Username'
option :password, type: :string, required: false,
desc: 'Password (Chef Compliance Only)'
option :token, type: :string, required: false,
desc: 'Access token'
option :refresh_token, type: :string, required: false,
desc: 'Chef Compliance refresh token (Chef Compliance Only)'
option :dctoken, type: :string, required: false,
desc: 'Data Collector token (Chef Automate Only)'
option :ent, type: :string, required: false,
desc: 'Enterprise for Chef Automate reporting (Chef Automate Only)'
def login(server)
options['server'] = server
InspecPlugins::Compliance::API.login(options)
config = InspecPlugins::Compliance::Configuration.new
puts "Stored configuration for Chef #{config['server_type'].capitalize}: #{config['server']}' with user: '#{config['user']}'"
end
desc 'profiles', 'list all available profiles in Chef Compliance'
option :owner, type: :string, required: false,
desc: 'owner whose profiles to list'
def profiles
config = InspecPlugins::Compliance::Configuration.new
return if !loggedin(config)
# set owner to config
config['owner'] = options['owner'] || config['user']
msg, profiles = InspecPlugins::Compliance::API.profiles(config)
profiles.sort_by! { |hsh| hsh['title'] }
if !profiles.empty?
# iterate over profiles
headline('Available profiles:')
profiles.each { |profile|
owner = profile['owner_id'] || profile['owner']
li("#{profile['title']} v#{profile['version']} (#{mark_text(owner + '/' + profile['name'])})")
}
else
puts msg if msg != 'success'
puts 'Could not find any profiles'
exit 1
end
rescue InspecPlugins::Compliance::ServerConfigurationMissing
STDERR.puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
exit 1
end
desc 'exec PROFILE', 'executes a Chef Compliance profile'
exec_options
def exec(*tests)
config = InspecPlugins::Compliance::Configuration.new
return if !loggedin(config)
o = opts(:exec).dup
diagnose(o)
configure_logger(o)
# iterate over tests and add compliance scheme
tests = tests.map { |t| 'compliance://' + InspecPlugins::Compliance::API.sanitize_profile_name(t) }
runner = Inspec::Runner.new(o)
tests.each { |target| runner.add_target(target) }
exit runner.run
rescue ArgumentError, RuntimeError, Train::UserError => e
$stderr.puts e.message
exit 1
end
desc 'download PROFILE', 'downloads a profile from Chef Compliance'
option :name, type: :string,
desc: 'Name of the archive filename (file type will be added)'
def download(profile_name)
o = options.dup
configure_logger(o)
config = InspecPlugins::Compliance::Configuration.new
return if !loggedin(config)
profile_name = InspecPlugins::Compliance::API.sanitize_profile_name(profile_name)
if InspecPlugins::Compliance::API.exist?(config, profile_name)
puts "Downloading `#{profile_name}`"
fetcher = InspecPlugins::Compliance::Fetcher.resolve(
{
compliance: profile_name,
},
)
# we provide a name, the fetcher adds the extension
_owner, id = profile_name.split('/')
file_name = fetcher.fetch(o.name || id)
puts "Profile stored to #{file_name}"
else
puts "Profile #{profile_name} is not available in Chef Compliance."
exit 1
end
end
desc 'upload PATH', 'uploads a local profile to Chef Compliance'
option :overwrite, type: :boolean, default: false,
desc: 'Overwrite existing profile on Server.'
option :owner, type: :string, required: false,
desc: 'Owner that should own the profile'
def upload(path) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, PerceivedComplexity, Metrics/CyclomaticComplexity
config = InspecPlugins::Compliance::Configuration.new
return if !loggedin(config)
# set owner to config
config['owner'] = options['owner'] || config['user']
unless File.exist?(path)
puts "Directory #{path} does not exist."
exit 1
end
vendor_deps(path, options) if File.directory?(path)
o = options.dup
configure_logger(o)
# only run against the mock backend, otherwise we run against the local system
o[:backend] = Inspec::Backend.create(target: 'mock://')
o[:check_mode] = true
o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
# check the profile, we only allow to upload valid profiles
profile = Inspec::Profile.for_target(path, o)
# start verification process
error_count = 0
error = lambda { |msg|
error_count += 1
puts msg
}
result = profile.check
unless result[:summary][:valid]
error.call('Profile check failed. Please fix the profile before upload.')
else
puts('Profile is valid')
end
# determine user information
if (config['token'].nil? && config['refresh_token'].nil?) || config['user'].nil?
error.call('Please login via `inspec compliance login`')
end
# read profile name from inspec.yml
profile_name = profile.params[:name]
# read profile version from inspec.yml
profile_version = profile.params[:version]
# check that the profile is not uploaded already,
# confirm upload to the user (overwrite with --force)
if InspecPlugins::Compliance::API.exist?(config, "#{config['owner']}/#{profile_name}##{profile_version}") && !options['overwrite']
error.call('Profile exists on the server, use --overwrite')
end
# abort if we found an error
if error_count > 0
puts "Found #{error_count} error(s)"
exit 1
end
# if it is a directory, tar it to tmp directory
generated = false
if File.directory?(path)
generated = true
archive_path = Dir::Tmpname.create([profile_name, '.tar.gz']) {}
puts "Generate temporary profile archive at #{archive_path}"
profile.archive({ output: archive_path, ignore_errors: false, overwrite: true })
else
archive_path = path
end
puts "Start upload to #{config['owner']}/#{profile_name}"
pname = ERB::Util.url_encode(profile_name)
if InspecPlugins::Compliance::API.is_automate_server?(config) || InspecPlugins::Compliance::API.is_automate2_server?(config)
puts 'Uploading to Chef Automate'
else
puts 'Uploading to Chef Compliance'
end
success, msg = InspecPlugins::Compliance::API.upload(config, config['owner'], pname, archive_path)
# delete temp file if it was temporary generated
File.delete(archive_path) if generated && File.exist?(archive_path)
if success
puts 'Successfully uploaded profile'
else
puts 'Error during profile upload:'
puts msg
exit 1
end
end
desc 'version', 'displays the version of the Chef Compliance server'
def version
config = InspecPlugins::Compliance::Configuration.new
info = InspecPlugins::Compliance::API.version(config)
if !info.nil? && info['version']
puts "Name: #{info['api']}"
puts "Version: #{info['version']}"
else
puts 'Could not determine server version.'
exit 1
end
rescue InspecPlugins::Compliance::ServerConfigurationMissing
puts "\nServer configuration information is missing. Please login using `inspec compliance login`"
exit 1
end
desc 'logout', 'user logout from Chef Compliance'
def logout
config = InspecPlugins::Compliance::Configuration.new
unless config.supported?(:oidc) || config['token'].nil? || config['server_type'] == 'automate'
config = InspecPlugins::Compliance::Configuration.new
url = "#{config['server']}/logout"
InspecPlugins::Compliance::HTTP.post(url, config['token'], config['insecure'], !config.supported?(:oidc))
end
success = config.destroy
if success
puts 'Successfully logged out'
else
puts 'Could not log out'
end
end
private
def loggedin(config)
serverknown = !config['server'].nil?
puts 'You need to login first with `inspec compliance login`' if !serverknown
serverknown
end
end
# register the subcommand to Inspec CLI registry
# Inspec::Plugins::CLI.add_subcommand(InspecPlugins::ComplianceCLI, 'compliance', 'compliance SUBCOMMAND ...', 'Chef InspecPlugins::Compliance commands', {})
end
end

View file

@ -0,0 +1,103 @@
# encoding: utf-8
module InspecPlugins
module Compliance
# stores configuration on local filesystem
class Configuration
def initialize
@config_path = File.join(Dir.home, '.inspec', 'compliance')
# ensure the directory is available
unless File.directory?(@config_path)
FileUtils.mkdir_p(@config_path)
end
# set config file path
@config_file = File.join(@config_path, '/config.json')
@config = {}
# load the data
get
end
# direct access to config
def [](key)
@config[key]
end
def []=(key, value)
@config[key] = value
end
def key?(key)
@config.key?(key)
end
def clean
@config = {}
end
# return the json data
def get
if File.exist?(@config_file)
file = File.read(@config_file)
@config = JSON.parse(file)
end
@config
end
# stores a hash to json
def store
File.open(@config_file, 'w') do |f|
f.chmod(0600)
f.write(@config.to_json)
end
end
# deletes data
def destroy
if File.exist?(@config_file)
File.delete(@config_file)
else
true
end
end
# return if the (stored) api version does not support a certain feature
def supported?(feature)
sup = version_with_support(feature)
# we do not know the version, therefore we do not know if its possible to use the feature
return if self['version'].nil? || self['version']['version'].nil?
if sup.is_a?(Array)
Gem::Version.new(self['version']['version']) >= sup[0] &&
Gem::Version.new(self['version']['version']) < sup[1]
else
Gem::Version.new(self['version']['version']) >= sup
end
end
# exit 1 if the version of compliance that we're working with doesn't support odic
def legacy_check!(feature)
return if supported?(feature)
puts "This feature (#{feature}) is not available for legacy installations."
puts 'Please upgrade to a recent version of Chef Compliance.'
exit 1
end
private
# for a feature, returns either:
# - a version v0: v supports v0 iff v0 <= v
# - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
def version_with_support(feature)
case feature.to_sym
when :oidc
Gem::Version.new('0.16.19')
else
Gem::Version.new('0.0.0')
end
end
end
end
end

View file

@ -0,0 +1,116 @@
# encoding: utf-8
require 'net/http'
require 'net/http/post/multipart'
require 'uri'
module InspecPlugins
module Compliance
# implements a simple http abstraction on top of Net::HTTP
class HTTP
# generic get requires
def self.get(url, headers = nil, insecure)
uri = _parse_url(url)
req = Net::HTTP::Get.new(uri.path)
headers&.each do |key, value|
req.add_field(key, value)
end
send_request(uri, req, insecure)
end
# generic post request
def self.post(url, token, insecure, basic_auth = false)
# form request
uri = _parse_url(url)
req = Net::HTTP::Post.new(uri.path)
if basic_auth
req.basic_auth token, ''
else
req['Authorization'] = "Bearer #{token}"
end
req.form_data={}
send_request(uri, req, insecure)
end
def self.post_with_headers(url, headers, body, insecure)
uri = _parse_url(url)
req = Net::HTTP::Post.new(uri.path)
req.body = body unless body.nil?
headers&.each do |key, value|
req.add_field(key, value)
end
send_request(uri, req, insecure)
end
# post a file
def self.post_file(url, headers, file_path, insecure)
uri = _parse_url(url)
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
http = Net::HTTP.new(uri.host, uri.port)
# set connection flags
http.use_ssl = (uri.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
req = Net::HTTP::Post.new(uri.path)
headers.each do |key, value|
req.add_field(key, value)
end
req.body_stream=File.open(file_path, 'rb')
req.add_field('Content-Length', File.size(file_path))
req.add_field('Content-Type', 'application/x-gzip')
boundary = 'INSPEC-PROFILE-UPLOAD'
req.add_field('session', boundary)
res=http.request(req)
res
end
def self.post_multipart_file(url, headers, file_path, insecure)
uri = _parse_url(url)
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
http = Net::HTTP.new(uri.host, uri.port)
# set connection flags
http.use_ssl = (uri.scheme == 'https')
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
File.open(file_path) do |tar|
req = Net::HTTP::Post::Multipart.new(uri, 'file' => UploadIO.new(tar, 'application/x-gzip', File.basename(file_path)))
headers.each do |key, value|
req.add_field(key, value)
end
res = http.request(req)
return res
end
end
# sends a http requests
def self.send_request(uri, req, insecure)
opts = {
use_ssl: uri.scheme == 'https',
}
opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
raise "Unable to parse URI: #{uri}" if uri.nil? || uri.host.nil?
res = Net::HTTP.start(uri.host, uri.port, opts) { |http|
http.request(req)
}
res
rescue OpenSSL::SSL::SSLError => e
raise e unless e.message.include? 'certificate verify failed'
puts "Error: Failed to connect to #{uri}."
puts 'If the server uses a self-signed certificate, please re-run the login command with the --insecure option.'
exit 1
end
def self._parse_url(url)
url = "https://#{url}" if URI.parse(url).scheme.nil?
URI.parse(url)
end
end
end
end

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View file

@ -0,0 +1,36 @@
# encoding: utf-8
module InspecPlugins
module Compliance
# is a helper that provides information which version of compliance supports
# which feature
class Support
# for a feature, returns either:
# - a version v0: v supports v0 iff v0 <= v
# - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
def self.version_with_support(feature)
case feature.to_sym
when :oidc # open id connect authentication
Gem::Version.new('0.16.19')
else
Gem::Version.new('0.0.0')
end
end
# determines if the given version support a certain feature
def self.supported?(feature, version)
sup = version_with_support(feature)
if sup.is_a?(Array)
Gem::Version.new(version) >= sup[0] &&
Gem::Version.new(version) < sup[1]
else
Gem::Version.new(version) >= sup
end
end
# we do not know the version, therefore we do not know if its possible to use the feature
# return if self['version'].nil? || self['version']['version'].nil?
end
end
end

View file

@ -0,0 +1,143 @@
# encoding: utf-8
require 'uri'
require 'inspec/fetcher'
require 'inspec/errors'
# InSpec Target Helper for Chef Compliance
# reuses UrlHelper, but it knows the target server and the access token already
# similar to `inspec exec http://localhost:2134/owners/%base%/compliance/%ssh%/tar --user %token%`
module InspecPlugins
module Compliance
class Fetcher < Fetchers::Url
name 'compliance'
priority 500
attr_reader :upstream_sha256
def initialize(target, opts)
super(target, opts)
@upstream_sha256 = ''
if target.is_a?(Hash) && target.key?(:url)
@target = target[:url]
@upstream_sha256 = target[:sha256]
elsif target.is_a?(String)
@target = target
end
end
def sha256
upstream_sha256.empty? ? super : upstream_sha256
end
def self.check_compliance_token(uri, config)
if config['token'].nil? && config['refresh_token'].nil?
if config['server_type'] == 'automate'
server = 'automate'
msg = 'inspec compliance login https://your_automate_server --user USER --ent ENT --dctoken DCTOKEN or --token USERTOKEN'
elsif config['server_type'] == 'automate2'
server = 'automate2'
msg = 'inspec compliance login https://your_automate2_server --user USER --token APITOKEN'
else
server = 'compliance'
msg = "inspec compliance login https://your_compliance_server --user admin --insecure --token 'PASTE TOKEN HERE' "
end
raise Inspec::FetcherFailure, <<~EOF
Cannot fetch #{uri} because your #{server} token has not been
configured.
Please login using
#{msg}
EOF
end
end
def self.get_target_uri(target)
if target.is_a?(String) && URI(target).scheme == 'compliance'
URI(target)
elsif target.respond_to?(:key?) && target.key?(:compliance)
URI("compliance://#{target[:compliance]}")
end
end
def self.resolve(target)
uri = get_target_uri(target)
return nil if uri.nil?
config = InspecPlugins::Compliance::Configuration.new
profile = InspecPlugins::Compliance::API.sanitize_profile_name(uri)
profile_fetch_url = InspecPlugins::Compliance::API.target_url(config, profile)
# we have detailed information available in our lockfile, no need to ask the server
if target.respond_to?(:key?) && target.key?(:sha256)
profile_checksum = target[:sha256]
else
check_compliance_token(uri, config)
# verifies that the target e.g base/ssh exists
# Call profiles directly instead of exist? to capture the results
# so we can access the upstream sha256 from the results.
_msg, profile_result = InspecPlugins::Compliance::API.profiles(config, profile)
if profile_result.empty?
raise Inspec::FetcherFailure, "The compliance profile #{profile} was not found on the configured compliance server"
else
# Guarantee sorting by verison and grab the latest.
# If version was specified, it will be the first and only result.
# Note we are calling the sha256 as a string, not a symbol since
# it was returned as json from the Compliance API.
profile_info = profile_result.sort_by { |x| Gem::Version.new(x['version']) }[0]
profile_checksum = profile_info.key?('sha256') ? profile_info['sha256'] : ''
end
end
# We need to pass the token to the fetcher
config['token'] = InspecPlugins::Compliance::API.get_token(config)
# Needed for automate2 post request
profile_stub = profile || target[:compliance]
config['profile'] = InspecPlugins::Compliance::API.profile_split(profile_stub)
new({ url: profile_fetch_url, sha256: profile_checksum }, config)
rescue URI::Error => _e
nil
end
# We want to save compliance: in the lockfile rather than url: to
# make sure we go back through the Compliance API handling.
def resolved_source
@resolved_source ||= {
compliance: compliance_profile_name,
url: @target,
sha256: sha256,
}
end
def to_s
'Chef Compliance Profile Loader'
end
private
# determine the owner_id and the profile name from the url
def compliance_profile_name
m = if InspecPlugins::Compliance::API.is_automate_server_pre_080?(@config)
%r{^#{@config['server']}/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
elsif InspecPlugins::Compliance::API.is_automate_server_080_and_later?(@config)
%r{^#{@config['server']}/profiles/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
else
%r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}
end.match(@target)
if InspecPlugins::Compliance::API.is_automate2_server?(@config)
m = {}
m[:owner] = @config['profile'][0]
m[:id] = @config['profile'][1]
end
raise 'Unable to determine compliance profile name. This can be caused by ' \
'an incorrect server in your configuration. Try to login to compliance ' \
'via the `inspec compliance login` command.' if m.nil?
"#{m[:owner]}/#{m[:id]}"
end
end
end
end

View file

@ -0,0 +1,43 @@
# encoding: utf-8
require_relative '../../../shared/core_plugin_test_helper.rb'
class ComplianceCli < MiniTest::Test
include CorePluginFunctionalHelper
def test_help_output
out = run_inspec_process('compliance help')
assert_equal out.exit_status, 0
assert_includes out.stdout, 'inspec compliance exec PROFILE'
end
def test_logout_command
out = run_inspec_process('compliance logout')
assert_equal out.exit_status, 0
assert_includes out.stdout, ''
end
def test_error_login_with_invalid_url
out = run_inspec_process('compliance login')
assert_equal out.exit_status, 1
assert_includes out.stderr, 'ERROR: "inspec compliance login" was called with no arguments'
end
def test_profile_list_without_auth
out = run_inspec_process('compliance profiles')
assert_equal out.exit_status, 0 # TODO: make this error
assert_includes out.stdout, 'You need to login first with `inspec compliance login`'
end
def test_error_upload_without_args
out = run_inspec_process('compliance upload')
assert_equal out.exit_status, 1
assert_includes out.stderr, 'ERROR: "inspec compliance upload" was called with no arguments'
end
def test_error_upload_with_fake_path
out = run_inspec_process('compliance upload /path/to/dir')
assert_equal out.exit_status, 0 # TODO: make this error
assert_includes out.stdout, 'You need to login first with `inspec compliance login`'
end
end

View file

@ -1,6 +1,9 @@
require 'helper' require 'minitest/autorun'
require 'mocha/setup'
require 'webmock/minitest'
require_relative '../../../lib/inspec-compliance/api.rb'
describe Compliance::API do describe InspecPlugins::Compliance::API do
let(:automate_options) do let(:automate_options) do
{ {
'server' => 'https://automate.example.com', 'server' => 'https://automate.example.com',
@ -49,13 +52,13 @@ describe Compliance::API do
describe '.login' do describe '.login' do
describe 'when target is a Chef Automate2 server' do describe 'when target is a Chef Automate2 server' do
before do before do
Compliance::API.expects(:determine_server_type).returns(:automate2) InspecPlugins::Compliance::API.expects(:determine_server_type).returns(:automate2)
end end
it 'raises an error if `--user` is missing' do it 'raises an error if `--user` is missing' do
options = automate_options options = automate_options
options.delete('user') options.delete('user')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify a user.*/) err.message.must_match(/Please specify a user.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -64,7 +67,7 @@ describe Compliance::API do
options = automate_options options = automate_options
options.delete('token') options.delete('token')
options.delete('dctoken') options.delete('dctoken')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify a token.*/) err.message.must_match(/Please specify a token.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -73,9 +76,9 @@ describe Compliance::API do
stub_request(:get, automate_options['server'] + '/compliance/version') stub_request(:get, automate_options['server'] + '/compliance/version')
.to_return(status: 200, body: '', headers: {}) .to_return(status: 200, body: '', headers: {})
options = automate_options options = automate_options
Compliance::Configuration.expects(:new).returns(fake_config) InspecPlugins::Compliance::Configuration.expects(:new).returns(fake_config)
Compliance::API.login(options) InspecPlugins::Compliance::API.login(options)
fake_config['automate']['ent'].must_equal('automate') fake_config['automate']['ent'].must_equal('automate')
fake_config['automate']['token_type'].must_equal('dctoken') fake_config['automate']['token_type'].must_equal('dctoken')
fake_config['user'].must_equal('someone') fake_config['user'].must_equal('someone')
@ -87,13 +90,13 @@ describe Compliance::API do
describe 'when target is a Chef Automate server' do describe 'when target is a Chef Automate server' do
before do before do
Compliance::API.expects(:determine_server_type).returns(:automate) InspecPlugins::Compliance::API.expects(:determine_server_type).returns(:automate)
end end
it 'raises an error if `--user` is missing' do it 'raises an error if `--user` is missing' do
options = automate_options options = automate_options
options.delete('user') options.delete('user')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify a user.*/) err.message.must_match(/Please specify a user.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -101,7 +104,7 @@ describe Compliance::API do
it 'raises an error if `--ent` is missing' do it 'raises an error if `--ent` is missing' do
options = automate_options options = automate_options
options.delete('ent') options.delete('ent')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify an enterprise.*/) err.message.must_match(/Please specify an enterprise.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -110,7 +113,7 @@ describe Compliance::API do
options = automate_options options = automate_options
options.delete('token') options.delete('token')
options.delete('dctoken') options.delete('dctoken')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify a token.*/) err.message.must_match(/Please specify a token.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -119,9 +122,9 @@ describe Compliance::API do
stub_request(:get, automate_options['server'] + '/compliance/version') stub_request(:get, automate_options['server'] + '/compliance/version')
.to_return(status: 200, body: '', headers: {}) .to_return(status: 200, body: '', headers: {})
options = automate_options options = automate_options
Compliance::Configuration.expects(:new).returns(fake_config) InspecPlugins::Compliance::Configuration.expects(:new).returns(fake_config)
Compliance::API.login(options) InspecPlugins::Compliance::API.login(options)
fake_config['automate']['ent'].must_equal('automate') fake_config['automate']['ent'].must_equal('automate')
fake_config['automate']['token_type'].must_equal('usertoken') fake_config['automate']['token_type'].must_equal('usertoken')
fake_config['user'].must_equal('someone') fake_config['user'].must_equal('someone')
@ -133,14 +136,14 @@ describe Compliance::API do
describe 'when target is a Chef Compliance server' do describe 'when target is a Chef Compliance server' do
before do before do
Compliance::API.expects(:determine_server_type).returns(:compliance) InspecPlugins::Compliance::API.expects(:determine_server_type).returns(:compliance)
end end
it 'raises an error if `--user` and `--refresh-token` are missing' do it 'raises an error if `--user` and `--refresh-token` are missing' do
options = automate_options options = automate_options
options.delete('user') options.delete('user')
options.delete('refresh_token') options.delete('refresh_token')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify a.*--user.*--refresh-token.*/) err.message.must_match(/Please specify a.*--user.*--refresh-token.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -150,7 +153,7 @@ describe Compliance::API do
options.delete('password') options.delete('password')
options.delete('token') options.delete('token')
options.delete('refresh_token') options.delete('refresh_token')
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify.*--password.*--token.*--refresh-token.*/) err.message.must_match(/Please specify.*--password.*--token.*--refresh-token.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
@ -159,9 +162,9 @@ describe Compliance::API do
stub_request(:get, compliance_options['server'] + '/api/version') stub_request(:get, compliance_options['server'] + '/api/version')
.to_return(status: 200, body: '', headers: {}) .to_return(status: 200, body: '', headers: {})
options = compliance_options options = compliance_options
Compliance::Configuration.expects(:new).returns(fake_config) InspecPlugins::Compliance::Configuration.expects(:new).returns(fake_config)
Compliance::API.login(options) InspecPlugins::Compliance::API.login(options)
fake_config['user'].must_equal('someone') fake_config['user'].must_equal('someone')
fake_config['server'].must_equal('https://compliance.example.com/api') fake_config['server'].must_equal('https://compliance.example.com/api')
fake_config['server_type'].must_equal('compliance') fake_config['server_type'].must_equal('compliance')
@ -172,14 +175,14 @@ describe Compliance::API do
describe 'when target is neither a Chef Compliance nor Chef Automate server' do describe 'when target is neither a Chef Compliance nor Chef Automate server' do
it 'raises an error if `https://SERVER` is missing' do it 'raises an error if `https://SERVER` is missing' do
options = {} options = {}
err = proc { Compliance::API.login(options) }.must_raise(ArgumentError) err = proc { InspecPlugins::Compliance::API.login(options) }.must_raise(ArgumentError)
err.message.must_match(/Please specify a server.*/) err.message.must_match(/Please specify a server.*/)
err.message.lines.length.must_equal(1) err.message.lines.length.must_equal(1)
end end
it 'rasies a `CannotDetermineServerType` error' do it 'rasies a `CannotDetermineServerType` error' do
Compliance::API.expects(:determine_server_type).returns(nil) InspecPlugins::Compliance::API.expects(:determine_server_type).returns(nil)
err = proc { Compliance::API.login(automate_options) }.must_raise(StandardError) err = proc { InspecPlugins::Compliance::API.login(automate_options) }.must_raise(StandardError)
err.message.must_match(/Unable to determine/) err.message.must_match(/Unable to determine/)
end end
end end

View file

@ -1,6 +1,8 @@
require 'helper' require 'minitest/autorun'
require 'mocha/setup'
require_relative '../../lib/inspec-compliance/api.rb'
describe Compliance::API do describe InspecPlugins::Compliance::API do
let(:profiles_response) do let(:profiles_response) do
[{ 'name'=>'apache-baseline', [{ 'name'=>'apache-baseline',
'title'=>'DevSec Apache Baseline', 'title'=>'DevSec Apache Baseline',
@ -49,15 +51,15 @@ describe Compliance::API do
end end
before do before do
Compliance::API.expects(:get_headers).returns(headers) InspecPlugins::Compliance::API.expects(:get_headers).returns(headers)
end end
describe 'when a 404 is received' do describe 'when a 404 is received' do
it 'should return an empty hash' do it 'should return an empty hash' do
response = mock response = mock
response.stubs(:code).returns('404') response.stubs(:code).returns('404')
Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response) InspecPlugins::Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response)
Compliance::API.version(config).must_equal({}) InspecPlugins::Compliance::API.version(config).must_equal({})
end end
end end
@ -66,8 +68,8 @@ describe Compliance::API do
response = mock response = mock
response.stubs(:code).returns('200') response.stubs(:code).returns('200')
response.stubs(:body).returns(nil) response.stubs(:body).returns(nil)
Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response) InspecPlugins::Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response)
Compliance::API.version(config).must_equal({}) InspecPlugins::Compliance::API.version(config).must_equal({})
end end
end end
@ -76,8 +78,8 @@ describe Compliance::API do
response = mock response = mock
response.stubs(:code).returns('200') response.stubs(:code).returns('200')
response.stubs(:body).returns('') response.stubs(:body).returns('')
Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response) InspecPlugins::Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response)
Compliance::API.version(config).must_equal({}) InspecPlugins::Compliance::API.version(config).must_equal({})
end end
end end
@ -86,8 +88,8 @@ describe Compliance::API do
response = mock response = mock
response.stubs(:code).returns('200') response.stubs(:code).returns('200')
response.stubs(:body).returns('{"api":"compliance"}') response.stubs(:body).returns('{"api":"compliance"}')
Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response) InspecPlugins::Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response)
Compliance::API.version(config).must_equal({}) InspecPlugins::Compliance::API.version(config).must_equal({})
end end
end end
@ -96,8 +98,8 @@ describe Compliance::API do
response = mock response = mock
response.stubs(:code).returns('200') response.stubs(:code).returns('200')
response.stubs(:body).returns('{"api":"compliance","version":""}') response.stubs(:body).returns('{"api":"compliance","version":""}')
Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response) InspecPlugins::Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response)
Compliance::API.version(config).must_equal({}) InspecPlugins::Compliance::API.version(config).must_equal({})
end end
end end
@ -106,8 +108,8 @@ describe Compliance::API do
response = mock response = mock
response.stubs(:code).returns('200') response.stubs(:code).returns('200')
response.stubs(:body).returns('{"api":"compliance","version":"1.2.3"}') response.stubs(:body).returns('{"api":"compliance","version":"1.2.3"}')
Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response) InspecPlugins::Compliance::HTTP.expects(:get).with('myserver/version', 'test-headers', true).returns(response)
Compliance::API.version(config).must_equal({ 'version' => '1.2.3', 'api' => 'compliance' }) InspecPlugins::Compliance::API.version(config).must_equal({ 'version' => '1.2.3', 'api' => 'compliance' })
end end
end end
end end
@ -115,80 +117,80 @@ describe Compliance::API do
describe 'automate/compliance is? checks' do describe 'automate/compliance is? checks' do
describe 'when the config has a compliance server_type' do describe 'when the config has a compliance server_type' do
it 'automate/compliance server is? methods return correctly' do it 'automate/compliance server is? methods return correctly' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'compliance' config['server_type'] = 'compliance'
Compliance::API.is_compliance_server?(config).must_equal true InspecPlugins::Compliance::API.is_compliance_server?(config).must_equal true
Compliance::API.is_automate_server?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server?(config).must_equal false
Compliance::API.is_automate_server_pre_080?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_pre_080?(config).must_equal false
Compliance::API.is_automate_server_080_and_later?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_080_and_later?(config).must_equal false
Compliance::API.is_automate2_server?(config).must_equal false InspecPlugins::Compliance::API.is_automate2_server?(config).must_equal false
end end
end end
describe 'when the config has a automate2 server_type' do describe 'when the config has a automate2 server_type' do
it 'automate/compliance server is? methods return correctly' do it 'automate/compliance server is? methods return correctly' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'automate2' config['server_type'] = 'automate2'
Compliance::API.is_compliance_server?(config).must_equal false InspecPlugins::Compliance::API.is_compliance_server?(config).must_equal false
Compliance::API.is_automate_server?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server?(config).must_equal false
Compliance::API.is_automate_server_pre_080?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_pre_080?(config).must_equal false
Compliance::API.is_automate_server_080_and_later?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_080_and_later?(config).must_equal false
Compliance::API.is_automate2_server?(config).must_equal true InspecPlugins::Compliance::API.is_automate2_server?(config).must_equal true
end end
end end
describe 'when the config has an automate server_type and no version key' do describe 'when the config has an automate server_type and no version key' do
it 'automate/compliance server is? methods return correctly' do it 'automate/compliance server is? methods return correctly' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'automate' config['server_type'] = 'automate'
Compliance::API.is_compliance_server?(config).must_equal false InspecPlugins::Compliance::API.is_compliance_server?(config).must_equal false
Compliance::API.is_automate_server?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server?(config).must_equal true
Compliance::API.is_automate_server_pre_080?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server_pre_080?(config).must_equal true
Compliance::API.is_automate_server_080_and_later?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_080_and_later?(config).must_equal false
Compliance::API.is_automate2_server?(config).must_equal false InspecPlugins::Compliance::API.is_automate2_server?(config).must_equal false
end end
end end
describe 'when the config has an automate server_type and a version key that is not a hash' do describe 'when the config has an automate server_type and a version key that is not a hash' do
it 'automate/compliance server is? methods return correctly' do it 'automate/compliance server is? methods return correctly' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'automate' config['server_type'] = 'automate'
config['version'] = '1.2.3' config['version'] = '1.2.3'
Compliance::API.is_compliance_server?(config).must_equal false InspecPlugins::Compliance::API.is_compliance_server?(config).must_equal false
Compliance::API.is_automate_server?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server?(config).must_equal true
Compliance::API.is_automate_server_pre_080?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server_pre_080?(config).must_equal true
Compliance::API.is_automate_server_080_and_later?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_080_and_later?(config).must_equal false
Compliance::API.is_automate2_server?(config).must_equal false InspecPlugins::Compliance::API.is_automate2_server?(config).must_equal false
end end
end end
describe 'when the config has an automate server_type and a version hash with no version' do describe 'when the config has an automate server_type and a version hash with no version' do
it 'automate/compliance server is? methods return correctly' do it 'automate/compliance server is? methods return correctly' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'automate' config['server_type'] = 'automate'
config['version'] = {} config['version'] = {}
Compliance::API.is_compliance_server?(config).must_equal false InspecPlugins::Compliance::API.is_compliance_server?(config).must_equal false
Compliance::API.is_automate_server?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server?(config).must_equal true
Compliance::API.is_automate_server_pre_080?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server_pre_080?(config).must_equal true
Compliance::API.is_automate_server_080_and_later?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_080_and_later?(config).must_equal false
end end
end end
describe 'when the config has an automate server_type and a version hash with a version' do describe 'when the config has an automate server_type and a version hash with a version' do
it 'automate/compliance server is? methods return correctly' do it 'automate/compliance server is? methods return correctly' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'automate' config['server_type'] = 'automate'
config['version'] = { 'version' => '0.8.1' } config['version'] = { 'version' => '0.8.1' }
Compliance::API.is_compliance_server?(config).must_equal false InspecPlugins::Compliance::API.is_compliance_server?(config).must_equal false
Compliance::API.is_automate_server?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server?(config).must_equal true
Compliance::API.is_automate_server_pre_080?(config).must_equal false InspecPlugins::Compliance::API.is_automate_server_pre_080?(config).must_equal false
Compliance::API.is_automate_server_080_and_later?(config).must_equal true InspecPlugins::Compliance::API.is_automate_server_080_and_later?(config).must_equal true
end end
end end
end end
@ -196,54 +198,54 @@ describe Compliance::API do
describe '.server_version_from_config' do describe '.server_version_from_config' do
it 'returns nil when the config has no version key' do it 'returns nil when the config has no version key' do
config = {} config = {}
Compliance::API.server_version_from_config(config).must_be_nil InspecPlugins::Compliance::API.server_version_from_config(config).must_be_nil
end end
it 'returns nil when the version value is not a hash' do it 'returns nil when the version value is not a hash' do
config = { 'version' => '123' } config = { 'version' => '123' }
Compliance::API.server_version_from_config(config).must_be_nil InspecPlugins::Compliance::API.server_version_from_config(config).must_be_nil
end end
it 'returns nil when the version value is a hash but has no version key inside' do it 'returns nil when the version value is a hash but has no version key inside' do
config = { 'version' => {} } config = { 'version' => {} }
Compliance::API.server_version_from_config(config).must_be_nil InspecPlugins::Compliance::API.server_version_from_config(config).must_be_nil
end end
it 'returns the version if the version value is a hash containing a version' do it 'returns the version if the version value is a hash containing a version' do
config = { 'version' => { 'version' => '1.2.3' } } config = { 'version' => { 'version' => '1.2.3' } }
Compliance::API.server_version_from_config(config).must_equal '1.2.3' InspecPlugins::Compliance::API.server_version_from_config(config).must_equal '1.2.3'
end end
end end
describe 'profile_split' do describe 'profile_split' do
it 'handles a profile without version' do it 'handles a profile without version' do
Compliance::API.profile_split('admin/apache-baseline').must_equal ['admin', 'apache-baseline', nil] InspecPlugins::Compliance::API.profile_split('admin/apache-baseline').must_equal ['admin', 'apache-baseline', nil]
end end
it 'handles a profile with a version' do it 'handles a profile with a version' do
Compliance::API.profile_split('admin/apache-baseline#2.0.1').must_equal ['admin', 'apache-baseline', '2.0.1'] InspecPlugins::Compliance::API.profile_split('admin/apache-baseline#2.0.1').must_equal ['admin', 'apache-baseline', '2.0.1']
end end
end end
describe 'target_url' do describe 'target_url' do
it 'handles a automate profile with and without version' do it 'handles a automate profile with and without version' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'automate' config['server_type'] = 'automate'
config['server'] = 'https://myautomate' config['server'] = 'https://myautomate'
config['version'] = '1.6.99' config['version'] = '1.6.99'
Compliance::API.target_url(config, 'admin/apache-baseline').must_equal 'https://myautomate/profiles/admin/apache-baseline/tar' InspecPlugins::Compliance::API.target_url(config, 'admin/apache-baseline').must_equal 'https://myautomate/profiles/admin/apache-baseline/tar'
Compliance::API.target_url(config, 'admin/apache-baseline#2.0.2').must_equal 'https://myautomate/profiles/admin/apache-baseline/version/2.0.2/tar' InspecPlugins::Compliance::API.target_url(config, 'admin/apache-baseline#2.0.2').must_equal 'https://myautomate/profiles/admin/apache-baseline/version/2.0.2/tar'
end end
it 'handles a chef-compliance profile with and without version' do it 'handles a chef-compliance profile with and without version' do
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['server_type'] = 'compliance' config['server_type'] = 'compliance'
config['server'] = 'https://mychefcompliance' config['server'] = 'https://mychefcompliance'
config['version'] = '1.1.2' config['version'] = '1.1.2'
Compliance::API.target_url(config, 'admin/apache-baseline').must_equal 'https://mychefcompliance/owners/admin/compliance/apache-baseline/tar' InspecPlugins::Compliance::API.target_url(config, 'admin/apache-baseline').must_equal 'https://mychefcompliance/owners/admin/compliance/apache-baseline/tar'
Compliance::API.target_url(config, 'admin/apache-baseline#2.0.2').must_equal 'https://mychefcompliance/owners/admin/compliance/apache-baseline/tar' InspecPlugins::Compliance::API.target_url(config, 'admin/apache-baseline#2.0.2').must_equal 'https://mychefcompliance/owners/admin/compliance/apache-baseline/tar'
end end
end end
@ -253,7 +255,7 @@ describe Compliance::API do
# skipping for that specific version # skipping for that specific version
return if RUBY_VERSION = '2.3.3' return if RUBY_VERSION = '2.3.3'
config = Compliance::Configuration.new config = InspecPlugins::Compliance::Configuration.new
config.clean config.clean
config['owner'] = 'admin' config['owner'] = 'admin'
config['server_type'] = 'automate' config['server_type'] = 'automate'
@ -266,10 +268,10 @@ describe Compliance::API do
.with(headers: { 'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Chef-Delivery-Enterprise'=>'automate', 'User-Agent'=>'Ruby', 'X-Data-Collector-Token'=>'' }) .with(headers: { 'Accept'=>'*/*', 'Accept-Encoding'=>'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Chef-Delivery-Enterprise'=>'automate', 'User-Agent'=>'Ruby', 'X-Data-Collector-Token'=>'' })
.to_return(status: 200, body: profiles_response.to_json, headers: {}) .to_return(status: 200, body: profiles_response.to_json, headers: {})
Compliance::API.exist?(config, 'admin/apache-baseline').must_equal true InspecPlugins::Compliance::API.exist?(config, 'admin/apache-baseline').must_equal true
Compliance::API.exist?(config, 'admin/apache-baseline#2.0.1').must_equal true InspecPlugins::Compliance::API.exist?(config, 'admin/apache-baseline#2.0.1').must_equal true
Compliance::API.exist?(config, 'admin/apache-baseline#2.0.999').must_equal false InspecPlugins::Compliance::API.exist?(config, 'admin/apache-baseline#2.0.999').must_equal false
Compliance::API.exist?(config, 'admin/missing-in-action').must_equal false InspecPlugins::Compliance::API.exist?(config, 'admin/missing-in-action').must_equal false
end end
end end
@ -288,25 +290,25 @@ describe Compliance::API do
it 'returns `:automate2` when a 400 is received from `https://URL/dex/auth`' do it 'returns `:automate2` when a 400 is received from `https://URL/dex/auth`' do
good_response.stubs(:code).returns('400') good_response.stubs(:code).returns('400')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate2_endpoint, headers, insecure) .with(url + automate2_endpoint, headers, insecure)
.returns(good_response) .returns(good_response)
Compliance::API.determine_server_type(url, insecure).must_equal(:automate2) InspecPlugins::Compliance::API.determine_server_type(url, insecure).must_equal(:automate2)
end end
it 'returns `:automate` when a 401 is received from `https://URL/compliance/version`' do it 'returns `:automate` when a 401 is received from `https://URL/compliance/version`' do
good_response.stubs(:code).returns('401') good_response.stubs(:code).returns('401')
bad_response.stubs(:code).returns('404') bad_response.stubs(:code).returns('404')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate2_endpoint, headers, insecure) .with(url + automate2_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate_endpoint, headers, insecure) .with(url + automate_endpoint, headers, insecure)
.returns(good_response) .returns(good_response)
Compliance::API.determine_server_type(url, insecure).must_equal(:automate) InspecPlugins::Compliance::API.determine_server_type(url, insecure).must_equal(:automate)
end end
# Chef Automate currently returns 401 for `/compliance/version` but some # Chef Automate currently returns 401 for `/compliance/version` but some
@ -317,67 +319,67 @@ describe Compliance::API do
good_response.stubs(:code).returns('200') good_response.stubs(:code).returns('200')
good_response.stubs(:body).returns('Are You Looking For the Chef Server?') good_response.stubs(:body).returns('Are You Looking For the Chef Server?')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate2_endpoint, headers, insecure) .with(url + automate2_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate_endpoint, headers, insecure) .with(url + automate_endpoint, headers, insecure)
.returns(good_response) .returns(good_response)
Compliance::API.determine_server_type(url, insecure).must_equal(:automate) InspecPlugins::Compliance::API.determine_server_type(url, insecure).must_equal(:automate)
end end
it 'returns `nil` if a 200 is received from `https://URL/compliance/version` but not redirected to Chef Manage' do it 'returns `nil` if a 200 is received from `https://URL/compliance/version` but not redirected to Chef Manage' do
bad_response.stubs(:code).returns('200') bad_response.stubs(:code).returns('200')
bad_response.stubs(:body).returns('No Chef Manage here') bad_response.stubs(:body).returns('No Chef Manage here')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate_endpoint, headers, insecure) .with(url + automate_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate2_endpoint, headers, insecure) .with(url + automate2_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
mock_compliance_response = mock mock_compliance_response = mock
mock_compliance_response.stubs(:code).returns('404') mock_compliance_response.stubs(:code).returns('404')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + compliance_endpoint, headers, insecure) .with(url + compliance_endpoint, headers, insecure)
.returns(mock_compliance_response) .returns(mock_compliance_response)
Compliance::API.determine_server_type(url, insecure).must_be_nil InspecPlugins::Compliance::API.determine_server_type(url, insecure).must_be_nil
end end
it 'returns `:compliance` when a 200 is received from `https://URL/api/version`' do it 'returns `:compliance` when a 200 is received from `https://URL/api/version`' do
good_response.stubs(:code).returns('200') good_response.stubs(:code).returns('200')
bad_response.stubs(:code).returns('404') bad_response.stubs(:code).returns('404')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate_endpoint, headers, insecure) .with(url + automate_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate2_endpoint, headers, insecure) .with(url + automate2_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + compliance_endpoint, headers, insecure) .with(url + compliance_endpoint, headers, insecure)
.returns(good_response) .returns(good_response)
Compliance::API.determine_server_type(url, insecure).must_equal(:compliance) InspecPlugins::Compliance::API.determine_server_type(url, insecure).must_equal(:compliance)
end end
it 'returns `nil` if it cannot determine the server type' do it 'returns `nil` if it cannot determine the server type' do
bad_response.stubs(:code).returns('404') bad_response.stubs(:code).returns('404')
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate2_endpoint, headers, insecure) .with(url + automate2_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + automate_endpoint, headers, insecure) .with(url + automate_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::HTTP.expects(:get) InspecPlugins::Compliance::HTTP.expects(:get)
.with(url + compliance_endpoint, headers, insecure) .with(url + compliance_endpoint, headers, insecure)
.returns(bad_response) .returns(bad_response)
Compliance::API.determine_server_type(url, insecure).must_be_nil InspecPlugins::Compliance::API.determine_server_type(url, insecure).must_be_nil
end end
end end
end end

View file

@ -1,10 +1,12 @@
require 'helper' require 'minitest/autorun'
require 'mocha/setup'
require_relative '../../lib/inspec-compliance/api.rb'
describe Compliance::Fetcher do describe InspecPlugins::Compliance::Fetcher do
let(:config) { { 'server' => 'myserver' } } let(:config) { { 'server' => 'myserver' } }
describe 'the check_compliance_token method' do describe 'the check_compliance_token method' do
let(:fetcher) { fetcher = Compliance::Fetcher.new('a/bad/url', config) } let(:fetcher) { fetcher = InspecPlugins::Compliance::Fetcher.new('a/bad/url', config) }
it 'returns without error if token is set' do it 'returns without error if token is set' do
config['token'] = 'my-token' config['token'] = 'my-token'
@ -18,59 +20,59 @@ describe Compliance::Fetcher do
end end
describe 'when the server is an automate2 server' do describe 'when the server is an automate2 server' do
before { Compliance::API.expects(:is_automate2_server?).with(config).returns(true) } before { InspecPlugins::Compliance::API.expects(:is_automate2_server?).with(config).returns(true) }
it 'returns the correct owner and profile name' do it 'returns the correct owner and profile name' do
config['profile'] = ['admin', 'ssh-baseline', nil] config['profile'] = ['admin', 'ssh-baseline', nil]
fetcher = Compliance::Fetcher.new('myserver/profile', config) fetcher = InspecPlugins::Compliance::Fetcher.new('myserver/profile', config)
fetcher.send(:compliance_profile_name).must_equal 'admin/ssh-baseline' fetcher.send(:compliance_profile_name).must_equal 'admin/ssh-baseline'
end end
end end
describe 'when the server is an automate server pre-0.8.0' do describe 'when the server is an automate server pre-0.8.0' do
before { Compliance::API.expects(:is_automate_server_pre_080?).with(config).returns(true) } before { InspecPlugins::Compliance::API.expects(:is_automate_server_pre_080?).with(config).returns(true) }
it 'returns the correct profile name when the url is correct' do it 'returns the correct profile name when the url is correct' do
fetcher = Compliance::Fetcher.new('myserver/myowner/myprofile/tar', config) fetcher = InspecPlugins::Compliance::Fetcher.new('myserver/myowner/myprofile/tar', config)
fetcher.send(:compliance_profile_name).must_equal 'myowner/myprofile' fetcher.send(:compliance_profile_name).must_equal 'myowner/myprofile'
end end
it 'raises an exception if the url is malformed' do it 'raises an exception if the url is malformed' do
fetcher = Compliance::Fetcher.new('a/bad/url', config) fetcher = InspecPlugins::Compliance::Fetcher.new('a/bad/url', config)
proc { fetcher.send(:compliance_profile_name) }.must_raise RuntimeError proc { fetcher.send(:compliance_profile_name) }.must_raise RuntimeError
end end
end end
describe 'when the server is an automate server 0.8.0-or-later' do describe 'when the server is an automate server 0.8.0-or-later' do
before do before do
Compliance::API.expects(:is_automate_server_pre_080?).with(config).returns(false) InspecPlugins::Compliance::API.expects(:is_automate_server_pre_080?).with(config).returns(false)
Compliance::API.expects(:is_automate_server_080_and_later?).with(config).returns(true) InspecPlugins::Compliance::API.expects(:is_automate_server_080_and_later?).with(config).returns(true)
end end
it 'returns the correct profile name when the url is correct' do it 'returns the correct profile name when the url is correct' do
fetcher = Compliance::Fetcher.new('myserver/profiles/myowner/myprofile/tar', config) fetcher = InspecPlugins::Compliance::Fetcher.new('myserver/profiles/myowner/myprofile/tar', config)
fetcher.send(:compliance_profile_name).must_equal 'myowner/myprofile' fetcher.send(:compliance_profile_name).must_equal 'myowner/myprofile'
end end
it 'raises an exception if the url is malformed' do it 'raises an exception if the url is malformed' do
fetcher = Compliance::Fetcher.new('a/bad/url', config) fetcher = InspecPlugins::Compliance::Fetcher.new('a/bad/url', config)
proc { fetcher.send(:compliance_profile_name) }.must_raise RuntimeError proc { fetcher.send(:compliance_profile_name) }.must_raise RuntimeError
end end
end end
describe 'when the server is not an automate server (likely a compliance server)' do describe 'when the server is not an automate server (likely a compliance server)' do
before do before do
Compliance::API.expects(:is_automate_server_pre_080?).with(config).returns(false) InspecPlugins::Compliance::API.expects(:is_automate_server_pre_080?).with(config).returns(false)
Compliance::API.expects(:is_automate_server_080_and_later?).with(config).returns(false) InspecPlugins::Compliance::API.expects(:is_automate_server_080_and_later?).with(config).returns(false)
end end
it 'returns the correct profile name when the url is correct' do it 'returns the correct profile name when the url is correct' do
fetcher = Compliance::Fetcher.new('myserver/owners/myowner/compliance/myprofile/tar', config) fetcher = InspecPlugins::Compliance::Fetcher.new('myserver/owners/myowner/compliance/myprofile/tar', config)
fetcher.send(:compliance_profile_name).must_equal 'myowner/myprofile' fetcher.send(:compliance_profile_name).must_equal 'myowner/myprofile'
end end
it 'raises an exception if the url is malformed' do it 'raises an exception if the url is malformed' do
fetcher = Compliance::Fetcher.new('a/bad/url', config) fetcher = InspecPlugins::Compliance::Fetcher.new('a/bad/url', config)
proc { fetcher.send(:compliance_profile_name) }.must_raise RuntimeError proc { fetcher.send(:compliance_profile_name) }.must_raise RuntimeError
end end
end end
@ -95,24 +97,24 @@ describe Compliance::Fetcher do
'latest_version'=>'' }] 'latest_version'=>'' }]
end end
before do before do
Compliance::Configuration.expects(:new).returns({ 'token' => '123abc', 'server' => 'https://a2.instance.com' }) InspecPlugins::Compliance::Configuration.expects(:new).returns({ 'token' => '123abc', 'server' => 'https://a2.instance.com' })
end end
it 'returns the correct profile name when parsing url' do it 'returns the correct profile name when parsing url' do
Compliance::API.stubs(:profiles).returns(['success', profiles_result]) InspecPlugins::Compliance::API.stubs(:profiles).returns(['success', profiles_result])
fetcher = Compliance::Fetcher.resolve('compliance://admin/ssh-baseline') fetcher = InspecPlugins::Compliance::Fetcher.resolve('compliance://admin/ssh-baseline')
assert = ['admin', 'ssh-baseline', nil] assert = ['admin', 'ssh-baseline', nil]
fetcher.instance_variable_get(:"@config")['profile'].must_equal assert fetcher.instance_variable_get(:"@config")['profile'].must_equal assert
end end
it 'returns the correct profile name when parsing compliance hash' do it 'returns the correct profile name when parsing compliance hash' do
Compliance::API.stubs(:profiles).returns(['success', profiles_result]) InspecPlugins::Compliance::API.stubs(:profiles).returns(['success', profiles_result])
hash = { hash = {
target: 'https://a2.instance.com/api/v0/compliance/tar', target: 'https://a2.instance.com/api/v0/compliance/tar',
compliance: 'admin/ssh-baseline', compliance: 'admin/ssh-baseline',
sha256: '132j1kjdasfasdoaefaewo12312', sha256: '132j1kjdasfasdoaefaewo12312',
} }
fetcher = Compliance::Fetcher.resolve(hash) fetcher = InspecPlugins::Compliance::Fetcher.resolve(hash)
assert = ['admin', 'ssh-baseline', nil] assert = ['admin', 'ssh-baseline', nil]
fetcher.instance_variable_get(:"@config")['profile'].must_equal assert fetcher.instance_variable_get(:"@config")['profile'].must_equal assert
end end
@ -139,14 +141,14 @@ describe Compliance::Fetcher do
end end
before do before do
Compliance::Configuration.expects(:new).returns({ 'token' => '123abc', 'server' => 'https://a2.instance.com' }) InspecPlugins::Compliance::Configuration.expects(:new).returns({ 'token' => '123abc', 'server' => 'https://a2.instance.com' })
end end
it 'contains the upstream_sha256' do it 'contains the upstream_sha256' do
Compliance::API.stubs(:profiles).returns(['success', profiles_result]) InspecPlugins::Compliance::API.stubs(:profiles).returns(['success', profiles_result])
prof = profiles_result[0] prof = profiles_result[0]
target = "compliance://#{prof['owner']}/#{prof['name']}" target = "compliance://#{prof['owner']}/#{prof['name']}"
fetcher = Compliance::Fetcher.resolve(target) fetcher = InspecPlugins::Compliance::Fetcher.resolve(target)
fetcher.upstream_sha256.must_equal prof['sha256'] fetcher.upstream_sha256.must_equal prof['sha256']
end end
end end

View file

@ -1,53 +0,0 @@
# encoding: utf-8
# author: Dominik Richter
# author: Christoph Hartmann
require 'functional/helper'
# basic testing without availability of any server
describe 'inspec compliance' do
include FunctionalHelper
it 'help' do
out = inspec('compliance help')
out.exit_status.must_equal 0
out.stdout.must_include 'inspec compliance exec PROFILE'
end
# ensure we are logged out
it 'logout' do
out = inspec('compliance logout')
out.exit_status.must_equal 0
out.stdout.must_include ''
end
it 'login server url missing' do
out = inspec('compliance login')
out.exit_status.must_equal 1
out.stderr.must_include 'ERROR: "inspec compliance login" was called with no arguments'
end
it 'inspec compliance profiles without authentication' do
out = inspec('compliance profile')
out.stdout.must_include 'You need to login first with `inspec compliance login`'
out.exit_status.must_equal 0
end
it 'try to upload a profile without directory' do
out = inspec('compliance upload')
out.stderr.must_include 'ERROR: "inspec compliance upload" was called with no arguments'
out.exit_status.must_equal 1
end
it 'try to upload a profile a non-existing path' do
out = inspec('compliance upload /path/to/dir')
out.stdout.must_include 'You need to login first with `inspec compliance login`'
out.exit_status.must_equal 0
end
it 'logout' do
out = inspec('compliance logout')
out.exit_status.must_equal 0
out.stdout.must_include ''
end
end

View file

@ -37,7 +37,7 @@ require 'inspec/impact'
require 'fetchers/mock' require 'fetchers/mock'
require 'inspec/dependencies/cache' require 'inspec/dependencies/cache'
require_relative '../lib/bundles/inspec-compliance' require_relative '../lib/bundles/inspec-supermarket'
require 'train' require 'train'
CMD = Train.create('local', command_runner: :generic).connection CMD = Train.create('local', command_runner: :generic).connection

View file

@ -31,7 +31,7 @@ describe 'BaseCLI' do
default_options = { mock: { compliance: 'mock' } } default_options = { mock: { compliance: 'mock' } }
Inspec::BaseCLI.stubs(:default_options).returns(default_options) Inspec::BaseCLI.stubs(:default_options).returns(default_options)
Compliance::API.expects(:login).with('mock') InspecPlugins::Compliance::API.expects(:login).with('mock')
cli.send(:opts, :mock) cli.send(:opts, :mock)
end end

View file

@ -25,11 +25,11 @@ class PluginLoaderTests < MiniTest::Test
@config_dir_path = File.join(mock_path, 'config_dirs') @config_dir_path = File.join(mock_path, 'config_dirs')
@bundled_plugins = [ @bundled_plugins = [
:'inspec-compliance',
:'inspec-supermarket', :'inspec-supermarket',
] ]
@core_plugins = [ @core_plugins = [
:'inspec-artifact', :'inspec-artifact',
:'inspec-compliance',
:'inspec-habitat', :'inspec-habitat',
:'inspec-init', :'inspec-init',
] ]