2016-02-05 07:38:45 +00:00
# encoding: utf-8
# author: Christoph Hartmann
# author: Dominik Richter
require 'thor'
2016-03-23 15:40:01 +00:00
require 'erb'
2016-02-05 07:38:45 +00:00
module Compliance
2016-02-05 10:06:00 +00:00
class ComplianceCLI < Inspec :: BaseCLI # rubocop:disable Metrics/ClassLength
2016-02-05 07:38:45 +00:00
namespace 'compliance'
2016-08-18 18:10:09 +00:00
# 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 SERVER --insecure --user='USER' --token='TOKEN' " , 'Log in to a Chef Compliance SERVER'
2016-03-01 19:51:23 +00:00
option :insecure , aliases : :k , type : :boolean ,
desc : 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
2016-04-08 12:03:41 +00:00
option :user , type : :string , required : false ,
2016-09-19 14:00:35 +00:00
desc : 'Chef Compliance Username'
2016-04-08 12:03:41 +00:00
option :password , type : :string , required : false ,
2016-09-19 14:00:35 +00:00
desc : 'Chef Compliance Password'
2016-03-14 14:08:27 +00:00
option :apipath , type : :string , default : '/api' ,
desc : 'Set the path to the API, defaults to /api'
2016-04-08 12:03:41 +00:00
option :token , type : :string , required : false ,
2016-03-23 13:32:10 +00:00
desc : 'Chef Compliance access token'
2016-04-08 12:03:41 +00:00
option :refresh_token , type : :string , required : false ,
desc : 'Chef Compliance refresh token'
2016-08-16 15:21:13 +00:00
def login ( server ) # rubocop:disable Metrics/AbcSize
2016-04-13 11:47:33 +00:00
# show warning if the Compliance Server does not support
2016-04-08 12:03:41 +00:00
options [ 'server' ] = server
url = options [ 'server' ] + options [ 'apipath' ]
2016-11-15 19:19:39 +00:00
2016-04-08 12:03:41 +00:00
if ! options [ 'user' ] . nil? && ! options [ 'password' ] . nil?
# username / password
2016-09-19 14:00:35 +00:00
_success , msg = login_username_password ( url , options [ 'user' ] , options [ 'password' ] , options [ 'insecure' ] )
2016-04-13 11:47:33 +00:00
elsif ! options [ 'user' ] . nil? && ! options [ 'token' ] . nil?
2016-04-08 12:03:41 +00:00
# access token
2016-08-18 00:00:26 +00:00
_success , msg = store_access_token ( url , options [ 'user' ] , options [ 'token' ] , options [ 'insecure' ] )
2016-04-08 12:03:41 +00:00
elsif ! options [ 'refresh_token' ] . nil? && ! options [ 'user' ] . nil?
# refresh token
2016-08-18 00:00:26 +00:00
_success , msg = store_refresh_token ( url , options [ 'refresh_token' ] , true , options [ 'user' ] , options [ 'insecure' ] )
2016-04-13 11:47:33 +00:00
# TODO: we should login with the refreshtoken here
elsif ! options [ 'refresh_token' ] . nil?
2016-08-18 00:00:26 +00:00
_success , msg = login_refreshtoken ( url , options )
2016-04-04 15:29:13 +00:00
else
2016-08-18 17:48:05 +00:00
puts 'Please run `inspec compliance login SERVER` with options --token or --refresh_token, --user, and --insecure or --not-insecure'
2016-04-13 11:47:33 +00:00
exit 1
2016-04-04 15:29:13 +00:00
end
2016-08-18 00:00:26 +00:00
puts '' , msg
2016-02-05 07:38:45 +00:00
end
2016-11-15 19:19:39 +00:00
desc " login_automate SERVER --user='USER' --ent='ENT' --dctoken or --usertoken='TOKEN' " , 'Log in to an Automate SERVER'
option :dctoken , type : :string ,
desc : 'Data Collector token'
option :usertoken , type : :string ,
desc : 'Automate user token'
option :user , type : :string ,
desc : 'Automate username'
option :ent , type : :string ,
desc : 'Enterprise for Chef Automate reporting'
2016-11-30 14:30:11 +00:00
option :insecure , aliases : :k , type : :boolean ,
desc : 'Explicitly allows InSpec to perform "insecure" SSL connections and transfers'
2016-11-15 19:19:39 +00:00
def login_automate ( server ) # rubocop:disable Metrics/AbcSize
options [ 'server' ] = server
url = options [ 'server' ] + '/compliance/profiles'
if url && ! options [ 'user' ] . nil? && ! options [ 'ent' ] . nil?
if ! options [ 'dctoken' ] . nil? || ! options [ 'usertoken' ] . nil?
2016-11-30 14:30:11 +00:00
msg = login_automate_config ( url , options [ 'user' ] , options [ 'dctoken' ] , options [ 'usertoken' ] , options [ 'ent' ] , options [ 'insecure' ] )
2016-11-15 19:19:39 +00:00
else
puts " Please specify a token using --dctoken='DATA_COLLECTOR_TOKEN' or usertoken='AUTOMATE_TOKEN' "
exit 1
end
else
2016-11-29 14:35:16 +00:00
puts " Please login to your automate instance using 'inspec compliance login_automate SERVER --user AUTOMATE_USER --ent AUTOMATE_ENT --dctoken DC_TOKEN or --usertoken USER_TOKEN' "
2016-11-15 19:19:39 +00:00
exit 1
end
puts '' , msg
end
2016-02-05 07:38:45 +00:00
desc 'profiles' , 'list all available profiles in Chef Compliance'
def profiles
2016-04-13 11:47:33 +00:00
config = Compliance :: Configuration . new
2016-04-29 00:20:25 +00:00
return if ! loggedin ( config )
2016-08-17 16:15:11 +00:00
msg , profiles = Compliance :: API . profiles ( config )
2016-02-05 07:38:45 +00:00
if ! profiles . empty?
# iterate over profiles
2016-02-05 10:06:00 +00:00
headline ( 'Available profiles:' )
2016-02-05 07:38:45 +00:00
profiles . each { | profile |
2016-02-05 10:06:00 +00:00
li ( " #{ profile [ :org ] } / #{ profile [ :name ] } " )
2016-02-05 07:38:45 +00:00
}
else
2016-08-17 16:15:11 +00:00
puts msg , 'Could not find any profiles'
2016-08-18 16:34:09 +00:00
exit 1
2016-02-05 07:38:45 +00:00
end
end
desc 'exec PROFILE' , 'executes a Chef Compliance profile'
2016-03-06 14:07:12 +00:00
exec_options
2016-02-05 07:38:45 +00:00
def exec ( * tests )
2016-04-29 00:20:25 +00:00
config = Compliance :: Configuration . new
return if ! loggedin ( config )
2016-02-05 07:38:45 +00:00
# iterate over tests and add compliance scheme
tests = tests . map { | t | 'compliance://' + t }
# execute profile from inspec exec implementation
diagnose
2016-02-21 22:17:01 +00:00
run_tests ( tests , opts )
2016-02-05 07:38:45 +00:00
end
2017-01-05 11:37:43 +00:00
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 )
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
2016-02-05 07:38:45 +00:00
desc 'upload PATH' , 'uploads a local profile to Chef Compliance'
2016-02-05 10:06:00 +00:00
option :overwrite , type : :boolean , default : false ,
desc : 'Overwrite existing profile on Chef Compliance.'
2016-04-29 00:20:25 +00:00
def upload ( path ) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, PerceivedComplexity, Metrics/CyclomaticComplexity
config = Compliance :: Configuration . new
return if ! loggedin ( config )
2016-04-13 14:54:29 +00:00
unless File . exist? ( path )
puts " Directory #{ path } does not exist. "
exit 1
end
2016-12-01 13:33:35 +00:00
vendor_deps ( path , options ) if File . directory? ( path )
2016-11-29 02:08:10 +00:00
2016-02-05 10:06:00 +00:00
o = options . dup
configure_logger ( o )
# check the profile, we only allow to upload valid profiles
2016-02-22 20:11:49 +00:00
profile = Inspec :: Profile . for_target ( path , o )
2016-02-05 10:06:00 +00:00
# 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 [ 'user' ] . nil?
error . call ( 'Please login via `inspec compliance login`' )
end
# owner
owner = config [ 'user' ]
# read profile name from inspec.yml
profile_name = profile . params [ :name ]
# check that the profile is not uploaded already,
# confirm upload to the user (overwrite with --force)
2016-04-13 11:47:33 +00:00
if Compliance :: API . exist? ( config , " #{ owner } / #{ profile_name } " ) && ! options [ 'overwrite' ]
2016-02-05 10:06:00 +00:00
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
2016-02-05 07:38:45 +00:00
2016-02-05 10:06:00 +00:00
# if it is a directory, tar it to tmp directory
if File . directory? ( path )
2016-03-23 15:13:23 +00:00
archive_path = Dir :: Tmpname . create ( [ profile_name , '.tar.gz' ] ) { }
2016-02-05 10:06:00 +00:00
puts " Generate temporary profile archive at #{ archive_path } "
2016-03-23 15:13:23 +00:00
profile . archive ( { output : archive_path , ignore_errors : false , overwrite : true } )
2016-02-05 10:06:00 +00:00
else
archive_path = path
end
puts " Start upload to #{ owner } / #{ profile_name } "
2016-03-23 15:40:01 +00:00
pname = ERB :: Util . url_encode ( profile_name )
2016-02-05 10:06:00 +00:00
2016-11-29 14:35:16 +00:00
config [ 'server_type' ] == 'automate' ? upload_msg = 'Uploading to Chef Automate' : upload_msg = 'Uploading to Chef Compliance'
2016-11-15 19:19:39 +00:00
puts upload_msg
2016-04-13 11:47:33 +00:00
success , msg = Compliance :: API . upload ( config , owner , pname , archive_path )
2016-02-05 07:38:45 +00:00
2016-02-05 10:06:00 +00:00
if success
2016-02-05 07:38:45 +00:00
puts 'Successfully uploaded profile'
else
2016-02-05 10:06:00 +00:00
puts 'Error during profile upload:'
puts msg
2016-08-18 16:34:09 +00:00
exit 1
2016-02-05 07:38:45 +00:00
end
end
desc 'version' , 'displays the version of the Chef Compliance server'
def version
2016-04-04 15:29:13 +00:00
config = Compliance :: Configuration . new
2016-11-29 14:35:16 +00:00
if config [ 'server_type' ] == 'automate'
2016-11-15 19:19:39 +00:00
puts 'Version not available when logged in with Automate.'
2016-02-05 07:38:45 +00:00
else
2016-11-15 19:19:39 +00:00
info = Compliance :: API . version ( config [ 'server' ] , config [ 'insecure' ] )
if ! info . nil? && info [ 'version' ]
puts " Chef Compliance version: #{ info [ 'version' ] } "
else
puts 'Could not determine server version.'
exit 1
end
2016-02-05 07:38:45 +00:00
end
end
desc 'logout' , 'user logout from Chef Compliance'
def logout
2016-04-13 14:54:29 +00:00
config = Compliance :: Configuration . new
2016-11-29 14:35:16 +00:00
unless config . supported? ( :oidc ) || config [ 'token' ] . nil? || config [ 'server_type' ] == 'automate'
2016-04-13 11:47:33 +00:00
config = Compliance :: Configuration . new
url = " #{ config [ 'server' ] } /logout "
Compliance :: API . post ( url , config [ 'token' ] , config [ 'insecure' ] , ! config . supported? ( :oidc ) )
2016-04-04 15:29:13 +00:00
end
2016-04-13 11:47:33 +00:00
success = config . destroy
2016-04-04 15:29:13 +00:00
if success
2016-02-05 07:38:45 +00:00
puts 'Successfully logged out'
else
puts 'Could not log out'
end
end
2016-04-13 11:47:33 +00:00
private
2016-11-30 14:30:11 +00:00
def login_automate_config ( url , user , dctoken , usertoken , ent , insecure ) # rubocop:disable Metrics/ParameterLists
2016-11-15 19:19:39 +00:00
config = Compliance :: Configuration . new
config [ 'user' ] = user
2016-11-29 14:35:16 +00:00
config [ 'server' ] = url
config [ 'automate' ] = { }
config [ 'automate' ] [ 'ent' ] = ent
config [ 'server_type' ] = 'automate'
2016-11-30 14:30:11 +00:00
config [ 'insecure' ] = insecure
2016-11-15 19:19:39 +00:00
# determine token method being used
if ! dctoken . nil?
config [ 'token' ] = dctoken
token_type = 'dctoken'
token_msg = 'data collector token'
else
config [ 'token' ] = usertoken
token_type = 'usertoken'
token_msg = 'automate user token'
end
2016-11-29 14:35:16 +00:00
config [ 'automate' ] [ 'token_type' ] = token_type
2016-11-15 19:19:39 +00:00
config . store
2016-11-30 14:30:11 +00:00
msg = " Stored configuration for Chef Automate: ' #{ url } ' with user: ' #{ user } ', ent: ' #{ ent } ' and your #{ token_msg } "
2016-11-15 19:19:39 +00:00
msg
end
2016-04-13 11:47:33 +00:00
def login_refreshtoken ( url , options )
2016-09-19 14:00:35 +00:00
success , msg , access_token = Compliance :: API . get_token_via_refresh_token ( url , options [ 'refresh_token' ] , options [ 'insecure' ] )
2016-04-13 11:47:33 +00:00
if success
config = Compliance :: Configuration . new
config [ 'server' ] = url
config [ 'token' ] = access_token
config [ 'insecure' ] = options [ 'insecure' ]
config [ 'version' ] = Compliance :: API . version ( url , options [ 'insecure' ] )
2016-11-29 14:35:16 +00:00
config [ 'server_type' ] = 'compliance'
2016-04-13 11:47:33 +00:00
config . store
end
[ success , msg ]
end
2016-09-19 14:00:35 +00:00
def login_username_password ( url , username , password , insecure )
2016-04-13 11:47:33 +00:00
config = Compliance :: Configuration . new
2016-09-19 14:00:35 +00:00
success , msg , api_token = Compliance :: API . get_token_via_password ( url , username , password , insecure )
if success
config [ 'server' ] = url
config [ 'user' ] = username
config [ 'token' ] = api_token
config [ 'insecure' ] = insecure
config [ 'version' ] = Compliance :: API . version ( url , insecure )
2016-11-29 14:35:16 +00:00
config [ 'server_type' ] = 'compliance'
2016-09-19 14:00:35 +00:00
config . store
success = true
2016-04-13 11:47:33 +00:00
end
[ success , msg ]
end
# saves a user access token (limited time)
def store_access_token ( url , user , token , insecure )
config = Compliance :: Configuration . new
config [ 'server' ] = url
config [ 'insecure' ] = insecure
config [ 'user' ] = user
config [ 'token' ] = token
config [ 'version' ] = Compliance :: API . version ( url , insecure )
config . store
2016-09-19 14:00:35 +00:00
[ true , 'API access token stored' ]
2016-04-13 11:47:33 +00:00
end
2016-09-19 14:00:35 +00:00
# saves a refresh token supplied by the user
2016-04-13 11:47:33 +00:00
def store_refresh_token ( url , refresh_token , verify , user , insecure )
config = Compliance :: Configuration . new
config [ 'server' ] = url
config [ 'refresh_token' ] = refresh_token
config [ 'user' ] = user
config [ 'insecure' ] = insecure
config [ 'version' ] = Compliance :: API . version ( url , insecure )
if ! verify
config . store
success = true
2016-09-19 14:00:35 +00:00
msg = 'API refresh token stored'
2016-04-13 11:47:33 +00:00
else
2016-09-19 14:00:35 +00:00
success , msg , access_token = Compliance :: API . get_token_via_refresh_token ( url , refresh_token , insecure )
2016-04-13 11:47:33 +00:00
if success
config [ 'token' ] = access_token
config . store
2016-09-19 14:00:35 +00:00
msg = 'API access token verified and stored'
2016-04-13 11:47:33 +00:00
end
end
[ success , msg ]
end
2016-04-29 00:20:25 +00:00
def loggedin ( config )
serverknown = ! config [ 'server' ] . nil?
2016-11-29 14:35:16 +00:00
puts 'You need to login first with `inspec compliance login` or `inspec compliance login_automate`' if ! serverknown
2016-04-29 00:20:25 +00:00
serverknown
end
2016-02-05 07:38:45 +00:00
end
2016-02-05 13:48:55 +00:00
# register the subcommand to Inspec CLI registry
2016-02-08 21:25:07 +00:00
Inspec :: Plugins :: CLI . add_subcommand ( ComplianceCLI , 'compliance' , 'compliance SUBCOMMAND ...' , 'Chef Compliance commands' , { } )
2016-02-05 07:38:45 +00:00
end