Class: Inspec::Rule

Inherits:
Object
  • Object
show all
Includes:
RSpec::Matchers
Defined in:
lib/inspec/rule.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(id, profile_id, resource_dsl, opts, &block) ⇒ Rule

Returns a new instance of Rule.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/inspec/rule.rb', line 24

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]
  @__na_rule = {}

  # evaluate the given definition
  return unless block_given?

  begin
    # Pre-check: apply waivers before evaluating the control block.
    # If the control is waived with run: false, skip the block entirely
    # to avoid eager resource evaluation (e.g., `command('find /').stdout`
    # executing expensive commands for waived controls).
    __apply_waivers

    unless @__skip_rule[:result] && @__skip_rule[:type] == :waiver
      instance_eval(&block)

      # Re-apply waivers after instance eval. This is a no-op in practice:
      # run:false waivers are already handled by the pre-check above (the
      # unless guard prevents instance_eval from running at all), and
      # run:true / no-run-key waivers do not set a skip flag. Kept for
      # defensive correctness in case waiver state changes during eval.
      __apply_waivers
    end

  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

    # instance_eval evaluates the describe block and raise errors if at the resource level any execution is failed
    # Waived controls expect not to raise any controls and get skipped if run is false so __apply_waivers needs to be called here too
    # so that waived control are actually gets waived.
    __apply_waivers
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *arguments, &block) ⇒ Object

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



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/inspec/rule.rb', line 246

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

Instance Attribute Details

#__profile_idObject (readonly)

Returns the value of attribute __profile_id.



22
23
24
# File 'lib/inspec/rule.rb', line 22

def __profile_id
  @__profile_id
end

#__waiver_dataObject (readonly)

Returns the value of attribute __waiver_data.



20
21
22
# File 'lib/inspec/rule.rb', line 20

def __waiver_data
  @__waiver_data
end

#na_impact_freezeObject

Returns the value of attribute na_impact_freeze.



21
22
23
# File 'lib/inspec/rule.rb', line 21

def na_impact_freeze
  @na_impact_freeze
end

#resource_dslObject

Returns the value of attribute resource_dsl.



21
22
23
# File 'lib/inspec/rule.rb', line 21

def resource_dsl
  @resource_dsl
end

Class Method Details

.checks(rule) ⇒ Object



284
285
286
# File 'lib/inspec/rule.rb', line 284

def self.checks(rule)
  rule.instance_variable_get(:@__checks)
end

.merge(dst, src) ⇒ Object

rubocop:disable Metrics/AbcSize



340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
# File 'lib/inspec/rule.rb', line 340

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) ⇒ Object



309
310
311
# File 'lib/inspec/rule.rb', line 309

def self.merge_changes(rule)
  rule.instance_variable_get(:@__merge_changes)
end

.merge_count(rule) ⇒ Object



305
306
307
# File 'lib/inspec/rule.rb', line 305

def self.merge_count(rule)
  rule.instance_variable_get(:@__merge_count)
end

.na_status(rule) ⇒ Object



292
293
294
# File 'lib/inspec/rule.rb', line 292

def self.na_status(rule)
  rule.instance_variable_get(:@__na_rule)
end

.prepare_checks(rule) ⇒ Object

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



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/inspec/rule.rb', line 315

def self.prepare_checks(rule)
  skip_check = skip_status(rule)
  na_check = na_status(rule)
  return checks(rule) unless skip_check[:result].eql?(true) || na_check[:result].eql?(true)

  resource = rule.noop
  if 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.skip_resource(msg)
  else
    if na_check[:message]
      msg = "N/A control due to #{na_check[:type]} condition: #{na_check[:message]}"
    else
      msg = "N/A control due to #{na_check[:type]} condition."
    end
    resource.fail_resource(msg)
  end

  [["describe", [resource], nil]]
end

.profile_id(rule) ⇒ Object



280
281
282
# File 'lib/inspec/rule.rb', line 280

def self.profile_id(rule)
  rule.instance_variable_get(:@__profile_id)
end

.rule_id(rule) ⇒ Object

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



272
273
274
# File 'lib/inspec/rule.rb', line 272

def self.rule_id(rule)
  rule.instance_variable_get(:@__rule_id)
end

.set_rule_id(rule, value) ⇒ Object



276
277
278
# File 'lib/inspec/rule.rb', line 276

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) ⇒ Object



296
297
298
299
300
301
302
303
# File 'lib/inspec/rule.rb', line 296

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) ⇒ Object



288
289
290
# File 'lib/inspec/rule.rb', line 288

def self.skip_status(rule)
  rule.instance_variable_get(:@__skip_rule)
end

Instance Method Details

#attribute(name, options = {}) ⇒ Object



238
239
240
241
# File 'lib/inspec/rule.rb', line 238

def attribute(name, options = {})
  Inspec.deprecate(:attrs_dsl, "Input name: #{name}, Profile: #{__profile_id}")
  input(name, options)
end

#desc(v = nil, data = nil) ⇒ Object



114
115
116
117
118
119
120
121
122
# File 'lib/inspec/rule.rb', line 114

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) ⇒ nil|DescribeBase

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

Parameters:

  • Resource (any)

    to be describe, string, or nil

  • An (Proc)

    optional block containing tests for the described resource

Returns:

  • (nil|DescribeBase)

    if called without arguments, returns DescribeBase



194
195
196
197
198
199
200
201
202
203
# File 'lib/inspec/rule.rb', line 194

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) ⇒ Object



124
125
126
127
128
# File 'lib/inspec/rule.rb', line 124

def descriptions(description_hash = nil)
  return @descriptions if description_hash.nil?

  @descriptions.merge!(description_hash)
end

#expect(value, &block) ⇒ Object



205
206
207
208
209
# File 'lib/inspec/rule.rb', line 205

def expect(value, &block)
  target = Inspec::Expect.new(value, &with_dsl(block))
  __add_check("expect", [value], target)
  target
end

#id(*_) ⇒ Object



91
92
93
94
# File 'lib/inspec/rule.rb', line 91

def id(*_)
  # never overwrite the ID
  @id
end

#impact(v = nil) ⇒ Object



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/inspec/rule.rb', line 96

def impact(v = nil)
  # N/A impact freeze is required when only_applicable_if block has reset impact value to zero"
  unless na_impact_freeze
    if v.is_a?(String)
      @impact = Inspec::Impact.impact_from_string(v)
    elsif !v.nil?
      @impact = v
    end
  end

  @impact
end

#input(input_name, options = {}) ⇒ Object

allow attributes to be accessed within control blocks



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/inspec/rule.rb', line 212

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) ⇒ Object

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



234
235
236
# File 'lib/inspec/rule.rb', line 234

def input_object(input_name)
  Inspec::InputRegistry.find_or_register_input(input_name, __profile_id)
end

#only_applicable_if(message = nil) ⇒ Object



170
171
172
173
174
175
176
177
178
179
180
# File 'lib/inspec/rule.rb', line 170

def only_applicable_if(message = nil)
  return unless block_given?
  return if yield

  impact(0.0)
  self.na_impact_freeze = true # this flag prevents impact value to reset to any other value

  @__na_rule[:result] ||= !yield
  @__na_rule[:type] = :only_applicable_if
  @__na_rule[:message] = message
end

#only_if(message = nil, impact: nil) ⇒ nil

Skip all checks if only_if is false

Parameters:

  • &block (Type)

    returns true if tests are added, false otherwise

Returns:

  • (nil)


160
161
162
163
164
165
166
167
168
# File 'lib/inspec/rule.rb', line 160

def only_if(message = nil, impact: nil)
  return unless block_given?
  return if @__skip_only_if_eval == true

  self.impact(impact) if impact && !yield
  @__skip_rule[:result] ||= !yield
  @__skip_rule[:type] = :only_if
  @__skip_rule[:message] = message
end

#ref(ref = nil, opts = {}) ⇒ Object



130
131
132
133
134
135
136
137
138
139
# File 'lib/inspec/rule.rb', line 130

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_fileObject



152
153
154
# File 'lib/inspec/rule.rb', line 152

def source_file
  @__file
end

#tag(*args) ⇒ Object



141
142
143
144
145
146
147
148
149
150
# File 'lib/inspec/rule.rb', line 141

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) ⇒ Object



109
110
111
112
# File 'lib/inspec/rule.rb', line 109

def title(v = nil)
  @title = v unless v.nil?
  @title
end

#to_sObject



87
88
89
# File 'lib/inspec/rule.rb', line 87

def to_s
  Inspec::Rule.rule_id(self)
end