class Inspec::Rule

Attributes

__profile_id[R]
__waiver_data[R]
resource_dsl[RW]

Public Class Methods

checks(rule) click to toggle source
# File lib/inspec/rule.rb, line 247
def self.checks(rule)
  rule.instance_variable_get(:@__checks)
end
merge(dst, src) click to toggle source
# File lib/inspec/rule.rb, line 289
def self.merge(dst, src) # rubocop:disable Metrics/AbcSize
  if src.id != dst.id
    # TODO: register an error, this case should not happen
    return
  end

  sp = rule_id(src)
  dp = rule_id(dst)
  if sp != dp
    # TODO: register an error, this case should not happen
    return
  end

  # merge all fields
  dst.impact(src.impact)                 unless src.impact.nil?
  dst.title(src.title)                   unless src.title.nil?
  dst.descriptions(src.descriptions)     unless src.descriptions.nil?
  dst.tag(src.tag)                       unless src.tag.nil?
  dst.ref(src.ref)                       unless src.ref.nil?

  # merge indirect fields
  # checks defined in the source will completely eliminate
  # all checks that were defined in the destination
  sc = checks(src)
  dst.instance_variable_set(:@__checks, sc) unless sc.empty?
  skip_check = skip_status(src)
  sr = skip_check[:result]
  msg = skip_check[:message]
  skip_type = skip_check[:type]
  set_skip_rule(dst, sr, msg, skip_type) unless sr.nil?

  # Save merge history
  dst.instance_variable_set(:@__merge_count, merge_count(dst) + 1)
  dst.instance_variable_set(
    :@__merge_changes,
    merge_changes(dst) << src.instance_variable_get(:@__source_location)
  )
end
merge_changes(rule) click to toggle source
# File lib/inspec/rule.rb, line 268
def self.merge_changes(rule)
  rule.instance_variable_get(:@__merge_changes)
end
merge_count(rule) click to toggle source
# File lib/inspec/rule.rb, line 264
def self.merge_count(rule)
  rule.instance_variable_get(:@__merge_count)
end
new(id, profile_id, resource_dsl, opts, &block) click to toggle source
# File lib/inspec/rule.rb, line 20
def initialize(id, profile_id, resource_dsl, opts, &block)
  @impact = nil
  @title = nil
  @descriptions = {}
  @refs = []
  @tags = {}

  @resource_dsl = resource_dsl
  extend resource_dsl # TODO: remove! do it via method_missing

  # not changeable by the user:
  @__code = nil
  @__block = block
  @__source_location = __get_block_source_location(&block)
  @__rule_id = id
  @__profile_id = profile_id
  @__checks = []
  @__skip_rule = {} # { result: true, message: "Why", type: [:only_if, :waiver] }
  @__merge_count = 0
  @__merge_changes = []
  @__skip_only_if_eval = opts[:skip_only_if_eval]

  # evaluate the given definition
  return unless block_given?

  begin
    instance_eval(&block)

    # By applying waivers *after* the instance eval, we assure that
    # waivers have higher precedence than only_if.
    __apply_waivers

  rescue SystemStackError, StandardError => e
    # We've encountered an exception while trying to eval the code inside the
    # control block. We need to prevent the exception from bubbling up, and
    # fail the control. Controls are failed by having a failed resource within
    # them; but since our control block is unsafe (and opaque) to us, let's
    # make a dummy and fail that.
    location = block.source_location.compact.join(":")
    describe "Control Source Code Error" do
      # Rubocop thinks we are raising an exception - we're actually calling RSpec's fail()
      its(location) { fail e.message } # rubocop: disable Style/SignalException
    end
  end
end
prepare_checks(rule) click to toggle source

If a rule is marked to be skipped, this creates a dummay array of “checks” with a skip outcome

# File lib/inspec/rule.rb, line 274
def self.prepare_checks(rule)
  skip_check = skip_status(rule)
  return checks(rule) unless skip_check[:result].eql?(true)

  if skip_check[:message]
    msg = "Skipped control due to #{skip_check[:type]} condition: #{skip_check[:message]}"
  else
    msg = "Skipped control due to #{skip_check[:type]} condition."
  end

  resource = rule.noop
  resource.skip_resource(msg)
  [["describe", [resource], nil]]
end
profile_id(rule) click to toggle source
# File lib/inspec/rule.rb, line 243
def self.profile_id(rule)
  rule.instance_variable_get(:@__profile_id)
end
rule_id(rule) click to toggle source

TODO: figure out why these violations exist and nuke them.

# File lib/inspec/rule.rb, line 235
def self.rule_id(rule)
  rule.instance_variable_get(:@__rule_id)
end
set_rule_id(rule, value) click to toggle source
# File lib/inspec/rule.rb, line 239
def self.set_rule_id(rule, value)
  rule.instance_variable_set(:@__rule_id, value)
end
set_skip_rule(rule, value, message = nil, type = :only_if) click to toggle source
# File lib/inspec/rule.rb, line 255
def self.set_skip_rule(rule, value, message = nil, type = :only_if)
  rule.instance_variable_set(:@__skip_rule,
                             {
                               result: value,
                               message: message,
                               type: type,
                             })
end
skip_status(rule) click to toggle source
# File lib/inspec/rule.rb, line 251
def self.skip_status(rule)
  rule.instance_variable_get(:@__skip_rule)
end

Public Instance Methods

attribute(name, options = {}) click to toggle source
# File lib/inspec/rule.rb, line 201
def attribute(name, options = {})
  Inspec.deprecate(:attrs_dsl, "Input name: #{name}, Profile: #{__profile_id}")
  input(name, options)
end
desc(v = nil, data = nil) click to toggle source
# File lib/inspec/rule.rb, line 90
def desc(v = nil, data = nil)
  return @descriptions[:default] if v.nil?

  if data.nil?
    @descriptions[:default] = unindent(v)
  else
    @descriptions[v.to_sym] = unindent(data)
  end
end
describe(*values, &block) click to toggle source

Describe will add one or more tests to this control. There is 2 ways of calling it:

describe resource do ... end

or

describe.one do ... end

@param [any] Resource to be describe, string, or nil @param [Proc] An optional block containing tests for the described resource @return [nil|DescribeBase] if called without arguments, returns DescribeBase

# File lib/inspec/rule.rb, line 157
def describe(*values, &block)
  if values.empty? && !block_given?
    dsl = resource_dsl
    Class.new(DescribeBase) do
      include dsl
    end.new(method(:__add_check))
  else
    __add_check("describe", values, with_dsl(block))
  end
end
descriptions(description_hash = nil) click to toggle source
# File lib/inspec/rule.rb, line 100
def descriptions(description_hash = nil)
  return @descriptions if description_hash.nil?

  @descriptions.merge!(description_hash)
end
expect(value, &block) click to toggle source
# File lib/inspec/rule.rb, line 168
def expect(value, &block)
  target = Inspec::Expect.new(value, &with_dsl(block))
  __add_check("expect", [value], target)
  target
end
id(*_) click to toggle source
# File lib/inspec/rule.rb, line 70
def id(*_)
  # never overwrite the ID
  @id
end
impact(v = nil) click to toggle source
# File lib/inspec/rule.rb, line 75
def impact(v = nil)
  if v.is_a?(String)
    @impact = Inspec::Impact.impact_from_string(v)
  elsif !v.nil?
    @impact = v
  end

  @impact
end
input(input_name, options = {}) click to toggle source

allow attributes to be accessed within control blocks

# File lib/inspec/rule.rb, line 175
def input(input_name, options = {})
  if options.empty?
    # Simply an access, no event here
    Inspec::InputRegistry.find_or_register_input(input_name, __profile_id).value
  else
    options[:priority] ||= 20
    options[:provider] = :inline_control_code
    evt = Inspec::Input.infer_event(options)
    Inspec::InputRegistry.find_or_register_input(
      input_name,
      __profile_id,
      type: options[:type],
      required: options[:required],
      description: options[:description],
      pattern: options[:pattern],
      event: evt
    ).value
  end
end
input_object(input_name) click to toggle source

Find the Input object, but don't collapse to a value. Will return nil on a miss.

# File lib/inspec/rule.rb, line 197
def input_object(input_name)
  Inspec::InputRegistry.find_or_register_input(input_name, __profile_id)
end
method_missing(method_name, *arguments, &block) click to toggle source

Support for Control DSL plugins. This is called when an unknown method is encountered within a control block.

Calls superclass method
# File lib/inspec/rule.rb, line 209
def method_missing(method_name, *arguments, &block)
  # Check to see if there is a control_dsl plugin activator hook with the method name
  registry = Inspec::Plugin::V2::Registry.instance
  hook = registry.find_activators(plugin_type: :control_dsl, activator_name: method_name).first
  if hook
    # OK, load the hook if it hasn't been already.  We'll then know a module,
    # which we can then inject into the context
    hook.activate

    # Inject the module's methods into the context.
    # implementation_class is the field name, but this is actually a module.
    self.class.include(hook.implementation_class)
    # Now that the module is loaded, it defined one or more methods
    # (presumably the one we were looking for.)
    # We still haven't called it, so do so now.
    send(method_name, *arguments, &block)
  else
    begin
      Inspec::DSL.method_missing_resource(inspec, method_name, *arguments)
    rescue LoadError
      super
    end
  end
end
only_if(message = nil) { || ... } click to toggle source

Skip all checks if only_if is false

@param [Type] &block returns true if tests are added, false otherwise @return [nil]

# File lib/inspec/rule.rb, line 136
def only_if(message = nil)
  return unless block_given?
  return if @__skip_only_if_eval == true

  @__skip_rule[:result] ||= !yield
  @__skip_rule[:type] = :only_if
  @__skip_rule[:message] = message
end
ref(ref = nil, opts = {}) click to toggle source
# File lib/inspec/rule.rb, line 106
def ref(ref = nil, opts = {})
  return @refs if ref.nil? && opts.empty?

  if opts.empty? && ref.is_a?(Hash)
    opts = ref
  else
    opts[:ref] = ref
  end
  @refs.push(opts)
end
source_file() click to toggle source
# File lib/inspec/rule.rb, line 128
def source_file
  @__file
end
tag(*args) click to toggle source
# File lib/inspec/rule.rb, line 117
def tag(*args)
  args.each do |arg|
    if arg.is_a?(Hash)
      @tags.merge!(arg)
    else
      @tags[arg] ||= nil
    end
  end
  @tags
end
title(v = nil) click to toggle source
# File lib/inspec/rule.rb, line 85
def title(v = nil)
  @title = v unless v.nil?
  @title
end
to_s() click to toggle source
# File lib/inspec/rule.rb, line 66
def to_s
  Inspec::Rule.rule_id(self)
end

Private Instance Methods

__add_check(describe_or_expect, values, block) click to toggle source
# File lib/inspec/rule.rb, line 330
def __add_check(describe_or_expect, values, block)
  @__checks.push([describe_or_expect, values, block])
end
__apply_waivers() click to toggle source

Look for an input with a matching ID, and if found, apply waiver skipping logic. Basically, if we have a current waiver, and it says to skip, we'll replace all the checks with a dummy check (same as only_if mechanism) Double underscore: not intended to be called as part of the DSL

# File lib/inspec/rule.rb, line 339
def __apply_waivers
  input_name = @__rule_id # TODO: control ID slugging
  registry = Inspec::InputRegistry.instance
  input = registry.inputs_by_profile.dig(__profile_id, input_name)
  return unless input && input.has_value? && input.value.is_a?(Hash)

  # An InSpec Input is a datastructure that tracks a profile parameter
  # over time. Its value can be set by many sources, and it keeps a
  # log of each "set" event so that when it is collapsed to a value,
  # it can determine the correct (highest priority) value.
  # Store in an instance variable for.. later reading???
  @__waiver_data = input.value
  __waiver_data["skipped_due_to_waiver"] = false
  __waiver_data["message"] = ""

  # Does it have an expiration date, and if so, is it in the future?
  # This sets a waiver message before checking `run: true`
  expiry = __waiver_data["expiration_date"]
  if expiry
    # YAML will automagically give us a Date or a Time.
    # If transcoding YAML between languages (e.g. Go) the date might have also ended up as a String.
    # A string that does not represent a valid time results in the date 0000-01-01.
    if [Date, Time].include?(expiry.class) || (expiry.is_a?(String) && Time.new(expiry).year != 0)
      expiry = expiry.to_time if expiry.is_a? Date
      expiry = Time.parse(expiry) if expiry.is_a? String
      if expiry < Time.now # If the waiver expired, return - no skip applied
        __waiver_data["message"] = "Waiver expired on #{expiry}, evaluating control normally"
        return
      end
    else
      ui = Inspec::UI.new
      ui.error("Unable to parse waiver expiration date '#{expiry}' for control #{@__rule_id}")
      ui.exit(:usage_error)
    end
  end

  # Waivers should have a hash value with keys possibly including "run" and
  # expiration_date. We only care here if it has a "run" key and it
  # is false-like, since all non-skipped waiver operations are handled
  # during reporting phase.
  return unless __waiver_data.key?("run") && !__waiver_data["run"]

  # OK, apply a skip.
  @__skip_rule[:result] = true
  @__skip_rule[:type] = :waiver
  @__skip_rule[:message] = __waiver_data["justification"]
  __waiver_data["skipped_due_to_waiver"] = true
end
__get_block_source_location(&block) click to toggle source

get the source location of the block

# File lib/inspec/rule.rb, line 433
def __get_block_source_location(&block)
  return {} unless block_given?

  r, l = block.source_location
  { ref: r, line: l }
rescue MethodSource::SourceNotFoundError
  {}
end
unindent(text) click to toggle source

Idio(ma)tic unindent, behaves similar to Ruby2.3 curly heredocs. Find the shortest indentation of non-empty lines and strip that from every line See: bugs.ruby-lang.org/issues/9098

It is implemented here to support pre-Ruby2.3 with this feature and to not force non-programmers to understand heredocs.

Please note: tabs are not supported! (they will be removed but they are not treated the same as in Ruby2.3 heredocs)

@param [String] text string which needs to be unindented @return [String] input with indentation removed; '' if input is nil

# File lib/inspec/rule.rb, line 425
def unindent(text)
  return "" if text.nil?

  len = text.split("\n").reject { |l| l.strip.empty? }.map { |x| x.index(/[^\s]/) }.compact.min
  text.gsub(/^[[:blank:]]{#{len}}/, "").strip
end
with_dsl(block) click to toggle source

Takes a block and returns a block that will run the given block with access to the resource_dsl of the current class. This is to ensure that inside the constructed Rspec::ExampleGroup users have access to DSL methods. Previous this was done in Inspec::Runner before sending the example groups to rspec. It was moved here to ensure that code inside `its` blocks hae the same visibility into resources as code outside its blocks.

@param [Proc] block @return [Proc]

# File lib/inspec/rule.rb, line 400
def with_dsl(block)
  return nil if block.nil?

  dsl = resource_dsl

  return block unless dsl

  proc do |*args|
    include dsl
    instance_exec(*args, &block)
  end
end