Merge pull request #3818 from inspec/ja/rewrite-habitat-plugin

Rewrite inspec-habitat plugin
This commit is contained in:
Clinton Wolfe 2019-03-08 09:51:46 -05:00 committed by GitHub
commit 74e2f72ed5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 923 additions and 435 deletions

View file

@ -0,0 +1,5 @@
source 'https://supermarket.chef.io'
group :integration do
cookbook 'inspec_habitat_fixture', path: 'test/cookbooks/inspec_habitat_fixture/'
end

View file

@ -0,0 +1,150 @@
# InSpec Habitat Plugin
## Summary
This plugin allows you to do the following:
1. Add Habitat configuration to a profile
2. Create/Upload a Habitat package from an InSpec profile
Creating a [Habitat](https://www.habitat.sh/) package from an InSpec profile
allows you to execute that profile as a service (via a Habitat Supervisor) on
any Linux based platform.
When running as a service, an InSpec report will be created in JSON format (by
default at `/hab/svc/YOUR_SERVICE/logs/inspec_last_run.json`). Additionally, a
log of the last run will be located at
`/hab/svc/YOUR_SERVICE/logs/inspec_log.txt` and CLI output is viewable in
the Supervisor logs by default. You can also configure this service to report
to [Chef Automate](https://www.chef.io/automate/).
See below for usage instructions.
## Plugin Usage
### Adding Habitat Configuration to an InSpec Profile
Run the following command:
```
inspec habitat profile setup PATH
```
This will create the following files:
- habitat/plan.sh (Provides build time instructions to Habitat)
- habitat/default.toml (Used to configure the running Habitat service)
- habitat/hooks/run (Shell script to execute this profile as a service)
- habitat/config/inspec_exec_config.json (JSON for `inspec exec` CLI options)
### Creating a Habitat Package
> This command requires Habitat to be installed and configured. For instructions
on how to do that see [here](https://www.habitat.sh/docs/install-habitat/).
Run the following command:
```
inspec habitat profile create PATH
```
This command will:
- Create a Habitat artifact (`.hart` file).
> NOTE: If you are fetching packages from Chef Automate see
[below](#Integrating-with-Chef-Automate).
### Uploading a Habitat Package
> This command requires Habitat to be installed and configured. For instructions
on how to do that see [here](https://www.habitat.sh/docs/install-habitat/).
Run the following command:
```
inspec habitat profile upload PATH
```
This command will:
- Create a Habitat artifact (`.hart` file).
- Upload the Habitat artifact to [bldr.habitat.sh](bldr.habitat.sh).
> NOTE: If you are fetching packages from Chef Automate see
[below](#Integrating-with-Chef-Automate).
## Habitat Package Usage
> This command requires Habitat to be installed and configured. For instructions
on how to do that see [here](https://www.habitat.sh/docs/install-habitat/).
General usage instructions for using Habitat packages can be found
[here](https://www.habitat.sh/docs/using-habitat/#Using-Habitat-Packages).
Installing the package from a HART file:
```
# See Habitat docs for more info. The below is for testing only.
hab pkg install PATH_TO_CREATED_HART_FILE
hab sup run YOUR_ORIGIN/inspec-profile-YOUR_PROFILE_NAME
```
Installing the package from the Public Builder Depot:
```
# See Habitat docs for more info. The below is for testing only.
hab pkg install YOUR_ORIGIN/inspec-profile-YOUR_PROFILE_NAME
hab sup run YOUR_ORIGIN/inspec-profile-YOUR_PROFILE_NAME
```
## Integrating with Chef Automate
### Fetching Profiles from Chef Automate During Build
Fetching profiles from Chef Automate requires authentication.
Run the following commands prior to creating/uploading your Habitat package:
```
# Remove -k if you are not using a self-signed certificate
inspec compliance login -k --user USER --token API_TOKEN https://AUTOMATE_FQDN
export HAB_STUDIO_SECRET_COMPLIANCE_CREDS=$(cat ~/.inspec/compliance/config.json)
```
### Sending InSpec Reports to Chef Automate
After running your Habitat package as a service you can configure it to report
to Chef Automate via a
[configuration update](https://www.habitat.sh/docs/using-habitat/#config-updates).
For example, create a TOML file (config.toml) that matches the below:
```
[automate]
url = 'https://chef-automate.test'
token = 'TOKEN'
user = 'admin'
```
Then apply it like so:
```
# The '1' here is the config version (increment this with each change)
hab config apply inspec-profile-PROFILE_NAME.default 1 /path/to/config.toml
```
This will apply the configuration to all services in the service group. For
more info on service groups see the
[Habitat docs](https://www.habitat.sh/docs/using-habitat/#service-groups)
## Testing
Lint, unit, and functional tests are ran from the root of the InSpec source:
```
bundle exec rake test
```
To execute the integration tests (Test Kitchen + Vagrant + VirtualBox) run the
following from the directory containing this README.md:
```
bundle exec kitchen test
```

View file

@ -0,0 +1,28 @@
---
driver:
name: vagrant
provisioner:
name: chef_solo
sudo: true
verifier:
name: inspec
platforms:
- name: ubuntu-18.04
lifecycle:
# Build the InSpec gem so it is available to install during `kitchen converge`
pre_create:
- cd ../../../ && gem build inspec.gemspec
- mv ../../../inspec-*.gem test/cookbooks/inspec_habitat_fixture/files/inspec-local.gem
post_converge:
- local: sleep 10 # Wait for Habitat to load/run hab service before `verify`
suites:
- name: default
run_list:
- recipe[inspec_habitat_fixture]
attributes:

View file

@ -10,21 +10,21 @@ module InspecPlugins
"#{basename} habitat profile #{command.usage}"
end
desc 'create PATH', 'Create a one-time Habitat artifact for the profile found at PATH'
desc 'create PATH', 'Create a Habitat artifact for the profile found at PATH'
option :output_dir, type: :string, required: false,
desc: 'Directory in which to save the generated Habitat artifact. Default: current directory'
def create(path)
InspecPlugins::Habitat::Profile.create(path, options)
desc: 'Output directory for the Habitat artifact. Default: current directory'
def create(path = '.')
InspecPlugins::Habitat::Profile.new(path, options).create
end
desc 'setup PATH', 'Configure the profile at PATH for Habitat, including a plan and hooks'
def setup(path)
InspecPlugins::Habitat::Profile.setup(path)
def setup(path = '.')
InspecPlugins::Habitat::Profile.new(path, options).setup
end
desc 'upload PATH', 'Create a one-time Habitat artifact for the profile found at PATH, and upload it to a Habitat Depot'
def upload(path)
InspecPlugins::Habitat::Profile.upload(path, options)
desc 'upload PATH', 'Create then upload a Habitat artifact for the profile found at PATH to the Habitat Builder Depot'
def upload(path = '.')
InspecPlugins::Habitat::Profile.new(path, options).upload
end
end

View file

@ -1,238 +1,226 @@
# encoding: utf-8
# author: Adam Leff
require 'inspec/profile_vendor'
require 'mixlib/shellout'
require 'tomlrb'
require 'ostruct'
module InspecPlugins
module Habitat
class Profile
attr_reader :options, :path, :profile
def self.create(path, options = {})
creator = new(path, options)
hart_file = creator.create
creator.copy(hart_file)
ensure
creator.delete_work_dir
end
def self.setup(path)
new(path).setup
end
def self.upload(path, options = {})
uploader = new(path, options)
uploader.upload
ensure
uploader.delete_work_dir
end
attr_reader :logger
def initialize(path, options = {})
@path = path
@options = options
@cli_config = nil
log_level = options.fetch('log_level', 'info')
@log = Inspec::Log
@log.level(log_level.to_sym)
@logger = Inspec::Log
logger.level(options.fetch(:log_level, 'info').to_sym)
end
def create
@log.info("Creating a Habitat artifact for profile: #{path}")
logger.info("Creating a Habitat artifact for '#{@path}'...")
validate_habitat_installed
validate_habitat_origin
create_profile_object
verify_profile
vendor_profile_dependencies
copy_profile_to_work_dir
create_habitat_directories(work_dir)
create_plan(work_dir)
create_run_hook(work_dir)
create_default_config(work_dir)
# Need to create working directory first so `ensure` doesn't error
working_dir = create_working_dir
# returns the path to the .hart file in the work directory
build_hart
habitat_config = read_habitat_config
verify_habitat_setup(habitat_config)
output_dir = @options[:output_dir] || Dir.pwd
unless File.directory?(output_dir)
exit_with_error("Output directory #{output_dir} is not a directory " \
'or does not exist.')
end
duplicated_profile = duplicate_profile(@path, working_dir)
prepare_profile!(duplicated_profile)
hart_file = build_hart(working_dir, habitat_config)
logger.debug("Copying artifact to #{output_dir}...")
destination = File.join(output_dir, File.basename(hart_file))
FileUtils.cp(hart_file, destination)
logger.info("Habitat artifact '#{@destination}' created.")
destination
rescue => e
@log.debug(e.backtrace.join("\n"))
exit_with_error(
'Unable to generate Habitat artifact.',
"#{e.class} -- #{e.message}",
)
logger.debug(e.backtrace.join("\n"))
exit_with_error('Unable to create Habitat artifact.')
ensure
if Dir.exist?(working_dir)
logger.debug("Deleting working directory #{working_dir}")
FileUtils.rm_rf(working_dir)
end
end
def copy(hart_file)
validate_output_dir
def setup(profile = profile_from_path(@path))
path = profile.root_path
logger.debug("Setting up #{path} for Habitat...")
@log.info("Copying artifact to #{output_dir}...")
copy_hart(hart_file)
plan_file = File.join(path, 'habitat', 'plan.sh')
logger.info("Generating Habitat plan at #{plan_file}...")
vars = {
profile: profile,
habitat_origin: read_habitat_config['origin'],
}
create_file_from_template(plan_file, 'plan.sh.erb', vars)
run_hook_file = File.join(path, 'habitat', 'hooks', 'run')
logger.info("Generating a Habitat run hook at #{run_hook_file}...")
create_file_from_template(run_hook_file, 'hooks/run.erb')
default_toml = File.join(path, 'habitat', 'default.toml')
logger.info("Generating a Habitat default.toml at #{default_toml}...")
create_file_from_template(default_toml, 'default.toml.erb')
config = File.join(path, 'habitat', 'config', 'inspec_exec_config.json')
logger.info("Generating #{config} for `inspec exec`...")
create_file_from_template(config, 'config/inspec_exec_config.json.erb')
end
def upload
validate_habitat_auth_token
hart_file = create
upload_hart(hart_file)
habitat_config = read_habitat_config
if habitat_config['auth_token'].nil?
exit_with_error(
'Unable to determine Habitat auth token for uploading.',
'Run `hab setup` or set the HAB_AUTH_TOKEN environment variable.',
)
end
# Run create command to create habitat artifact
hart = create
logger.info("Uploading Habitat artifact #{hart}...")
upload_hart(hart, habitat_config)
logger.info("Habitat artifact #{hart} uploaded.")
rescue => e
@log.debug(e.backtrace.join("\n"))
exit_with_error(
'Unable to upload Habitat artifact.',
"#{e.class} -- #{e.message}",
)
end
def delete_work_dir
@log.debug("Deleting work directory #{work_dir}")
FileUtils.rm_rf(work_dir) if Dir.exist?(work_dir)
end
def setup
@log.info("Setting up profile at #{path} for Habitat...")
create_profile_object
verify_profile
vendor_profile_dependencies
create_habitat_directories(path)
create_plan(path)
create_run_hook(path)
create_default_config(path)
logger.debug(e.backtrace.join("\n"))
exit_with_error('Unable to upload Habitat artifact.')
end
private
def create_profile_object
@profile = Inspec::Profile.for_target(
def create_working_dir
working_dir = Dir.mktmpdir
logger.debug("Generated working directory #{working_dir}")
working_dir
end
def duplicate_profile(path, working_dir)
profile = profile_from_path(path)
copy_profile_to_working_dir(profile, working_dir)
profile_from_path(working_dir)
end
def prepare_profile!(profile)
vendored_profile = vendor_profile_dependencies!(profile)
verify_profile(vendored_profile)
setup(vendored_profile)
end
def profile_from_path(path)
Inspec::Profile.for_target(
path,
backend: Inspec::Backend.create(Inspec::Config.mock),
)
end
def verify_profile
@log.info('Checking to see if the profile is valid...')
def copy_profile_to_working_dir(profile, working_dir)
logger.debug('Copying profile contents to the working directory...')
profile.files.each do |profile_file|
next if File.extname(profile_file) == '.hart'
unless profile.check[:summary][:valid]
exit_with_error('Profile check failed. Please fix the profile before creating a Habitat artifact.')
end
@log.info('Profile is valid.')
end
def vendor_profile_dependencies
profile_vendor = Inspec::ProfileVendor.new(path)
if profile_vendor.lockfile.exist? && profile_vendor.cache_path.exist?
@log.info("Profile's dependencies are already vendored, skipping vendor process.")
else
@log.info("Vendoring the profile's dependencies...")
profile_vendor.vendor!
@log.info('Ensuring all vendored content has read permissions...')
profile_vendor.make_readable
# refresh the profile object since the profile now has new files
create_profile_object
end
end
def validate_habitat_installed
@log.info('Checking to see if Habitat is installed...')
cmd = Mixlib::ShellOut.new('hab --version')
cmd.run_command
exit_with_error('Unable to run Habitat commands.', cmd.stderr) if cmd.error?
end
def validate_habitat_origin
exit_with_error(
'Unable to determine Habitat origin name.',
'Run `hab setup` or set the HAB_ORIGIN environment variable.',
) if habitat_origin.nil?
end
def validate_habitat_auth_token
exit_with_error(
'Unable to determine Habitat auth token for publishing.',
'Run `hab setup` or set the HAB_AUTH_TOKEN environment variable.',
) if habitat_auth_token.nil?
end
def validate_output_dir
exit_with_error("Output directory #{output_dir} is not a directory or does not exist.") unless
File.directory?(output_dir)
end
def work_dir
return @work_dir if @work_dir
@work_dir ||= Dir.mktmpdir('inspec-habitat-exporter')
@log.debug("Generated work directory #{@work_dir}")
@work_dir
end
def create_habitat_directories(parent_directory)
[
File.join(parent_directory, 'habitat'),
File.join(parent_directory, 'habitat', 'hooks'),
].each do |dir|
Dir.mkdir(dir) unless Dir.exist?(dir)
end
end
def copy_profile_to_work_dir
@log.info('Copying profile contents to the work directory...')
profile.files.each do |f|
src = File.join(profile.root_path, f)
dst = File.join(work_dir, f)
if File.directory?(f)
@log.debug("Creating directory #{dst}")
src = File.join(profile.root_path, profile_file)
dst = File.join(working_dir, profile_file)
if File.directory?(profile_file)
logger.debug("Creating directory #{dst}")
FileUtils.mkdir_p(dst)
else
@log.debug("Copying file #{src} to #{dst}")
logger.debug("Copying file #{src} to #{dst}")
FileUtils.cp_r(src, dst)
end
end
end
def create_plan(directory)
plan_file = File.join(directory, 'habitat', 'plan.sh')
@log.info("Generating Habitat plan at #{plan_file}...")
File.write(plan_file, plan_contents)
def verify_profile(profile)
logger.debug('Checking to see if the profile is valid...')
unless profile.check[:summary][:valid]
exit_with_error('Profile check failed. Please fix the profile ' \
'before creating a Habitat artifact.')
end
logger.debug('Profile is valid.')
end
def create_run_hook(directory)
run_hook_file = File.join(directory, 'habitat', 'hooks', 'run')
@log.info("Generating a Habitat run hook at #{run_hook_file}...")
File.write(run_hook_file, run_hook_contents)
def vendor_profile_dependencies!(profile)
profile_vendor = Inspec::ProfileVendor.new(profile.root_path)
if profile_vendor.lockfile.exist? && profile_vendor.cache_path.exist?
logger.debug("Profile's dependencies are already vendored, skipping " \
'vendor process.')
else
logger.debug("Vendoring the profile's dependencies...")
profile_vendor.vendor!
logger.debug('Ensuring all vendored content has read permissions...')
profile_vendor.make_readable
end
# Return new profile since it has changed
Inspec::Profile.for_target(
profile.root_path,
backend: Inspec::Backend.create(Inspec::Config.mock),
)
end
def create_default_config(directory)
default_toml = File.join(directory, 'habitat', 'default.toml')
@log.info("Generating Habitat's default.toml configuration...")
File.write(default_toml, 'sleep_time = 300')
def verify_habitat_setup(habitat_config)
logger.debug('Checking to see if Habitat is installed...')
cmd = Mixlib::ShellOut.new('hab --version')
cmd.run_command
if cmd.error?
exit_with_error('Unable to run Habitat commands.', cmd.stderr)
end
if habitat_config['origin'].nil?
exit_with_error(
'Unable to determine Habitat origin name.',
'Run `hab setup` or set the HAB_ORIGIN environment variable.',
)
end
end
def build_hart
@log.info('Building our Habitat artifact...')
def create_file_from_template(file, template, vars = {})
FileUtils.mkdir_p(File.dirname(file))
template_path = File.join(__dir__, '../../templates/habitat', template)
contents = ERB.new(File.read(template_path))
.result(OpenStruct.new(vars).instance_eval { binding })
File.write(file, contents)
end
def build_hart(working_dir, habitat_config)
logger.debug('Building our Habitat artifact...')
env = {
'TERM' => 'vt100',
'HAB_ORIGIN' => habitat_origin,
'HAB_ORIGIN' => habitat_config['origin'],
'HAB_NONINTERACTIVE' => 'true',
}
env['RUST_LOG'] = 'debug' if @log.level == :debug
env['RUST_LOG'] = 'debug' if logger.level == :debug
# TODO: Would love to use Mixlib::ShellOut here, but it doesn't
# seem to preserve the STDIN tty, and docker gets angry.
Dir.chdir(work_dir) do
Dir.chdir(working_dir) do
unless system(env, 'hab pkg build .')
exit_with_error('Unable to build the Habitat artifact.')
end
end
hart_files = Dir.glob(File.join(work_dir, 'results', '*.hart'))
hart_files = Dir.glob(File.join(working_dir, 'results', '*.hart'))
if hart_files.length > 1
exit_with_error('More than one Habitat artifact was created which was not expected.')
exit_with_error('More than one Habitat artifact was created which ' \
'was not expected.')
elsif hart_files.empty?
exit_with_error('No Habitat artifact was created.')
end
@ -240,21 +228,16 @@ module InspecPlugins
hart_files.first
end
def copy_hart(working_dir_hart)
hart_basename = File.basename(working_dir_hart)
dst = File.join(output_dir, hart_basename)
FileUtils.cp(working_dir_hart, dst)
def upload_hart(hart_file, habitat_config)
logger.debug("Uploading '#{hart_file}' to the Habitat Builder Depot...")
dst
end
def upload_hart(hart_file)
@log.info('Uploading the Habitat artifact to our Depot...')
config = habitat_config
env = {
'TERM' => 'vt100',
'HAB_AUTH_TOKEN' => habitat_auth_token,
'HAB_AUTH_TOKEN' => config['auth_token'],
'HAB_NONINTERACTIVE' => 'true',
'HAB_ORIGIN' => config['origin'],
'TERM' => 'vt100',
}
env['HAB_DEPOT_URL'] = ENV['HAB_DEPOT_URL'] if ENV['HAB_DEPOT_URL']
@ -269,124 +252,25 @@ module InspecPlugins
)
end
@log.info('Upload complete!')
logger.debug('Upload complete!')
end
def habitat_origin
ENV['HAB_ORIGIN'] || habitat_cli_config['origin']
end
def habitat_auth_token
ENV['HAB_AUTH_TOKEN'] || habitat_cli_config['auth_token']
end
def habitat_cli_config
return @cli_config if @cli_config
config_file = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
return {} unless File.exist?(config_file)
@cli_config = Tomlrb.load_file(config_file)
end
def output_dir
options[:output_dir] || Dir.pwd
def read_habitat_config
cli_toml = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
cli_toml = '/hab/etc/cli.toml' unless File.exist?(cli_toml)
cli_config = File.exist?(cli_toml) ? Tomlrb.load_file(cli_toml) : {}
cli_config['origin'] ||= ENV['HAB_ORIGIN']
cli_config['auth_token'] ||= ENV['HAB_AUTH_TOKEN']
cli_config
end
def exit_with_error(*errors)
errors.each do |error_msg|
@log.error(error_msg)
logger.error(error_msg)
end
exit 1
end
def package_name
"inspec-profile-#{profile.name}"
end
def plan_contents
plan = <<~EOL
pkg_name=#{package_name}
pkg_version=#{profile.version}
pkg_origin=#{habitat_origin}
pkg_deps=(chef/inspec)
EOL
plan += "pkg_license='#{profile.metadata.params[:license]}'\n\n" if profile.metadata.params[:license]
plan += <<~EOL
do_setup_environment() {
ARCHIVE_PATH="$HAB_CACHE_SRC_PATH/$pkg_dirname/$pkg_name-$pkg_version.tar.gz"
}
do_build() {
if [ ! -f $PLAN_CONTEXT/../inspec.yml ]; then
exit_with 'Cannot find inspec.yml. Please build from profile root.' 1
fi
local profile_files=($(ls $PLAN_CONTEXT/../ -I habitat -I results))
local profile_location="$HAB_CACHE_SRC_PATH/$pkg_dirname/build"
mkdir -p $profile_location
build_line "Copying profile files to $profile_location"
cp -R ${profile_files[@]} $profile_location
build_line "Archiving $ARCHIVE_PATH"
inspec archive "$HAB_CACHE_SRC_PATH/$pkg_dirname/build" \
-o $ARCHIVE_PATH \
--overwrite
}
do_install() {
mkdir -p $pkg_prefix/profiles
cp $ARCHIVE_PATH $pkg_prefix/profiles
}
EOL
plan
end
def run_hook_contents
<<~EOL
#!{{pkgPathFor "core/bash"}}/bin/bash
# Redirect stderr to stdout
# This will be captured by Habitat and viewable via `journalctl`
# NOTE: We might want log to "{{pkg.svc_path}}/logs" and handle rotation
exec 2>&1
# InSpec will try to create a .cache directory in the user's home directory
# so this needs to be someplace writeable by the hab user
export HOME={{pkg.svc_var_path}}
RESULTS_DIR="{{pkg.svc_var_path}}/inspec_results"
RESULTS_FILE="${RESULTS_DIR}/{{pkg.name}}.json"
# Create a directory for InSpec reporter output
mkdir -p $(dirname $RESULTS_FILE)
while true; do
echo "Executing InSpec for {{pkg.ident}}"
inspec exec "{{pkg.path}}/profiles/*" --reporter=json > ${RESULTS_FILE}
EXIT_STATUS=$?
if [ $EXIT_STATUS -eq 0 ]; then
echo "InSpec run completed successfully."
elif [ $EXIT_STATUS -eq 100 ]; then
echo "InSpec run completed successfully, with at least 1 failed test"
elif [ $EXIT_STATUS -eq 101 ]; then
echo "InSpec run completed successfully, with skipped tests and no failures"
else
echo "InSpec run did not complete successfully. Exited with status: $?"
fi
echo "Results located here: ${RESULTS_FILE}"
echo "Sleeping for {{cfg.sleep_time}} seconds"
sleep {{cfg.sleep_time}}
done
EOL
end
end
end
end

View file

@ -0,0 +1,25 @@
{
"target_id": "{{ sys.member_id }}",
"reporter": {
"cli": {
"stdout": {{cfg.report_to_stdout}}
},
"json": {
"file": "{{pkg.svc_path}}/logs/inspec_last_run.json"
}{{#if cfg.automate.token ~}},
"automate" : {
"url": "{{cfg.automate.url}}/data-collector/v0/",
"token": "{{cfg.automate.token}}",
"node_name": "{{ sys.hostname }}",
"verify_ssl": false
}{{/if ~}}
}
{{#if cfg.automate.token }},
"compliance": {
"server" : "{{cfg.automate.url}}",
"token" : "{{cfg.automate.token}}",
"user" : "{{cfg.automate.user}}",
"insecure" : true,
"ent" : "automate"
}{{/if }}
}

View file

@ -0,0 +1,9 @@
interval = 300
report_to_stdout = true
# Uncomment and replace values to report to Automate.
# This can also be applied at runtime via `hab config apply`
#[automate]
#url = 'https://chef-automate.test'
#token = 'TOKEN'
#user = 'admin'

View file

@ -0,0 +1,32 @@
#!/bin/sh
exec 2>&1
CONFIG="{{pkg.svc_config_path}}/inspec_exec_config.json"
INTERVAL="{{cfg.interval}}"
LOG_FILE="{{pkg.svc_path}}/logs/inspec_log.txt"
PROFILE_IDENT="{{pkg.origin}}/{{pkg.name}}"
PROFILE_PATH="{{pkg.path}}/{{pkg.name}}-{{pkg.version}}.tar.gz"
while true; do
echo "Executing ${PROFILE_IDENT}"
exec inspec exec ${PROFILE_PATH} --json-config ${CONFIG} 2>&1 | tee ${LOG_FILE}
exit_code=$?
if [ $exit_code -eq 1 ]; then
echo "InSpec run failed."
else
echo "InSpec run completed successfully."
if [ $exit_code -eq 0 ]; then
echo "No controls failed or were skipped."
elif [ $exit_code -eq 100 ]; then
echo "At least 1 control failed."
elif [ $exit_code -eq 101 ]; then
echo "No controls failed but at least 1 skipped."
fi
fi
echo "Results are logged here: ${LOG_FILE}"
echo "Sleeping for ${INTERVAL} seconds"
sleep ${INTERVAL}
done

View file

@ -0,0 +1,85 @@
pkg_name=<%= "inspec-profile-#{profile.name}" %>
pkg_version=<%= profile.version %>
pkg_origin=<%= habitat_origin %>
pkg_deps=(chef/inspec)
pkg_build_deps=(chef/inspec core/jq-static)
pkg_svc_user=root
<%= "pkg_license='#{profile.metadata.params[:license]}'" if profile.metadata.params[:license]%>
do_before() {
# Exit with error if not in the directory with 'inspec.yml'.
# This can happen if someone does 'hab studio enter' from within the
# 'habitat/' directory.
if [ ! -f "$PLAN_CONTEXT/../inspec.yml" ]; then
message="ERROR: Cannot find inspec.yml."
message="$message Please build from the profile root"
build_line "$message"
return 1
fi
# Execute an 'inspec compliance login' if a profile needs to be fetched from
# the Automate server
if [ "$(grep "compliance: " "$PLAN_CONTEXT/../inspec.yml")" ]; then
_do_compliance_login;
fi
}
do_setup_environment() {
set_buildtime_env PROFILE_CACHE_DIR "$HAB_CACHE_SRC_PATH/$pkg_dirname"
set_buildtime_env ARCHIVE_NAME "$pkg_name-$pkg_version.tar.gz"
# InSpec loads `pry` which tries to expand `~`. This fails if HOME isn't set.
set_runtime_env HOME "$pkg_svc_var_path"
# InSpec will create a `.inspec` directory in the user's home directory.
# This overrides that to write to a place within the running service's path.
# NOTE: Setting HOME does the same currently. This is here to be explicit.
set_runtime_env INSPEC_CONFIG_DIR "$pkg_svc_var_path"
}
do_unpack() {
# Change directory to where the profile files are
pushd "$PLAN_CONTEXT/../" > /dev/null
# Get a list of all files in the profile except those that are Habitat related
profile_files=($(ls -I habitat -I results -I "*.hart"))
mkdir -p "$PROFILE_CACHE_DIR" > /dev/null
# Copy just the profile files to the profile cache directory
cp -r ${profile_files[@]} "$PROFILE_CACHE_DIR"
}
do_build() {
inspec archive "$PROFILE_CACHE_DIR" \
--overwrite \
-o "$PROFILE_CACHE_DIR/$ARCHIVE_NAME"
}
do_install() {
cp "$PROFILE_CACHE_DIR/$ARCHIVE_NAME" "$pkg_prefix"
}
_do_compliance_login() {
if [ -z $COMPLIANCE_CREDS ]; then
message="ERROR: Please perform an 'inspec compliance login' and set"
message="$message \$HAB_STUDIO_SECRET_COMPLIANCE_CREDS to the contents of"
message="$message '~/.inspec/compliance/config.json'"
build_line "$message"
return 1
fi
user=$(echo $COMPLIANCE_CREDS | jq .user | sed 's/"//g')
token=$(echo $COMPLIANCE_CREDS | jq .token | sed 's/"//g')
automate_server=$(echo $COMPLIANCE_CREDS | \
jq .server | \
sed 's/\/api\/v0//' | \
sed 's/"//g'
)
insecure=$(echo $COMPLIANCE_CREDS | jq .insecure)
inspec compliance login --insecure $insecure \
--user $user \
--token $token \
$automate_server
}

View file

@ -0,0 +1,2 @@
source 'https://supermarket.chef.io'
metadata

View file

@ -0,0 +1,3 @@
# InSpec Habitat Fixture
Used to setup a server for testing the `inspec-habitat` plugin

View file

@ -0,0 +1,28 @@
#!/usr/bin/expect -f
set timeout -1
spawn hab setup
expect "Connect to an on-premises bldr instance?"
send -- "No\r"
expect "Set up a default origin?"
send -- "Yes\r"
expect "Default origin name"
send -- "vagrant\r"
expect "Create an origin key for `vagrant'?"
send -- "Yes\r"
expect "Set up a default Habitat personal access token?"
send -- "No\r"
expect "Set up a default Habitat Supervisor CtlGateway secret?"
send -- "No\r"
expect "Enable analytics?"
send -- "No\r"
expect eof

View file

@ -0,0 +1,9 @@
name 'inspec_habitat_fixture'
maintainer 'The Authors'
maintainer_email 'you@example.com'
license 'All Rights Reserved'
description 'Used for testing the inspec-habitat plugin'
version '0.1.0'
chef_version '>= 13.0'
depends 'habitat'

View file

@ -0,0 +1,61 @@
#
# Cookbook:: kitchen_setup_cookbook
# Recipe:: default
#
# Copyright:: 2019, The Authors, All Rights Reserved.
package %w(ruby ruby-dev gcc g++ make expect)
base_dir = '/home/vagrant'
cookbook_file "#{base_dir}/inspec-local.gem" do
source 'inspec-local.gem'
action :create
end
gem_package 'inspec' do
source "#{base_dir}/inspec-local.gem"
subscribes :install, "cookbook_file[#{base_dir}/inspec-local.gem]", :immediately
end
cookbook_file "#{base_dir}/hab_setup.exp" do
source 'hab_setup.exp'
mode '0755'
action :create
end
hab_install 'install habitat'
hab_sup 'setup hab supervisor'
execute 'setup hab cli' do
command "#{base_dir}/hab_setup.exp"
live_stream true
not_if { ::File.exist?('/hab/etc/cli.toml') }
not_if { ::File.exist?('~/.hab/etc/cli.toml') }
end
execute 'create inspec profile for testing' do
command "inspec init profile #{base_dir}/hab_test_profile"
live_stream true
creates "#{base_dir}/hab_test_profile"
end
directory "#{base_dir}/output"
execute 'create hart file from profile' do
command "inspec habitat profile create #{base_dir}/hab_test_profile --output_dir '#{base_dir}/output'"
live_stream true
not_if "find #{base_dir}/output | grep vagrant-inspec-profile-hab_test_profile-0.1.0-.*.hart"
end
execute 'install vagrant/inspec-profile-hab_test_profile' do
command "hab pkg install #{base_dir}/output/*.hart"
live_stream true
not_if 'hab pkg list --origin vagrant | grep inspec-profile'
end
execute 'load vagrant/inspec-profile-hab_test_profile into supervisor' do
command 'hab svc load vagrant/inspec-profile-hab_test_profile'
live_stream true
not_if 'sudo hab svc status | grep "vagrant/inspec-profile-hab_test_profile"'
end

View file

@ -0,0 +1,38 @@
require_relative '../../../shared/core_plugin_test_helper.rb'
require 'fileutils'
class ProfileCli < MiniTest::Test
include CorePluginFunctionalHelper
def setup
@tmpdir = Dir.mktmpdir
@habitat_profile = File.join(@tmpdir, 'habitat-profile')
run_inspec_process('init profile ' + @habitat_profile)
end
def teardown
FileUtils.remove_entry_secure(@tmpdir)
end
def test_setup_subcommand
result = run_inspec_process('habitat profile setup ' + @habitat_profile + ' --log-level debug')
# Command runs without error
assert_empty result.stderr
assert_equal 0, result.exit_status
# Command creates only expected files
base_dir = File.join(@tmpdir, 'habitat-profile', 'habitat')
files = %w{
default.toml
plan.sh
config
config/inspec_exec_config.json
hooks
hooks/run
}
actual_files = Dir.glob(File.join(base_dir, '**/*'))
expected_files = files.map { |x| File.join(base_dir, x) }
assert_equal actual_files.sort, expected_files.sort
end
end

View file

@ -0,0 +1,3 @@
# Habitat InSpec Test Profile
This profile is used to test `inspec habitat profile` commands

View file

@ -0,0 +1,40 @@
control 'inspec-habitat-create' do
title 'Create command'
output_hart_dir = '/home/vagrant/output'
find_hart_output = command("find #{output_hart_dir} -name '*.hart'").stdout
hart_files = find_hart_output.split("\n")
hab_profile_path = '/home/vagrant/hab_test_profile'
find_profile_files_command = "find #{hab_profile_path} -type f -printf '%f\n'"
profile_files = command(find_profile_files_command).stdout.split("\n").sort
expected_files = %w{
.gitkeep
README.md
example.rb
inspec.yml
}
describe '`inspec habitat profile create`' do
it 'should create exactly 1 hart file' do
expect(hart_files.length).to eq(1)
end
it 'does not add any extra files to a default generated profile' do
expect(profile_files).to eq(expected_files)
end
end
end
control 'inspec-habitat-service' do
title 'inspec-profile-hab_test_profile service'
describe 'The running service' do
it 'should create a log file' do
log = '/hab/svc/inspec-profile-hab_test_profile/logs/inspec_log.txt'
expect(file(log).exist?).to be(true)
end
it 'should create a JSON file for the last run' do
log = '/hab/svc/inspec-profile-hab_test_profile/logs/inspec_last_run.json'
JSON.parse(file(log).content)
end
end
end

View file

@ -0,0 +1,10 @@
name: inspec_habitat
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os

View file

@ -0,0 +1,3 @@
# Example InSpec Profile
This profile is used for unit testing the `inspec-habitat` profile

View file

@ -0,0 +1,7 @@
control 'example' do
impact 0.7
title 'Example control'
describe 'example' do
it { should cmp 'example' }
end
end

View file

@ -0,0 +1,10 @@
name: example_profile
title: InSpec Profile
maintainer: The Authors
copyright: The Authors
copyright_email: you@example.com
license: Apache-2.0
summary: An InSpec Compliance Profile
version: 0.1.0
supports:
platform: os

View file

@ -1,184 +1,240 @@
require 'mixlib/log'
require 'ostruct'
require 'fileutils'
require 'minitest/autorun'
require 'mocha/setup'
require_relative '../../lib/inspec-habitat/profile.rb'
describe InspecPlugins::Habitat::Profile do
let(:profile) do
OpenStruct.new(
name: 'my_profile',
version: '1.2.3',
files: %w(file1 file2)
class InspecPlugins::Habitat::ProfileTest < MiniTest::Unit::TestCase
def setup
@tmpdir = Dir.mktmpdir
@output_dir = File.join(@tmpdir, 'output')
FileUtils.mkdir(@output_dir)
@fake_hart_file = FileUtils.touch(File.join(@tmpdir, 'fake-hart.hart'))[0]
# Path from `__FILE__` needed to support running tests in `inspec/inspec`
@test_profile_path = File.join(
File.expand_path(File.dirname(__FILE__)),
'../',
'support',
'example_profile'
)
@test_profile = Inspec::Profile.for_target(
@test_profile_path,
backend: Inspec::Backend.create(Inspec::Config.mock),
)
end
let(:subject) { InspecPlugins::Habitat::Profile.new('/path/to/profile', { 'log_level' => 'fatal' }) }
@hab_profile = InspecPlugins::Habitat::Profile.new(
@test_profile_path,
{ output_dir: @output_dir },
)
@mock_hab_config = {
'auth_token' => 'FAKETOKEN',
'origin' => 'fake_origin',
}
before do
Inspec::Log.level(:fatal)
end
describe '#verify_profile' do
it 'exits if the profile is not valid' do
profile = mock
profile.stubs(:check).returns(summary: { valid: false })
subject.expects(:profile).returns(profile)
proc { subject.send(:verify_profile) }.must_raise SystemExit
end
it 'does not exist if the profile is valid' do
profile = mock
profile.stubs(:check).returns(summary: { valid: true })
subject.expects(:profile).returns(profile)
subject.send(:verify_profile)
end
def after_run
FileUtils.remove_entry_secure(@tmpdir)
end
describe '#vendor_profile_dependencies' do
let(:profile_vendor) do
profile_vendor = mock
profile_vendor.stubs(:lockfile).returns(lockfile)
profile_vendor.stubs(:cache_path).returns(cache_path)
profile_vendor
end
let(:lockfile) { mock }
let(:cache_path) { mock }
def test_create_raises_if_output_dir_does_not_exist
profile = InspecPlugins::Habitat::Profile.new(
@test_profile_path,
{
output_dir: '/not/a/real/path',
log_level: 'fatal',
},
)
before do
Inspec::ProfileVendor.expects(:new).returns(profile_vendor)
end
assert_raises(SystemExit) { profile.create }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
describe 'when lockfile exists and cache dir exists' do
it 'does not vendor the dependencies' do
lockfile.expects(:exist?).returns(true)
cache_path.expects(:exist?).returns(true)
profile_vendor.expects(:vendor!).never
profile_vendor.expects(:make_readable).never
subject.send(:vendor_profile_dependencies)
def test_create
file_count = Dir.glob(File.join(@test_profile_path, '**/*')).count
@hab_profile.stub :read_habitat_config, @mock_hab_config do
@hab_profile.stub :verify_habitat_setup, nil do
@hab_profile.stub :build_hart, @fake_hart_file do
@hab_profile.create
end
end
end
describe 'when the lockfile exists but the cache dir does not' do
it 'vendors the dependencies and refreshes the profile object' do
lockfile.expects(:exist?).returns(true)
cache_path.expects(:exist?).returns(false)
profile_vendor.expects(:vendor!)
profile_vendor.expects(:make_readable)
subject.expects(:create_profile_object)
# It should not modify target profile
new_file_count = Dir.glob(File.join(@test_profile_path, '**/*')).count
assert_equal new_file_count, file_count
subject.send(:vendor_profile_dependencies)
end
# It should create 1 Habitat artifact
output_files = Dir.glob(File.join(@output_dir, '**/*'))
assert_equal 1, output_files.count
assert_equal 'fake-hart.hart', File.basename(output_files.first)
end
def test_create_rasies_if_habitat_is_not_installed
cmd = MiniTest::Mock.new
cmd.expect(:error?, true)
cmd.expect(:run_command, nil)
Mixlib::ShellOut.stub :new, cmd, 'hab --version' do
assert_raises(SystemExit) { @hab_profile.create }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
describe 'when the lockfile does not exist' do
it 'vendors the dependencies and refreshes the profile object' do
lockfile.expects(:exist?).returns(false)
profile_vendor.expects(:vendor!)
profile_vendor.expects(:make_readable)
subject.expects(:create_profile_object)
cmd.verify
end
subject.send(:vendor_profile_dependencies)
def test_upload
@hab_profile.stub :read_habitat_config, @mock_hab_config do
@hab_profile.stub :create, @fake_hart_file do
@hab_profile.stub :upload_hart, nil do
@hab_profile.upload
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
end
end
end
describe '#validate_habitat_installed' do
it 'exits if hab --version fails' do
cmd = mock
cmd.stubs(:error?).returns(true)
cmd.stubs(:run_command)
cmd.stubs(:stdout)
cmd.stubs(:stderr)
Mixlib::ShellOut.expects(:new).with('hab --version').returns(cmd)
proc { subject.send(:validate_habitat_installed) }.must_raise SystemExit
def test_upload_raises_if_no_habitat_auth_token_is_found
@hab_profile.stub :read_habitat_config, {} do
assert_raises(SystemExit) { @hab_profile.upload }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
end
describe '#validate_habitat_origin' do
it 'does not exit if the origin key exists' do
subject.expects(:habitat_origin).returns('12345')
subject.send(:validate_habitat_origin)
end
it 'exits if no origin key exists' do
subject.expects(:habitat_origin).returns(nil)
proc { subject.send(:validate_habitat_origin) }.must_raise SystemExit
def test_create_working_dir
Dir.stub :mktmpdir, '/tmp/fakedir' do
assert_equal '/tmp/fakedir', @hab_profile.send(:create_working_dir)
end
end
describe '#validate_habitat_auth_token' do
it 'does not exit if the auth_token exists' do
subject.expects(:habitat_auth_token).returns('12345')
subject.send(:validate_habitat_auth_token)
end
def test_duplicate_profile
current_profile = @test_profile
duplicated_profile = @hab_profile.send(:duplicate_profile,
@test_profile_path,
@tmpdir)
assert duplicated_profile.is_a?(Inspec::Profile)
assert duplicated_profile.sha256 == current_profile.sha256.to_s
refute_same duplicated_profile.root_path, current_profile.root_path
end
it 'exits if no auth_token exists' do
subject.expects(:habitat_auth_token).returns(nil)
proc { subject.send(:validate_habitat_auth_token) }.must_raise SystemExit
def test_profile_from_path
profile = @hab_profile.send(:profile_from_path, @test_profile_path)
assert profile.is_a?(Inspec::Profile)
end
def test_copy_profile_to_working_dir
duplicated_profile = @hab_profile.send(:duplicate_profile,
@test_profile_path,
@tmpdir)
dst = File.join(@tmpdir, 'working_dir')
FileUtils.mkdir_p(dst)
@hab_profile.send(:copy_profile_to_working_dir, duplicated_profile, dst)
expected_files = %w{
README.md
inspec.yml
example.rb
}
actual_files = Dir.glob(File.join(dst, '**/*')).map do |path|
next unless File.file?(path)
File.basename(path)
end.compact
assert(actual_files.sort == expected_files.sort)
end
def test_verify_profile_raises_if_profile_is_not_valid
bad_profile_path = File.join(@tmpdir, 'bad_profile')
FileUtils.mkdir_p(File.join(bad_profile_path))
FileUtils.touch(File.join(bad_profile_path, 'inspec.yml'))
bad_profile = Inspec::Profile.for_target(
bad_profile_path,
backend: Inspec::Backend.create(Inspec::Config.mock),
)
assert_raises(SystemExit) { @hab_profile.send(:verify_profile, bad_profile) }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
def test_vendor_profile_dependencies_does_not_vendor_if_already_vendored
mock_lock_file = MiniTest::Mock.new
mock_lock_file.expect(:exist?, true)
mock_cache_path = MiniTest::Mock.new
mock_cache_path.expect(:exist?, true)
mock = MiniTest::Mock.new
mock.expect(:lockfile, mock_lock_file)
mock.expect(:cache_path, mock_cache_path)
Inspec::ProfileVendor.stub :new, mock do
new_profile = @hab_profile.send(:vendor_profile_dependencies!,
@test_profile)
assert new_profile.is_a?(Inspec::Profile)
end
end
describe '#build_hart' do
before do
subject.expects(:work_dir).at_least_once.returns(Dir.tmpdir)
end
def test_vendor_profile_dependencies
mock_lock_file = MiniTest::Mock.new
mock_lock_file.expect(:exist?, false)
it 'exits if the build fails' do
subject.expects(:system).returns(false)
proc { subject.send(:build_hart) }.must_raise SystemExit
end
mock = MiniTest::Mock.new
mock.expect(:lockfile, mock_lock_file)
mock.expect(:vendor!, nil)
mock.expect(:make_readable, nil)
it 'exits if more than one hart is created' do
subject.expects(:system).returns(true)
Dir.expects(:glob).returns(%w(hart1 hart2))
proc { subject.send(:build_hart) }.must_raise SystemExit
end
it 'exits if more than no hart is created' do
subject.expects(:system).returns(true)
Dir.expects(:glob).returns([])
proc { subject.send(:build_hart) }.must_raise SystemExit
end
it 'returns the hart filename' do
subject.expects(:system).returns(true)
Dir.expects(:glob).returns(%w(hart1))
subject.send(:build_hart).must_equal('hart1')
Inspec::ProfileVendor.stub :new, mock do
new_profile = @hab_profile.send(:vendor_profile_dependencies!,
@test_profile)
assert new_profile.is_a?(Inspec::Profile)
end
mock.verify
end
describe '#upload_hart' do
it 'exits if the upload failed' do
env = {
'TERM' => 'vt100',
'HAB_AUTH_TOKEN' => 'my_token',
'HAB_NONINTERACTIVE' => 'true',
}
def test_verify_habitat_setup_raises_if_hab_version_errors
mock = MiniTest::Mock.new
mock.expect(:run_command, nil)
mock.expect(:error?, true)
mock.expect(:stderr, 'This would be an error message')
cmd = mock
cmd.stubs(:run_command)
cmd.stubs(:error?).returns(true)
cmd.stubs(:stdout)
cmd.stubs(:stderr)
subject.expects(:habitat_auth_token).returns('my_token')
Mixlib::ShellOut.expects(:new).with("hab pkg upload my_hart", env: env).returns(cmd)
proc { subject.send(:upload_hart, 'my_hart') }.must_raise SystemExit
Mixlib::ShellOut.stub(:new, mock) do
assert_raises(SystemExit) { @hab_profile.send(:verify_habitat_setup, {}) }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
mock.verify
end
describe '#habitat_cli_config' do
it 'returns an empty hash if the CLI config does not exist' do
File.expects(:exist?).with(File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')).returns(false)
subject.send(:habitat_cli_config).must_equal({})
end
def test_verify_habitat_setup_raises_if_not_habitat_origin
mock = MiniTest::Mock.new
mock.expect(:run_command, nil)
mock.expect(:error?, false)
it 'returns parsed TOML from the hab config file' do
config_file = File.join(ENV['HOME'], '.hab', 'etc', 'cli.toml')
File.expects(:exist?).with(config_file).returns(true)
Tomlrb.expects(:load_file).with(config_file).returns(foo: 1)
subject.send(:habitat_cli_config).must_equal(foo: 1)
Mixlib::ShellOut.stub(:new, mock) do
assert_raises(SystemExit) { @hab_profile.send(:verify_habitat_setup, {}) }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
mock.verify
end
# TODO: Figure out how to stub system()
# def test_build_hart
# end
def test_upload_hart_raises_if_hab_pkg_upload_fails
mock = MiniTest::Mock.new
mock.expect(:run_command, nil)
mock.expect(:error?, true)
mock.expect(:stdout, 'This would contain output from `hab`')
mock.expect(:stderr, 'This would be an error message')
Mixlib::ShellOut.stub(:new, mock) do
assert_raises(SystemExit) { @hab_profile.send(:upload_hart, @fake_hart_file, {}) }
# TODO: Figure out how to capture and validate `Inspec::Log.error`
end
end
end