mirror of
https://github.com/inspec/inspec
synced 2024-11-27 15:10:44 +00:00
Merge pull request #671 from chef/dr/formatter-redesign
JSON formatter redesign
This commit is contained in:
commit
84ee58d89f
18 changed files with 407 additions and 379 deletions
|
@ -15,8 +15,6 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
|
||||||
desc: 'Show diagnostics (versions, configurations)'
|
desc: 'Show diagnostics (versions, configurations)'
|
||||||
|
|
||||||
desc 'json PATH', 'read all tests in PATH and generate a JSON summary'
|
desc 'json PATH', 'read all tests in PATH and generate a JSON summary'
|
||||||
option :id, type: :string,
|
|
||||||
desc: 'Attach a profile ID to all test results'
|
|
||||||
option :output, aliases: :o, type: :string,
|
option :output, aliases: :o, type: :string,
|
||||||
desc: 'Save the created profile to a path'
|
desc: 'Save the created profile to a path'
|
||||||
option :controls, type: :array,
|
option :controls, type: :array,
|
||||||
|
|
|
@ -31,37 +31,6 @@ module Inspec::DSL
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Register a given rule with RSpec and
|
|
||||||
# let it run. This happens after everything
|
|
||||||
# else is merged in.
|
|
||||||
def self.execute_rule(r, profile_id)
|
|
||||||
checks = ::Inspec::Rule.prepare_checks(r)
|
|
||||||
fid = InspecBaseRule.full_id(r, profile_id)
|
|
||||||
checks.each do |m, a, b|
|
|
||||||
# check if the resource is skippable and skipped
|
|
||||||
cres = rule_from_check(m, a, b)
|
|
||||||
set_rspec_ids(cres, fid) if m == 'describe'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# merge two rules completely; all defined
|
|
||||||
# fields from src will be overwritten in dst
|
|
||||||
def self.merge_rules(dst, src)
|
|
||||||
InspecBaseRule.merge dst, src
|
|
||||||
end
|
|
||||||
|
|
||||||
# Attach an ID attribute to the
|
|
||||||
# metadata of all examples
|
|
||||||
# TODO: remove this once IDs are in rspec-core
|
|
||||||
def self.set_rspec_ids(obj, id)
|
|
||||||
obj.examples.each {|ex|
|
|
||||||
ex.metadata[:id] = id
|
|
||||||
}
|
|
||||||
obj.children.each {|c|
|
|
||||||
set_rspec_ids(c, id)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.load_spec_files_for_profile(bind_context, opts, &block)
|
def self.load_spec_files_for_profile(bind_context, opts, &block)
|
||||||
# get all spec files
|
# get all spec files
|
||||||
target = get_reference_profile(opts[:profile_id], opts[:conf])
|
target = get_reference_profile(opts[:profile_id], opts[:conf])
|
||||||
|
@ -121,24 +90,3 @@ module Inspec::DSL
|
||||||
ctx
|
ctx
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
module Inspec::GlobalDSL
|
|
||||||
def __register_rule(r)
|
|
||||||
# make sure the profile id is attached to the rule
|
|
||||||
::Inspec::DSL.execute_rule(r, __profile_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def __unregister_rule(_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module Inspec::DSLHelper
|
|
||||||
def self.bind_dsl(scope)
|
|
||||||
(class << scope; self; end).class_exec do
|
|
||||||
include Inspec::DSL
|
|
||||||
include Inspec::GlobalDSL
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
::Inspec::DSLHelper.bind_dsl(self)
|
|
||||||
|
|
|
@ -11,7 +11,6 @@ require 'inspec/metadata'
|
||||||
module Inspec
|
module Inspec
|
||||||
class Profile # rubocop:disable Metrics/ClassLength
|
class Profile # rubocop:disable Metrics/ClassLength
|
||||||
extend Forwardable
|
extend Forwardable
|
||||||
attr_reader :path
|
|
||||||
|
|
||||||
def self.resolve_target(target, opts)
|
def self.resolve_target(target, opts)
|
||||||
# Fetchers retrieve file contents
|
# Fetchers retrieve file contents
|
||||||
|
@ -35,6 +34,7 @@ module Inspec
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :source_reader
|
attr_reader :source_reader
|
||||||
|
attr_accessor :runner_context
|
||||||
def_delegator :@source_reader, :tests
|
def_delegator :@source_reader, :tests
|
||||||
def_delegator :@source_reader, :libraries
|
def_delegator :@source_reader, :libraries
|
||||||
def_delegator :@source_reader, :metadata
|
def_delegator :@source_reader, :metadata
|
||||||
|
@ -46,6 +46,7 @@ module Inspec
|
||||||
@logger = @options[:logger] || Logger.new(nil)
|
@logger = @options[:logger] || Logger.new(nil)
|
||||||
@source_reader = source_reader
|
@source_reader = source_reader
|
||||||
@profile_id = @options[:id]
|
@profile_id = @options[:id]
|
||||||
|
@runner_context = nil
|
||||||
Metadata.finalize(@source_reader.metadata, @profile_id)
|
Metadata.finalize(@source_reader.metadata, @profile_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,24 +56,16 @@ module Inspec
|
||||||
|
|
||||||
def info
|
def info
|
||||||
res = params.dup
|
res = params.dup
|
||||||
rules = {}
|
controls = res[:controls].map do |id, rule|
|
||||||
res[:rules].each do |gid, group|
|
next if id.to_s.empty?
|
||||||
next if gid.to_s.empty?
|
data = rule.dup
|
||||||
rules[gid] = { title: gid, rules: {} }
|
data.delete(:checks)
|
||||||
group.each do |id, rule|
|
data[:impact] ||= 0.5
|
||||||
next if id.to_s.empty?
|
data[:impact] = 1.0 if data[:impact] > 1.0
|
||||||
data = rule.dup
|
data[:impact] = 0.0 if data[:impact] < 0.0
|
||||||
data.delete(:checks)
|
[id, data]
|
||||||
data[:impact] ||= 0.5
|
|
||||||
data[:impact] = 1.0 if data[:impact] > 1.0
|
|
||||||
data[:impact] = 0.0 if data[:impact] < 0.0
|
|
||||||
rules[gid][:rules][id] = data
|
|
||||||
# TODO: temporarily flatten the group down; replace this with
|
|
||||||
# proper hierarchy later on
|
|
||||||
rules[gid][:title] = data[:group_title]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
res[:rules] = rules
|
res[:controls] = Hash[controls.compact]
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -137,7 +130,7 @@ module Inspec
|
||||||
warn.call(@target, 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.')
|
warn.call(@target, 0, 0, nil, 'Profile uses deprecated `test` directory, rename it to `controls`.')
|
||||||
end
|
end
|
||||||
|
|
||||||
count = rules_count
|
count = controls_count
|
||||||
result[:summary][:controls] = count
|
result[:summary][:controls] = count
|
||||||
if count == 0
|
if count == 0
|
||||||
warn.call(nil, nil, nil, nil, 'No controls or tests were defined.')
|
warn.call(nil, nil, nil, nil, 'No controls or tests were defined.')
|
||||||
|
@ -146,18 +139,15 @@ module Inspec
|
||||||
end
|
end
|
||||||
|
|
||||||
# iterate over hash of groups
|
# iterate over hash of groups
|
||||||
params[:rules].each { |group, controls|
|
params[:controls].each { |id, control|
|
||||||
@logger.info "Verify all controls in #{group}"
|
sfile, sline = control[:source_location]
|
||||||
controls.each { |id, control|
|
error.call(sfile, sline, nil, id, 'Avoid controls with empty IDs') if id.nil? or id.empty?
|
||||||
sfile, sline = control[:source_location]
|
next if id.start_with? '(generated '
|
||||||
error.call(sfile, sline, nil, id, 'Avoid controls with empty IDs') if id.nil? or id.empty?
|
warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
|
||||||
next if id.start_with? '(generated '
|
warn.call(sfile, sline, nil, id, "Control #{id} has no description") if control[:desc].to_s.empty?
|
||||||
warn.call(sfile, sline, nil, id, "Control #{id} has no title") if control[:title].to_s.empty?
|
warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
|
||||||
warn.call(sfile, sline, nil, id, "Control #{id} has no description") if control[:desc].to_s.empty?
|
warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
|
||||||
warn.call(sfile, sline, nil, id, "Control #{id} has impact > 1.0") if control[:impact].to_f > 1.0
|
warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty?
|
||||||
warn.call(sfile, sline, nil, id, "Control #{id} has impact < 0.0") if control[:impact].to_f < 0.0
|
|
||||||
warn.call(sfile, sline, nil, id, "Control #{id} has no tests defined") if control[:checks].nil? or control[:checks].empty?
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# profile is valid if we could not find any error
|
# profile is valid if we could not find any error
|
||||||
|
@ -167,8 +157,8 @@ module Inspec
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def rules_count
|
def controls_count
|
||||||
params[:rules].values.map { |hm| hm.values.length }.inject(:+) || 0
|
params[:controls].values.length
|
||||||
end
|
end
|
||||||
|
|
||||||
# generates a archive of a folder profile
|
# generates a archive of a folder profile
|
||||||
|
@ -233,38 +223,63 @@ module Inspec
|
||||||
def load_params
|
def load_params
|
||||||
params = @source_reader.metadata.params
|
params = @source_reader.metadata.params
|
||||||
params[:name] = @profile_id unless @profile_id.nil?
|
params[:name] = @profile_id unless @profile_id.nil?
|
||||||
params[:rules] = rules = {}
|
load_checks_params(params)
|
||||||
prefix = @source_reader.target.prefix || ''
|
|
||||||
|
|
||||||
# we're checking a profile, we don't care if it runs on the host machine
|
|
||||||
opts = @options.dup
|
|
||||||
opts[:ignore_supports] = true
|
|
||||||
runner = Runner.new(
|
|
||||||
id: @profile_id,
|
|
||||||
backend: :mock,
|
|
||||||
test_collector: opts.delete(:test_collector),
|
|
||||||
)
|
|
||||||
runner.add_profile(self, opts)
|
|
||||||
|
|
||||||
runner.rules.each do |id, rule|
|
|
||||||
file = rule.instance_variable_get(:@__file)
|
|
||||||
file = file[prefix.length..-1] if file.start_with?(prefix)
|
|
||||||
rules[file] ||= {}
|
|
||||||
rules[file][id] = {
|
|
||||||
title: rule.title,
|
|
||||||
desc: rule.desc,
|
|
||||||
impact: rule.impact,
|
|
||||||
refs: rule.ref,
|
|
||||||
tags: rule.tag,
|
|
||||||
checks: Inspec::Rule.checks(rule),
|
|
||||||
code: rule.instance_variable_get(:@__code),
|
|
||||||
source_location: rule.instance_variable_get(:@__source_location),
|
|
||||||
group_title: rule.instance_variable_get(:@__group_title),
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@profile_id ||= params[:name]
|
@profile_id ||= params[:name]
|
||||||
params
|
params
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def load_checks_params(params)
|
||||||
|
params[:controls] = controls = {}
|
||||||
|
params[:groups] = groups = {}
|
||||||
|
prefix = @source_reader.target.prefix || ''
|
||||||
|
|
||||||
|
if @runner_context.nil?
|
||||||
|
# we're checking a profile, we don't care if it runs on the host machine
|
||||||
|
opts = @options.dup
|
||||||
|
opts[:ignore_supports] = true
|
||||||
|
runner = Runner.new(
|
||||||
|
id: @profile_id,
|
||||||
|
backend: :mock,
|
||||||
|
test_collector: opts.delete(:test_collector),
|
||||||
|
)
|
||||||
|
runner.add_profile(self, opts)
|
||||||
|
runner.rules.values.each do |rule|
|
||||||
|
f = load_rule_filepath(prefix, rule)
|
||||||
|
load_rule(rule, f, controls, groups)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# load from context
|
||||||
|
@runner_context.rules.values.each do |rule|
|
||||||
|
f = load_rule_filepath(prefix, rule)
|
||||||
|
load_rule(rule, f, controls, groups)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_rule_filepath(prefix, rule)
|
||||||
|
file = rule.instance_variable_get(:@__file)
|
||||||
|
file = file[prefix.length..-1] if file.start_with?(prefix)
|
||||||
|
file
|
||||||
|
end
|
||||||
|
|
||||||
|
def load_rule(rule, file, controls, groups)
|
||||||
|
id = Inspec::Rule.rule_id(rule)
|
||||||
|
controls[id] = {
|
||||||
|
title: rule.title,
|
||||||
|
desc: rule.desc,
|
||||||
|
impact: rule.impact,
|
||||||
|
refs: rule.ref,
|
||||||
|
tags: rule.tag,
|
||||||
|
checks: Inspec::Rule.checks(rule),
|
||||||
|
code: rule.instance_variable_get(:@__code),
|
||||||
|
source_location: rule.instance_variable_get(:@__source_location),
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[file] ||= {
|
||||||
|
title: rule.instance_variable_get(:@__group_title),
|
||||||
|
controls: [],
|
||||||
|
}
|
||||||
|
groups[file][:controls].push(id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -41,24 +41,19 @@ module Inspec
|
||||||
end
|
end
|
||||||
|
|
||||||
def unregister_rule(id)
|
def unregister_rule(id)
|
||||||
full_id = Inspec::Rule.full_id(@profile_id, id)
|
@rules.delete(full_id(@profile_id, id))
|
||||||
@rules[full_id] = nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_rule(r)
|
def register_rule(r)
|
||||||
# get the full ID
|
# get the full ID
|
||||||
r.instance_variable_set(:@__file, @current_load[:file])
|
r.instance_variable_set(:@__file, @current_load[:file])
|
||||||
r.instance_variable_set(:@__group_title, @current_load[:title])
|
r.instance_variable_set(:@__group_title, @current_load[:title])
|
||||||
full_id = Inspec::Rule.full_id(@profile_id, r)
|
|
||||||
if full_id.nil?
|
|
||||||
# TODO: error
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# add the rule to the registry
|
# add the rule to the registry
|
||||||
existing = @rules[full_id]
|
fid = full_id(Inspec::Rule.profile_id(r), Inspec::Rule.rule_id(r))
|
||||||
|
existing = @rules[fid]
|
||||||
if existing.nil?
|
if existing.nil?
|
||||||
@rules[full_id] = r
|
@rules[fid] = r
|
||||||
else
|
else
|
||||||
Inspec::Rule.merge(existing, r)
|
Inspec::Rule.merge(existing, r)
|
||||||
end
|
end
|
||||||
|
@ -70,6 +65,11 @@ module Inspec
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def full_id(pid, rid)
|
||||||
|
return rid.to_s if pid.to_s.empty?
|
||||||
|
pid.to_s + '/' + rid.to_s
|
||||||
|
end
|
||||||
|
|
||||||
# Create the context for controls. This includes all components of the DSL,
|
# Create the context for controls. This includes all components of the DSL,
|
||||||
# including matchers and resources.
|
# including matchers and resources.
|
||||||
#
|
#
|
||||||
|
@ -93,6 +93,7 @@ module Inspec
|
||||||
# @return [ProfileContextClass]
|
# @return [ProfileContextClass]
|
||||||
def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
def create_context(resources_dsl, rule_class) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
||||||
profile_context_owner = self
|
profile_context_owner = self
|
||||||
|
profile_id = @profile_id
|
||||||
|
|
||||||
# rubocop:disable Lint/NestedMethodDefinition
|
# rubocop:disable Lint/NestedMethodDefinition
|
||||||
Class.new do
|
Class.new do
|
||||||
|
@ -116,7 +117,7 @@ module Inspec
|
||||||
define_method :control do |*args, &block|
|
define_method :control do |*args, &block|
|
||||||
id = args[0]
|
id = args[0]
|
||||||
opts = args[1] || {}
|
opts = args[1] || {}
|
||||||
register_control(rule_class.new(id, opts, &block))
|
register_control(rule_class.new(id, profile_id, opts, &block))
|
||||||
end
|
end
|
||||||
|
|
||||||
define_method :describe do |*args, &block|
|
define_method :describe do |*args, &block|
|
||||||
|
@ -124,7 +125,7 @@ module Inspec
|
||||||
id = "(generated from #{loc} #{SecureRandom.hex})"
|
id = "(generated from #{loc} #{SecureRandom.hex})"
|
||||||
|
|
||||||
res = nil
|
res = nil
|
||||||
rule = rule_class.new(id, {}) do
|
rule = rule_class.new(id, profile_id, {}) do
|
||||||
res = describe(*args, &block)
|
res = describe(*args, &block)
|
||||||
end
|
end
|
||||||
register_control(rule, &block)
|
register_control(rule, &block)
|
||||||
|
|
|
@ -5,43 +5,46 @@
|
||||||
require 'rspec/core'
|
require 'rspec/core'
|
||||||
require 'rspec/core/formatters/json_formatter'
|
require 'rspec/core/formatters/json_formatter'
|
||||||
|
|
||||||
# Extend the basic RSpec JSON Formatter
|
# Vanilla RSpec JSON formatter with a slight extension to show example IDs.
|
||||||
# to give us an ID in its output
|
# TODO: Remove these lines when RSpec includes the ID natively
|
||||||
# TODO: remove once RSpec has IDs in stable (probably v3.3/v4.0)
|
class InspecRspecVanilla < RSpec::Core::Formatters::JsonFormatter
|
||||||
module RSpec::Core::Formatters
|
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
||||||
class JsonFormatter
|
|
||||||
private
|
|
||||||
|
|
||||||
def format_example(example)
|
private
|
||||||
{
|
|
||||||
description: example.description,
|
def format_example(example)
|
||||||
full_description: example.full_description,
|
res = super(example)
|
||||||
status: example.execution_result.status.to_s,
|
res[:id] = example.metadata[:id]
|
||||||
file_path: example.metadata['file_path'],
|
res
|
||||||
line_number: example.metadata['line_number'],
|
|
||||||
run_time: example.execution_result.run_time,
|
|
||||||
pending_message: example.execution_result.pending_message,
|
|
||||||
id: example.metadata[:id],
|
|
||||||
impact: example.metadata[:impact],
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
|
# Minimal JSON formatter for inspec. Only contains limited information about
|
||||||
|
# examples without any extras.
|
||||||
|
class InspecRspecMiniJson < RSpec::Core::Formatters::JsonFormatter
|
||||||
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
||||||
|
|
||||||
def add_profile(profile)
|
def dump_summary(summary)
|
||||||
@profiles ||= []
|
@output_hash[:version] = Inspec::VERSION
|
||||||
@profiles.push(profile)
|
@output_hash[:summary] = {
|
||||||
|
duration: summary.duration,
|
||||||
|
example_count: summary.example_count,
|
||||||
|
failure_count: summary.failure_count,
|
||||||
|
skip_count: summary.pending_count,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def dump_summary(summary)
|
def stop(notification)
|
||||||
super(summary)
|
@output_hash[:controls] = notification.examples.map do |example|
|
||||||
@output_hash[:profiles] = Array(@profiles).map do |profile|
|
format_example(example).tap do |hash|
|
||||||
r = profile.params.dup
|
e = example.exception
|
||||||
r.delete(:rules)
|
next unless e
|
||||||
r
|
hash[:message] = e.message
|
||||||
|
|
||||||
|
next if e.is_a? RSpec::Expectations::ExpectationNotMetError
|
||||||
|
hash[:exception] = e.class.name
|
||||||
|
hash[:backtrace] = e.backtrace
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -50,21 +53,71 @@ class InspecRspecFormatter < RSpec::Core::Formatters::JsonFormatter
|
||||||
def format_example(example)
|
def format_example(example)
|
||||||
res = {
|
res = {
|
||||||
id: example.metadata[:id],
|
id: example.metadata[:id],
|
||||||
title: example.metadata[:title],
|
|
||||||
desc: example.metadata[:desc],
|
|
||||||
code: example.metadata[:code],
|
|
||||||
impact: example.metadata[:impact],
|
|
||||||
status: example.execution_result.status.to_s,
|
status: example.execution_result.status.to_s,
|
||||||
code_desc: example.full_description,
|
code_desc: example.full_description,
|
||||||
ref: example.metadata['file_path'],
|
|
||||||
ref_line: example.metadata['line_number'],
|
|
||||||
run_time: example.execution_result.run_time,
|
|
||||||
start_time: example.execution_result.started_at.to_s,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# pending messages are embedded in the resources description
|
unless (pid = example.metadata[:profile_id]).nil?
|
||||||
res[:pending] = example.metadata[:description] if res[:status] == 'pending'
|
res[:profile_id] = pid
|
||||||
|
end
|
||||||
|
|
||||||
|
if res[:status] == 'pending'
|
||||||
|
res[:status] = 'skipped'
|
||||||
|
res[:skip_message] = example.metadata[:description]
|
||||||
|
res[:resource] = example.metadata[:described_class].to_s
|
||||||
|
end
|
||||||
|
|
||||||
res
|
res
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class InspecRspecJson < InspecRspecMiniJson
|
||||||
|
RSpec::Core::Formatters.register self, :message, :dump_summary, :dump_profile, :stop, :close
|
||||||
|
|
||||||
|
def add_profile(profile)
|
||||||
|
@profiles ||= []
|
||||||
|
@profiles.push(profile)
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump_one_example(example, profiles, missing)
|
||||||
|
profile = profiles[example[:profile_id]]
|
||||||
|
return missing.push(example) if profile.nil? || profile[:controls].nil?
|
||||||
|
|
||||||
|
control = profile[:controls][example[:id]]
|
||||||
|
return missing.push(example) if control.nil?
|
||||||
|
|
||||||
|
control[:results] ||= []
|
||||||
|
example.delete(:id)
|
||||||
|
example.delete(:profile_id)
|
||||||
|
control[:results].push(example)
|
||||||
|
end
|
||||||
|
|
||||||
|
def profile_info(profile)
|
||||||
|
info = profile.info.dup
|
||||||
|
[info[:name], info]
|
||||||
|
end
|
||||||
|
|
||||||
|
def dump_summary(summary)
|
||||||
|
super(summary)
|
||||||
|
@profiles ||= []
|
||||||
|
examples = @output_hash.delete(:controls)
|
||||||
|
profiles = Hash[@profiles.map { |x| profile_info(x) }]
|
||||||
|
missing = []
|
||||||
|
|
||||||
|
examples.each do |example|
|
||||||
|
dump_one_example(example, profiles, missing)
|
||||||
|
end
|
||||||
|
|
||||||
|
@output_hash[:profiles] = profiles
|
||||||
|
@output_hash[:other_checks] = missing
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def format_example(example)
|
||||||
|
super(example).tap do |res|
|
||||||
|
res[:run_time] = example.execution_result.run_time
|
||||||
|
res[:start_time] = example.execution_result.started_at.to_s
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -12,8 +12,7 @@ module Inspec
|
||||||
class Rule # rubocop:disable Metrics/ClassLength
|
class Rule # rubocop:disable Metrics/ClassLength
|
||||||
include ::RSpec::Matchers
|
include ::RSpec::Matchers
|
||||||
|
|
||||||
def initialize(id, _opts, &block)
|
def initialize(id, profile_id, _opts, &block)
|
||||||
@id = id
|
|
||||||
@impact = nil
|
@impact = nil
|
||||||
@title = nil
|
@title = nil
|
||||||
@desc = nil
|
@desc = nil
|
||||||
|
@ -24,7 +23,8 @@ module Inspec
|
||||||
@__block = block
|
@__block = block
|
||||||
@__code = __get_block_source(&block)
|
@__code = __get_block_source(&block)
|
||||||
@__source_location = __get_block_source_location(&block)
|
@__source_location = __get_block_source_location(&block)
|
||||||
@__rule_id = nil
|
@__rule_id = id
|
||||||
|
@__profile_id = profile_id
|
||||||
@__checks = []
|
@__checks = []
|
||||||
@__skip_rule = nil
|
@__skip_rule = nil
|
||||||
|
|
||||||
|
@ -119,6 +119,10 @@ module Inspec
|
||||||
rule.instance_variable_set(:@__rule_id, value)
|
rule.instance_variable_set(:@__rule_id, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.profile_id(rule)
|
||||||
|
rule.instance_variable_get(:@__profile_id)
|
||||||
|
end
|
||||||
|
|
||||||
def self.checks(rule)
|
def self.checks(rule)
|
||||||
rule.instance_variable_get(:@__checks)
|
rule.instance_variable_get(:@__checks)
|
||||||
end
|
end
|
||||||
|
@ -167,32 +171,6 @@ module Inspec
|
||||||
set_skip_rule(dst, sr) unless sr.nil?
|
set_skip_rule(dst, sr) unless sr.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get the full id consisting of profile id + rule id
|
|
||||||
# for the rule. If the rule's profile id is empty,
|
|
||||||
# the given profile_id will be used instead and also
|
|
||||||
# set for the rule.
|
|
||||||
def self.full_id(profile_id, rule)
|
|
||||||
if rule.is_a?(String) or rule.nil?
|
|
||||||
rid = rule
|
|
||||||
else
|
|
||||||
# As the profile context is exclusively pulled with a
|
|
||||||
# profile ID, attach it to the rule if necessary.
|
|
||||||
rid = rule.instance_variable_get(:@id)
|
|
||||||
if rid.nil?
|
|
||||||
# TODO: Message about skipping this rule
|
|
||||||
# due to missing ID
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
pid = rule_id(rule)
|
|
||||||
pid = set_rule_id(rule, profile_id) if pid.nil?
|
|
||||||
|
|
||||||
# if we don't have a profile id, just return the rule's ID
|
|
||||||
return rid if pid.nil? or pid.empty?
|
|
||||||
# otherwise combine them
|
|
||||||
"#{pid}/#{rid}"
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def __add_check(describe_or_expect, values, block)
|
def __add_check(describe_or_expect, values, block)
|
||||||
|
|
|
@ -18,7 +18,6 @@ module Inspec
|
||||||
attr_reader :backend, :rules
|
attr_reader :backend, :rules
|
||||||
def initialize(conf = {})
|
def initialize(conf = {})
|
||||||
@rules = {}
|
@rules = {}
|
||||||
@profile_id = conf[:id]
|
|
||||||
@conf = conf.dup
|
@conf = conf.dup
|
||||||
@conf[:logger] ||= Logger.new(nil)
|
@conf[:logger] ||= Logger.new(nil)
|
||||||
|
|
||||||
|
@ -74,6 +73,7 @@ module Inspec
|
||||||
|
|
||||||
@test_collector.add_profile(profile)
|
@test_collector.add_profile(profile)
|
||||||
options[:metadata] = profile.metadata
|
options[:metadata] = profile.metadata
|
||||||
|
options[:profile] = profile
|
||||||
|
|
||||||
libs = profile.libraries.map do |k, v|
|
libs = profile.libraries.map do |k, v|
|
||||||
{ ref: k, content: v }
|
{ ref: k, content: v }
|
||||||
|
@ -88,7 +88,10 @@ module Inspec
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_context(options = {})
|
def create_context(options = {})
|
||||||
Inspec::ProfileContext.new(@profile_id, @backend, @conf.merge(options))
|
meta = options['metadata']
|
||||||
|
profile_id = nil
|
||||||
|
profile_id = meta.params[:name] unless meta.nil?
|
||||||
|
Inspec::ProfileContext.new(profile_id, @backend, @conf.merge(options))
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_content(tests, libs, options = {})
|
def add_content(tests, libs, options = {})
|
||||||
|
@ -101,6 +104,11 @@ module Inspec
|
||||||
ctx.reload_dsl
|
ctx.reload_dsl
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# hand the context to the profile for further evaluation
|
||||||
|
unless (profile = options['profile']).nil?
|
||||||
|
profile.runner_context = ctx
|
||||||
|
end
|
||||||
|
|
||||||
# evaluate the test content
|
# evaluate the test content
|
||||||
tests = [tests] unless tests.is_a? Array
|
tests = [tests] unless tests.is_a? Array
|
||||||
tests.each { |t| add_test_to_context(t, ctx) }
|
tests.each { |t| add_test_to_context(t, ctx) }
|
||||||
|
@ -124,7 +132,10 @@ module Inspec
|
||||||
|
|
||||||
def filter_controls(controls_map, include_list)
|
def filter_controls(controls_map, include_list)
|
||||||
return controls_map if include_list.nil? || include_list.empty?
|
return controls_map if include_list.nil? || include_list.empty?
|
||||||
controls_map.select { |k, _| include_list.include?(k) }
|
controls_map.select do |_, c|
|
||||||
|
id = ::Inspec::Rule.rule_id(c)
|
||||||
|
include_list.include?(id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def block_source_info(block)
|
def block_source_info(block)
|
||||||
|
@ -186,7 +197,7 @@ module Inspec
|
||||||
# scope.
|
# scope.
|
||||||
dsl = Inspec::Resource.create_dsl(backend)
|
dsl = Inspec::Resource.create_dsl(backend)
|
||||||
example.send(:include, dsl)
|
example.send(:include, dsl)
|
||||||
@test_collector.add_test(example, rule_id, rule)
|
@test_collector.add_test(example, rule)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,7 @@ module Inspec
|
||||||
@profiles.push(profile)
|
@profiles.push(profile)
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_test(example, _rule_id, _rule)
|
def add_test(example, _rule)
|
||||||
@tests.push(example)
|
@tests.push(example)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,8 @@ require 'rspec/its'
|
||||||
require 'inspec/rspec_json_formatter'
|
require 'inspec/rspec_json_formatter'
|
||||||
|
|
||||||
# There be dragons!! Or borgs, or something...
|
# There be dragons!! Or borgs, or something...
|
||||||
# This file and all its contents cannot yet be tested. Once it is included
|
# This file and all its contents cannot be unit-tested. both test-suits
|
||||||
# in our unit test suite, it deactivates all other checks completely.
|
# collide and disable all unit tests that have been added.
|
||||||
# To circumvent this, we need functional tests which tackle the RSpec runner
|
|
||||||
# or a separate suite of unit tests to which get along with this.
|
|
||||||
|
|
||||||
module Inspec
|
module Inspec
|
||||||
class RunnerRspec
|
class RunnerRspec
|
||||||
|
@ -35,7 +33,7 @@ module Inspec
|
||||||
# @return [nil]
|
# @return [nil]
|
||||||
def add_profile(profile)
|
def add_profile(profile)
|
||||||
RSpec.configuration.formatters
|
RSpec.configuration.formatters
|
||||||
.find_all { |c| c.is_a? InspecRspecFormatter }
|
.find_all { |c| c.is_a? InspecRspecJson }
|
||||||
.each do |fmt|
|
.each do |fmt|
|
||||||
fmt.add_profile(profile)
|
fmt.add_profile(profile)
|
||||||
end
|
end
|
||||||
|
@ -46,8 +44,8 @@ module Inspec
|
||||||
# @param [RSpecExampleGroup] example test
|
# @param [RSpecExampleGroup] example test
|
||||||
# @param [String] rule_id the ID associated with this check
|
# @param [String] rule_id the ID associated with this check
|
||||||
# @return [nil]
|
# @return [nil]
|
||||||
def add_test(example, rule_id, rule)
|
def add_test(example, rule)
|
||||||
set_rspec_ids(example, rule_id, rule)
|
set_rspec_ids(example, rule)
|
||||||
@tests.example_groups.push(example)
|
@tests.example_groups.push(example)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -83,6 +81,12 @@ module Inspec
|
||||||
RSpec.configuration.reset
|
RSpec.configuration.reset
|
||||||
end
|
end
|
||||||
|
|
||||||
|
FORMATTERS = {
|
||||||
|
'json-min' => 'InspecRspecMiniJson',
|
||||||
|
'json' => 'InspecRspecJson',
|
||||||
|
'json-rspec' => 'InspecRspecVanilla',
|
||||||
|
}.freeze
|
||||||
|
|
||||||
# Configure the output formatter and stream to be used with RSpec.
|
# Configure the output formatter and stream to be used with RSpec.
|
||||||
#
|
#
|
||||||
# @return [nil]
|
# @return [nil]
|
||||||
|
@ -93,8 +97,7 @@ module Inspec
|
||||||
RSpec.configuration.output_stream = @conf['output']
|
RSpec.configuration.output_stream = @conf['output']
|
||||||
end
|
end
|
||||||
|
|
||||||
format = @conf['format'] || 'progress'
|
format = FORMATTERS[@conf['format']] || @conf['format'] || 'progress'
|
||||||
format = 'InspecRspecFormatter' if format == 'fulljson'
|
|
||||||
RSpec.configuration.add_formatter(format)
|
RSpec.configuration.add_formatter(format)
|
||||||
RSpec.configuration.color = @conf['color']
|
RSpec.configuration.color = @conf['color']
|
||||||
|
|
||||||
|
@ -111,27 +114,26 @@ module Inspec
|
||||||
# by the InSpec adjusted json formatter (rspec_json_formatter).
|
# by the InSpec adjusted json formatter (rspec_json_formatter).
|
||||||
#
|
#
|
||||||
# @param [RSpecExampleGroup] example object which contains a check
|
# @param [RSpecExampleGroup] example object which contains a check
|
||||||
# @param [Type] id describe id
|
|
||||||
# @return [Type] description of returned object
|
# @return [Type] description of returned object
|
||||||
def set_rspec_ids(example, id, rule)
|
def set_rspec_ids(example, rule)
|
||||||
example.metadata[:id] = id
|
assign_rspec_ids(example.metadata, rule)
|
||||||
example.metadata[:impact] = rule.impact
|
|
||||||
example.metadata[:title] = rule.title
|
|
||||||
example.metadata[:desc] = rule.desc
|
|
||||||
example.metadata[:code] = rule.instance_variable_get(:@__code)
|
|
||||||
example.metadata[:source_location] = rule.instance_variable_get(:@__source_location)
|
|
||||||
example.filtered_examples.each do |e|
|
example.filtered_examples.each do |e|
|
||||||
e.metadata[:id] = id
|
assign_rspec_ids(e.metadata, rule)
|
||||||
e.metadata[:impact] = rule.impact
|
|
||||||
e.metadata[:title] = rule.title
|
|
||||||
e.metadata[:desc] = rule.desc
|
|
||||||
e.metadata[:code] = rule.instance_variable_get(:@__code)
|
|
||||||
e.metadata[:source_location] = rule.instance_variable_get(:@__source_location)
|
|
||||||
end
|
end
|
||||||
example.children.each do |child|
|
example.children.each do |child|
|
||||||
set_rspec_ids(child, id, rule)
|
set_rspec_ids(child, rule)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def assign_rspec_ids(metadata, rule)
|
||||||
|
metadata[:id] = ::Inspec::Rule.rule_id(rule)
|
||||||
|
metadata[:profile_id] = ::Inspec::Rule.profile_id(rule)
|
||||||
|
metadata[:impact] = rule.impact
|
||||||
|
metadata[:title] = rule.title
|
||||||
|
metadata[:desc] = rule.desc
|
||||||
|
metadata[:code] = rule.instance_variable_get(:@__code)
|
||||||
|
metadata[:source_location] = rule.instance_variable_get(:@__source_location)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class RSpecReporter < RSpec::Core::Formatters::JsonFormatter
|
class RSpecReporter < RSpec::Core::Formatters::JsonFormatter
|
||||||
|
|
|
@ -45,8 +45,6 @@ module Inspec
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.exec_options
|
def self.exec_options
|
||||||
option :id, type: :string,
|
|
||||||
desc: 'Attach a profile ID to all test results'
|
|
||||||
target_options
|
target_options
|
||||||
profile_options
|
profile_options
|
||||||
option :controls, type: :array,
|
option :controls, type: :array,
|
||||||
|
|
|
@ -20,6 +20,7 @@ module FunctionalHelper
|
||||||
let(:examples_path) { File.join(repo_path, 'examples') }
|
let(:examples_path) { File.join(repo_path, 'examples') }
|
||||||
|
|
||||||
let(:example_profile) { File.join(examples_path, 'profile') }
|
let(:example_profile) { File.join(examples_path, 'profile') }
|
||||||
|
let(:example_control) { File.join(example_profile, 'controls', 'example.rb') }
|
||||||
let(:inheritance_profile) { File.join(examples_path, 'profile') }
|
let(:inheritance_profile) { File.join(examples_path, 'profile') }
|
||||||
|
|
||||||
let(:dst) {
|
let(:dst) {
|
||||||
|
|
|
@ -44,6 +44,6 @@ describe 'example inheritance profile' do
|
||||||
s = out.stdout
|
s = out.stdout
|
||||||
hm = JSON.load(s)
|
hm = JSON.load(s)
|
||||||
hm['name'].must_equal 'inheritance'
|
hm['name'].must_equal 'inheritance'
|
||||||
hm['rules'].length.must_equal 1 # TODO: flatten out or search deeper!
|
hm['controls'].length.must_equal 3
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
122
test/functional/inspec_exec_json_test.rb
Normal file
122
test/functional/inspec_exec_json_test.rb
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
# encoding: utf-8
|
||||||
|
# author: Dominik Richter
|
||||||
|
# author: Christoph Hartmann
|
||||||
|
|
||||||
|
require 'functional/helper'
|
||||||
|
|
||||||
|
describe 'inspec exec with json formatter' do
|
||||||
|
include FunctionalHelper
|
||||||
|
|
||||||
|
it 'can execute a simple file with the json formatter' do
|
||||||
|
out = inspec('exec ' + example_control + ' --format json')
|
||||||
|
out.stderr.must_equal ''
|
||||||
|
out.exit_status.must_equal 0
|
||||||
|
JSON.load(out.stdout).must_be_kind_of Hash
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'can execute the profile with the json formatter' do
|
||||||
|
out = inspec('exec ' + example_profile + ' --format json')
|
||||||
|
out.stderr.must_equal ''
|
||||||
|
out.exit_status.must_equal 0
|
||||||
|
JSON.load(out.stdout).must_be_kind_of Hash
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'execute a profile with json formatting' do
|
||||||
|
let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format json').stdout) }
|
||||||
|
let(:profile) { json['profiles']['profile'] }
|
||||||
|
let(:controls) { profile['controls'] }
|
||||||
|
let(:ex1) { controls['tmp-1.0'] }
|
||||||
|
let(:ex2) {
|
||||||
|
k = controls.keys.find { |x| x =~ /generated/ }
|
||||||
|
controls[k]
|
||||||
|
}
|
||||||
|
let(:ex3) { profile['controls']['gordon-1.0'] }
|
||||||
|
let(:check_result) {
|
||||||
|
ex3['results'].find { |x| x['resource'] == 'gordon_config' }
|
||||||
|
}
|
||||||
|
|
||||||
|
it 'has all the metadata' do
|
||||||
|
actual = profile.dup
|
||||||
|
key = actual.delete('controls').keys
|
||||||
|
.find { |x| x =~ /generated from example.rb/ }
|
||||||
|
|
||||||
|
actual.must_equal({
|
||||||
|
"name" => "profile",
|
||||||
|
"title" => "InSpec Example Profile",
|
||||||
|
"maintainer" => "Chef Software, Inc.",
|
||||||
|
"copyright" => "Chef Software, Inc.",
|
||||||
|
"copyright_email" => "support@chef.io",
|
||||||
|
"license" => "Apache 2 license",
|
||||||
|
"summary" => "Demonstrates the use of InSpec Compliance Profile",
|
||||||
|
"version" => "1.0.0",
|
||||||
|
"supports" => [{"os-family" => "unix"}],
|
||||||
|
"groups" => {
|
||||||
|
"controls/meta.rb" => {"title"=>"SSH Server Configuration", "controls"=>["ssh-1"]},
|
||||||
|
"controls/example.rb" => {"title"=>"/tmp profile", "controls"=>["tmp-1.0", key]},
|
||||||
|
"controls/gordon.rb" => {"title"=>"Gordon Config Checks", "controls"=>["gordon-1.0"]},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'must have 4 controls' do
|
||||||
|
controls.length.must_equal 4
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has an id for every control' do
|
||||||
|
controls.keys.find(&:nil?).must_be :nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has no missing checks' do
|
||||||
|
json['other_checks'].must_equal([])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has results for every control' do
|
||||||
|
ex1['results'].length.must_equal 1
|
||||||
|
ex2['results'].length.must_equal 1
|
||||||
|
ex3['results'].length.must_equal 2
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has the right result for tmp-1.0' do
|
||||||
|
actual = ex1.dup
|
||||||
|
|
||||||
|
src = actual.delete('source_location')
|
||||||
|
src[0].must_match %r{examples/profile/controls/example.rb$}
|
||||||
|
src[1].must_equal 8
|
||||||
|
|
||||||
|
result = actual.delete('results')[0]
|
||||||
|
result.wont_be :nil?
|
||||||
|
result['status'].must_equal 'passed'
|
||||||
|
result['code_desc'].must_equal 'File /tmp should be directory'
|
||||||
|
result['run_time'].wont_be :nil?
|
||||||
|
result['start_time'].wont_be :nil?
|
||||||
|
|
||||||
|
actual.must_equal({
|
||||||
|
"title" => "Create /tmp directory",
|
||||||
|
"desc" => "An optional description...",
|
||||||
|
"impact" => 0.7,
|
||||||
|
"refs" => [
|
||||||
|
{
|
||||||
|
"url" => "http://...",
|
||||||
|
"ref" => "Document A-12"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags" => {
|
||||||
|
"data" => "temp data",
|
||||||
|
"security" => nil
|
||||||
|
},
|
||||||
|
"code" => "control \"tmp-1.0\" do # A unique ID for this control\n impact 0.7 # The criticality, if this control fails.\n title \"Create /tmp directory\" # A human-readable title\n desc \"An optional description...\" # Describe why this is needed\n tag data: \"temp data\" # A tag allows you to associate key information\n tag \"security\" # to the test\n ref \"Document A-12\", url: 'http://...' # Additional references\n\n describe file('/tmp') do # The actual test\n it { should be_directory }\n end\nend\n",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'with a profile that is not supported on this OS/platform' do
|
||||||
|
let(:out) { inspec('exec ' + File.join(profile_path, 'skippy-profile-os') + ' --format json') }
|
||||||
|
let(:json) { JSON.load(out.stdout) }
|
||||||
|
|
||||||
|
# TODO: failure handling in json formatters...
|
||||||
|
|
||||||
|
it 'never runs the actual resource' do
|
||||||
|
File.exist?('/tmp/inspec_test_DONT_CREATE').must_equal false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,15 +22,13 @@ describe 'inspec exec' do
|
||||||
out.stdout.must_include '1 example, 0 failures'
|
out.stdout.must_include '1 example, 0 failures'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can execute the profile with the json formatter' do
|
it 'can execute the profile with the mini json formatter' do
|
||||||
out = inspec('exec ' + example_profile + ' --format json')
|
out = inspec('exec ' + example_profile + ' --format json-min')
|
||||||
out.stderr.must_equal ''
|
out.stderr.must_equal ''
|
||||||
out.exit_status.must_equal 0
|
out.exit_status.must_equal 0
|
||||||
JSON.load(out.stdout).must_be_kind_of Hash
|
JSON.load(out.stdout).must_be_kind_of Hash
|
||||||
end
|
end
|
||||||
|
|
||||||
let(:example_control) { File.join(example_profile, 'controls', 'example.rb') }
|
|
||||||
|
|
||||||
it 'can execute a simple file with the default formatter' do
|
it 'can execute a simple file with the default formatter' do
|
||||||
out = inspec('exec ' + example_control)
|
out = inspec('exec ' + example_control)
|
||||||
out.stderr.must_equal ''
|
out.stderr.must_equal ''
|
||||||
|
@ -38,126 +36,45 @@ describe 'inspec exec' do
|
||||||
out.stdout.must_include '2 examples, 0 failures'
|
out.stdout.must_include '2 examples, 0 failures'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can execute a simple file with the json formatter' do
|
it 'can execute a simple file with the mini json formatter' do
|
||||||
out = inspec('exec ' + example_control + ' --format json')
|
out = inspec('exec ' + example_control + ' --format json-min')
|
||||||
out.stderr.must_equal ''
|
out.stderr.must_equal ''
|
||||||
out.exit_status.must_equal 0
|
out.exit_status.must_equal 0
|
||||||
JSON.load(out.stdout).must_be_kind_of Hash
|
JSON.load(out.stdout).must_be_kind_of Hash
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can execute a simple file with the fulljson formatter' do
|
describe 'execute a profile with mini json formatting' do
|
||||||
out = inspec('exec ' + example_control + ' --format fulljson')
|
let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format json-min').stdout) }
|
||||||
out.stderr.must_equal ''
|
let(:controls) { json['controls'] }
|
||||||
out.exit_status.must_equal 0
|
let(:ex1) { controls.find{|x| x['id'] == 'tmp-1.0'} }
|
||||||
JSON.load(out.stdout).must_be_kind_of Hash
|
let(:ex2) { controls.find{|x| x['id'] =~ /generated/} }
|
||||||
end
|
let(:ex3) { controls.find{|x| x['id'] == 'gordon-1.0'} }
|
||||||
|
|
||||||
describe 'execute a profile with json formatting' do
|
|
||||||
let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format json').stdout) }
|
|
||||||
let(:examples) { json['examples'] }
|
|
||||||
let(:ex1) { examples.find{|x| x['id'] == 'tmp-1.0'} }
|
|
||||||
let(:ex2) { examples.find{|x| x['id'] =~ /generated/} }
|
|
||||||
let(:ex3) { examples.find{|x| x['id'] == 'gordon-1.0'} }
|
|
||||||
|
|
||||||
it 'must have 5 examples' do
|
it 'must have 5 examples' do
|
||||||
json['examples'].length.must_equal 5
|
json['controls'].length.must_equal 5
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'id in json' do
|
it 'has an id' do
|
||||||
examples.find { |ex| !ex.key? 'id' }.must_be :nil?
|
controls.find { |ex| !ex.key? 'id' }.must_be :nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'impact in json' do
|
it 'has a profile_id' do
|
||||||
ex1['impact'].must_equal 0.7
|
controls.find { |ex| !ex.key? 'profile_id' }.must_be :nil?
|
||||||
ex2['impact'].must_be :nil?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'status in json' do
|
it 'has a code_desc' do
|
||||||
|
ex1['code_desc'].must_equal 'File /tmp should be directory'
|
||||||
|
controls.find { |ex| !ex.key? 'code_desc' }.must_be :nil?
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'has a status' do
|
||||||
ex1['status'].must_equal 'passed'
|
ex1['status'].must_equal 'passed'
|
||||||
ex3['status'].must_equal 'pending'
|
ex3['status'].must_equal 'skipped'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'pending message in json' do
|
it 'has a skip_message' do
|
||||||
ex1['pending_message'].must_be :nil?
|
ex1['skip_message'].must_be :nil?
|
||||||
ex3['pending_message'].must_equal 'Not yet implemented'
|
ex3['skip_message'].must_equal "Can't find file \"/tmp/gordon/config.yaml\""
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'execute a profile with fulljson formatting' do
|
|
||||||
let(:json) { JSON.load(inspec('exec ' + example_profile + ' --format fulljson').stdout) }
|
|
||||||
let(:examples) { json['examples'] }
|
|
||||||
let(:metadata) { json['profiles'][0] }
|
|
||||||
let(:ex1) { examples.find{|x| x['id'] == 'tmp-1.0'} }
|
|
||||||
let(:ex2) { examples.find{|x| x['id'] =~ /generated/} }
|
|
||||||
let(:ex3) { examples.find{|x| x['id'] == 'gordon-1.0'} }
|
|
||||||
|
|
||||||
it 'has all the metadata' do
|
|
||||||
metadata.must_equal({
|
|
||||||
"name" => "profile",
|
|
||||||
"title" => "InSpec Example Profile",
|
|
||||||
"maintainer" => "Chef Software, Inc.",
|
|
||||||
"copyright" => "Chef Software, Inc.",
|
|
||||||
"copyright_email" => "support@chef.io",
|
|
||||||
"license" => "Apache 2 license",
|
|
||||||
"summary" => "Demonstrates the use of InSpec Compliance Profile",
|
|
||||||
"version" => "1.0.0",
|
|
||||||
"supports" => [{"os-family" => "unix"}]
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'must have 5 examples' do
|
|
||||||
json['examples'].length.must_equal 5
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'id in json' do
|
|
||||||
examples.find { |ex| !ex.key? 'id' }.must_be :nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'title in json' do
|
|
||||||
ex3['title'].must_equal 'Verify the version number of Gordon'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'desc in json' do
|
|
||||||
ex3['desc'].must_equal 'An optional description...'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'code in json' do
|
|
||||||
ex3['code'].wont_be :nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'code_desc in json' do
|
|
||||||
ex3['code_desc'].wont_be :nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'impact in json' do
|
|
||||||
ex1['impact'].must_equal 0.7
|
|
||||||
ex2['impact'].must_be :nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'status in json' do
|
|
||||||
ex1['status'].must_equal 'passed'
|
|
||||||
ex3['status'].must_equal 'pending'
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'ref in json' do
|
|
||||||
ex1['ref'].must_match %r{examples/profile/controls/example.rb$}
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'ref_line in json' do
|
|
||||||
ex1['ref_line'].must_equal 16
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'run_time in json' do
|
|
||||||
ex1['run_time'].wont_be :nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'start_time in json' do
|
|
||||||
ex1['start_time'].wont_be :nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'pending message in json' do
|
|
||||||
ex1['pending'].must_be :nil?
|
|
||||||
ex3['pending'].must_equal "Can't find file \"/tmp/gordon/config.yaml\""
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -171,17 +88,6 @@ describe 'inspec exec' do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'with a profile that is not supported on this OS/platform' do
|
|
||||||
let(:out) { inspec('exec ' + File.join(profile_path, 'skippy-profile-os') + ' --format fulljson') }
|
|
||||||
let(:json) { JSON.load(out.stdout) }
|
|
||||||
|
|
||||||
# TODO: failure handling in json formatters...
|
|
||||||
|
|
||||||
it 'never runs the actual resource' do
|
|
||||||
File.exist?('/tmp/inspec_test_DONT_CREATE').must_equal false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'with a profile that is supported on this version of inspec' do
|
describe 'with a profile that is supported on this version of inspec' do
|
||||||
let(:out) { inspec('exec ' + File.join(profile_path, 'supported_inspec')) }
|
let(:out) { inspec('exec ' + File.join(profile_path, 'supported_inspec')) }
|
||||||
|
|
||||||
|
|
|
@ -42,36 +42,36 @@ describe 'inspec json' do
|
||||||
json['copyright'].must_equal 'Chef Software, Inc.'
|
json['copyright'].must_equal 'Chef Software, Inc.'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has rules' do
|
it 'has controls' do
|
||||||
json['rules'].length.must_equal 3 # TODO: flatten out or search deeper!
|
json['controls'].length.must_equal 4
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'a rule' do
|
describe 'a control' do
|
||||||
let(:rule) { json['rules']['controls/example.rb']['rules']['tmp-1.0'] }
|
let(:control) { json['controls']['tmp-1.0'] }
|
||||||
|
|
||||||
it 'has a title' do
|
it 'has a title' do
|
||||||
rule['title'].must_equal 'Create /tmp directory'
|
control['title'].must_equal 'Create /tmp directory'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has a description' do
|
it 'has a description' do
|
||||||
rule['desc'].must_equal 'An optional description...'
|
control['desc'].must_equal 'An optional description...'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has an impact' do
|
it 'has an impact' do
|
||||||
rule['impact'].must_equal 0.7
|
control['impact'].must_equal 0.7
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has a ref' do
|
it 'has a ref' do
|
||||||
rule['refs'].must_equal([{'ref' => 'Document A-12', 'url' => 'http://...'}])
|
control['refs'].must_equal([{'ref' => 'Document A-12', 'url' => 'http://...'}])
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has a source location' do
|
it 'has a source location' do
|
||||||
loc = File.join(example_profile, '/controls/example.rb')
|
loc = File.join(example_profile, '/controls/example.rb')
|
||||||
rule['source_location'].must_equal [loc, 8]
|
control['source_location'].must_equal [loc, 8]
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has a the source code' do
|
it 'has a the source code' do
|
||||||
rule['code'].must_match /\Acontrol \"tmp-1.0\" do.*end\n\Z/m
|
control['code'].must_match /\Acontrol \"tmp-1.0\" do.*end\n\Z/m
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -86,10 +86,8 @@ describe 'inspec json' do
|
||||||
|
|
||||||
it 'only has one control included' do
|
it 'only has one control included' do
|
||||||
json = JSON.load(out.stdout)
|
json = JSON.load(out.stdout)
|
||||||
grps = json['rules']
|
json['controls'].keys.must_equal %w{tmp-1.0}
|
||||||
grps.keys.must_equal ['controls/example.rb']
|
json['groups'].keys.must_equal %w{controls/example.rb}
|
||||||
rules = grps.values[0]['rules']
|
|
||||||
rules.keys.must_equal ['tmp-1.0']
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,6 +97,6 @@ describe 'inspec json' do
|
||||||
out.exit_status.must_equal 0
|
out.exit_status.must_equal 0
|
||||||
hm = JSON.load(File.read(dst.path))
|
hm = JSON.load(File.read(dst.path))
|
||||||
hm['name'].must_equal 'profile'
|
hm['name'].must_equal 'profile'
|
||||||
hm['rules'].length.must_equal 3 # TODO: flatten out or search deeper!
|
hm['controls'].length.must_equal 4
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -12,7 +12,7 @@ describe 'controls' do
|
||||||
}
|
}
|
||||||
opts = { test_collector: Inspec::RunnerMock.new }
|
opts = { test_collector: Inspec::RunnerMock.new }
|
||||||
Inspec::Profile.for_target(data, opts)
|
Inspec::Profile.for_target(data, opts)
|
||||||
.params[:rules].values[0]['1']
|
.params[:controls]['1']
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'works with empty refs' do
|
it 'works with empty refs' do
|
||||||
|
|
|
@ -177,13 +177,13 @@ describe Inspec::ProfileContext do
|
||||||
|
|
||||||
it 'provides the control keyword in the global DSL' do
|
it 'provides the control keyword in the global DSL' do
|
||||||
profile.load('control 1')
|
profile.load('control 1')
|
||||||
profile.rules.keys.must_equal [1]
|
profile.rules.keys.must_equal ['1']
|
||||||
profile.rules.values[0].must_be_kind_of Inspec::Rule
|
profile.rules.values[0].must_be_kind_of Inspec::Rule
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'provides the rule keyword in the global DSL (legacy mode)' do
|
it 'provides the rule keyword in the global DSL (legacy mode)' do
|
||||||
profile.load('rule 1')
|
profile.load('rule 1')
|
||||||
profile.rules.keys.must_equal [1]
|
profile.rules.keys.must_equal ['1']
|
||||||
profile.rules.values[0].must_be_kind_of Inspec::Rule
|
profile.rules.values[0].must_be_kind_of Inspec::Rule
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,8 +16,8 @@ describe Inspec::Profile do
|
||||||
profile.params[:name].must_be_nil
|
profile.params[:name].must_be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has no rules' do
|
it 'has no controls' do
|
||||||
profile.params[:rules].must_equal({})
|
profile.params[:controls].must_equal({})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,8 +28,8 @@ describe Inspec::Profile do
|
||||||
profile.params[:name].must_be_nil
|
profile.params[:name].must_be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has no rules' do
|
it 'has no controls' do
|
||||||
profile.params[:rules].must_equal({})
|
profile.params[:controls].must_equal({})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -41,8 +41,8 @@ describe Inspec::Profile do
|
||||||
profile.params[:name].must_equal 'yumyum profile'
|
profile.params[:name].must_equal 'yumyum profile'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has no rules' do
|
it 'has no controls' do
|
||||||
profile.params[:rules].must_equal({})
|
profile.params[:controls].must_equal({})
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can overwrite the profile ID' do
|
it 'can overwrite the profile ID' do
|
||||||
|
@ -59,8 +59,8 @@ describe Inspec::Profile do
|
||||||
profile.params[:name].must_equal 'metadata profile'
|
profile.params[:name].must_equal 'metadata profile'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'has no rules' do
|
it 'has no controls' do
|
||||||
profile.params[:rules].must_equal({})
|
profile.params[:controls].must_equal({})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -179,11 +179,10 @@ describe Inspec::Profile do
|
||||||
describe 'a complete metadata profile with controls' do
|
describe 'a complete metadata profile with controls' do
|
||||||
let(:profile_id) { 'complete-profile' }
|
let(:profile_id) { 'complete-profile' }
|
||||||
|
|
||||||
it 'prints ok messages and counts the rules' do
|
it 'prints ok messages and counts the controls' do
|
||||||
logger.expect :info, nil, ["Checking profile in #{home}/mock/profiles/#{profile_id}"]
|
logger.expect :info, nil, ["Checking profile in #{home}/mock/profiles/#{profile_id}"]
|
||||||
logger.expect :info, nil, ['Metadata OK.']
|
logger.expect :info, nil, ['Metadata OK.']
|
||||||
logger.expect :info, nil, ['Found 1 controls.']
|
logger.expect :info, nil, ['Found 1 controls.']
|
||||||
logger.expect :info, nil, ["Verify all controls in controls/filesystem_spec.rb"]
|
|
||||||
logger.expect :info, nil, ['Control definitions OK.']
|
logger.expect :info, nil, ['Control definitions OK.']
|
||||||
|
|
||||||
result = MockLoader.load_profile(profile_id, {logger: logger}).check
|
result = MockLoader.load_profile(profile_id, {logger: logger}).check
|
||||||
|
@ -205,11 +204,10 @@ describe Inspec::Profile do
|
||||||
let(:profile_path) { MockLoader.profile_tgz(profile_id) }
|
let(:profile_path) { MockLoader.profile_tgz(profile_id) }
|
||||||
let(:profile) { MockLoader.load_profile(profile_path, {logger: logger}) }
|
let(:profile) { MockLoader.load_profile(profile_path, {logger: logger}) }
|
||||||
|
|
||||||
it 'prints ok messages and counts the rules' do
|
it 'prints ok messages and counts the controls' do
|
||||||
logger.expect :info, nil, ["Checking profile in #{home}/mock/profiles/#{profile_id}"]
|
logger.expect :info, nil, ["Checking profile in #{home}/mock/profiles/#{profile_id}"]
|
||||||
logger.expect :info, nil, ['Metadata OK.']
|
logger.expect :info, nil, ['Metadata OK.']
|
||||||
logger.expect :info, nil, ['Found 1 controls.']
|
logger.expect :info, nil, ['Found 1 controls.']
|
||||||
logger.expect :info, nil, ["Verify all controls in controls/filesystem_spec.rb"]
|
|
||||||
logger.expect :info, nil, ['Control definitions OK.']
|
logger.expect :info, nil, ['Control definitions OK.']
|
||||||
|
|
||||||
result = MockLoader.load_profile(profile_id, {logger: logger}).check
|
result = MockLoader.load_profile(profile_id, {logger: logger}).check
|
||||||
|
@ -231,11 +229,10 @@ describe Inspec::Profile do
|
||||||
let(:profile_path) { MockLoader.profile_zip(profile_id) }
|
let(:profile_path) { MockLoader.profile_zip(profile_id) }
|
||||||
let(:profile) { MockLoader.load_profile(profile_path, {logger: logger}) }
|
let(:profile) { MockLoader.load_profile(profile_path, {logger: logger}) }
|
||||||
|
|
||||||
it 'prints ok messages and counts the rules' do
|
it 'prints ok messages and counts the controls' do
|
||||||
logger.expect :info, nil, ["Checking profile in #{home}/mock/profiles/#{profile_id}"]
|
logger.expect :info, nil, ["Checking profile in #{home}/mock/profiles/#{profile_id}"]
|
||||||
logger.expect :info, nil, ['Metadata OK.']
|
logger.expect :info, nil, ['Metadata OK.']
|
||||||
logger.expect :info, nil, ['Found 1 controls.']
|
logger.expect :info, nil, ['Found 1 controls.']
|
||||||
logger.expect :info, nil, ["Verify all controls in controls/filesystem_spec.rb"]
|
|
||||||
logger.expect :info, nil, ['Control definitions OK.']
|
logger.expect :info, nil, ['Control definitions OK.']
|
||||||
|
|
||||||
result = MockLoader.load_profile(profile_id, {logger: logger}).check
|
result = MockLoader.load_profile(profile_id, {logger: logger}).check
|
||||||
|
|
Loading…
Reference in a new issue