inspec-core 2.2.78 → 2.2.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,10 @@ require 'inspec/runner'
15
15
  require 'inspec/shell'
16
16
  require 'inspec/formatters'
17
17
  require 'inspec/reporters'
18
+ require 'inspec/attribute_registry'
19
+ require 'inspec/rspec_extensions'
20
+ require 'inspec/globals'
21
+ require 'inspec/impact'
18
22
 
19
23
  require 'inspec/plugin/v2'
20
24
  require 'inspec/plugin/v1'
@@ -0,0 +1,83 @@
1
+ require 'forwardable'
2
+ require 'singleton'
3
+ require 'inspec/objects/attribute'
4
+
5
+ module Inspec
6
+ class AttributeRegistry
7
+ include Singleton
8
+ extend Forwardable
9
+
10
+ attr_reader :list
11
+ def_delegator :list, :each
12
+ def_delegator :list, :[]
13
+ def_delegator :list, :key?, :profile_exist?
14
+ def_delegator :list, :select
15
+
16
+ # These self methods are convenience methods so you dont always
17
+ # have to specify instance when calling the registry
18
+ def self.find_attribute(name, profile)
19
+ instance.find_attribute(name, profile)
20
+ end
21
+
22
+ def self.register_attribute(name, profile, options = {})
23
+ instance.register_attribute(name, profile, options)
24
+ end
25
+
26
+ def self.register_profile_alias(name, alias_name)
27
+ instance.register_profile_alias(name, alias_name)
28
+ end
29
+
30
+ def self.list_attributes_for_profile(profile)
31
+ instance.list_attributes_for_profile(profile)
32
+ end
33
+
34
+ def initialize
35
+ # this is a collection of profiles which have a value of attribute objects
36
+ @list = {}
37
+
38
+ # this is a list of optional profile name overrides set in the inspec.yml
39
+ @profile_aliases = {}
40
+ end
41
+
42
+ def find_attribute(name, profile)
43
+ profile = @profile_aliases[profile] if !profile_exist?(profile) && @profile_aliases[profile]
44
+ unless profile_exist?(profile)
45
+ error = Inspec::AttributeRegistry::ProfileError.new
46
+ error.profile_name = profile
47
+ raise error, "Profile '#{error.profile_name}' does not have any attributes"
48
+ end
49
+
50
+ unless list[profile].key?(name)
51
+ error = Inspec::AttributeRegistry::AttributeError.new
52
+ error.attribute_name = name
53
+ error.profile_name = profile
54
+ raise error, "Profile '#{error.profile_name}' does not have a attribute with name '#{error.attribute_name}'"
55
+ end
56
+ list[profile][name]
57
+ end
58
+
59
+ def register_attribute(name, profile, options = {})
60
+ # check for a profile override name
61
+ if profile_exist?(profile) && list[profile][name] && options.empty?
62
+ list[profile][name]
63
+ else
64
+ list[profile] = {} unless profile_exist?(profile)
65
+ list[profile][name] = Inspec::Attribute.new(name, options)
66
+ end
67
+ end
68
+
69
+ def register_profile_alias(name, alias_name)
70
+ @profile_aliases[name] = alias_name
71
+ end
72
+
73
+ def list_attributes_for_profile(profile)
74
+ list[profile] = {} unless profile_exist?(profile)
75
+ list[profile]
76
+ end
77
+
78
+ def __reset
79
+ @list = {}
80
+ @profile_aliases = {}
81
+ end
82
+ end
83
+ end
@@ -8,6 +8,10 @@ require 'inspec/profile_vendor'
8
8
 
9
9
  module Inspec
10
10
  class BaseCLI < Thor
11
+ class << self
12
+ attr_accessor :inspec_cli_command
13
+ end
14
+
11
15
  # https://github.com/erikhuda/thor/issues/244
12
16
  def self.exit_on_failure?
13
17
  true
@@ -62,6 +66,8 @@ module Inspec
62
66
  desc: 'Specifies the bastion port if applicable'
63
67
  option :insecure, type: :boolean, default: false,
64
68
  desc: 'Disable SSL verification on select targets'
69
+ option :target_id, type: :string,
70
+ desc: 'Provide a ID which will be included on reports'
65
71
  end
66
72
 
67
73
  def self.profile_options
@@ -134,7 +140,7 @@ module Inspec
134
140
  if opts['reporter'].is_a?(Array)
135
141
  reports = {}
136
142
  opts['reporter'].each do |report|
137
- reporter_name, target = report.split(':')
143
+ reporter_name, target = report.split(':', 2)
138
144
  if target.nil? || target.strip == '-'
139
145
  reports[reporter_name] = { 'stdout' => true }
140
146
  else
@@ -142,6 +148,7 @@ module Inspec
142
148
  'file' => target,
143
149
  'stdout' => false,
144
150
  }
151
+ reports[reporter_name]['target_id'] = opts['target_id'] if opts['target_id']
145
152
  end
146
153
  end
147
154
  opts['reporter'] = reports
@@ -152,6 +159,7 @@ module Inspec
152
159
  opts['reporter'].each do |reporter_name, config|
153
160
  opts['reporter'][reporter_name] = {} if config.nil?
154
161
  opts['reporter'][reporter_name]['stdout'] = true if opts['reporter'][reporter_name].empty?
162
+ opts['reporter'][reporter_name]['target_id'] = opts['target_id'] if opts['target_id']
155
163
  end
156
164
  end
157
165
 
@@ -295,6 +303,7 @@ module Inspec
295
303
  # start with default options if we have any
296
304
  opts = BaseCLI.default_options[type] unless type.nil? || BaseCLI.default_options[type].nil?
297
305
  opts['type'] = type unless type.nil?
306
+ Inspec::BaseCLI.inspec_cli_command = type
298
307
 
299
308
  # merge in any options from json-config
300
309
  json_config = options_json
@@ -35,6 +35,9 @@ class Inspec::InspecCLI < Inspec::BaseCLI
35
35
  def json(target)
36
36
  o = opts.dup
37
37
  diagnose(o)
38
+ o['log_location'] = STDERR
39
+ configure_logger(o)
40
+
38
41
  o[:backend] = Inspec::Backend.create(target: 'mock://')
39
42
  o[:check_mode] = true
40
43
  o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
@@ -119,6 +122,10 @@ class Inspec::InspecCLI < Inspec::BaseCLI
119
122
  desc: 'Overwrite existing vendored dependencies and lockfile.'
120
123
  def vendor(path = nil)
121
124
  o = opts.dup
125
+ configure_logger(o)
126
+ o[:logger] = Logger.new(STDOUT)
127
+ o[:logger].level = get_log_level(o.log_level)
128
+
122
129
  vendor_deps(path, o)
123
130
  end
124
131
 
@@ -141,7 +148,11 @@ class Inspec::InspecCLI < Inspec::BaseCLI
141
148
  o[:logger] = Logger.new(STDOUT)
142
149
  o[:logger].level = get_log_level(o.log_level)
143
150
  o[:backend] = Inspec::Backend.create(target: 'mock://')
144
- o[:vendor_cache] = Inspec::Cache.new(o[:vendor_cache])
151
+
152
+ # Force vendoring with overwrite when archiving
153
+ vendor_options = o.dup
154
+ vendor_options[:overwrite] = true
155
+ vendor_deps(path, vendor_options)
145
156
 
146
157
  profile = Inspec::Profile.for_target(path, o)
147
158
  result = profile.check
@@ -19,11 +19,16 @@ module Inspec
19
19
  #
20
20
  # @param [ResourcesDSL] resources_dsl which has all resources to attach
21
21
  # @return [RuleContext] the inner context of rules
22
- def self.rule_context(resources_dsl)
22
+ def self.rule_context(resources_dsl, profile_id)
23
23
  require 'rspec/core/dsl'
24
24
  Class.new(Inspec::Rule) do
25
25
  include RSpec::Core::DSL
26
26
  with_resource_dsl resources_dsl
27
+
28
+ # allow attributes to be accessed within control blocks
29
+ define_method :attribute do |name|
30
+ Inspec::AttributeRegistry.find_attribute(name, profile_id).value
31
+ end
27
32
  end
28
33
  end
29
34
 
@@ -36,9 +41,9 @@ module Inspec
36
41
  # @param outer_dsl [OuterDSLClass]
37
42
  # @return [ProfileContextClass]
38
43
  def self.create(profile_context, resources_dsl) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
39
- rule_class = rule_context(resources_dsl)
40
44
  profile_context_owner = profile_context
41
45
  profile_id = profile_context.profile_id
46
+ rule_class = rule_context(resources_dsl, profile_id)
42
47
 
43
48
  Class.new do # rubocop:disable Metrics/BlockLength
44
49
  include Inspec::DSL
@@ -137,8 +142,12 @@ module Inspec
137
142
  end
138
143
 
139
144
  # method for attributes; import attribute handling
140
- define_method :attribute do |name, options|
141
- profile_context_owner.register_attribute(name, options)
145
+ define_method :attribute do |name, options = {}|
146
+ if options.empty?
147
+ Inspec::AttributeRegistry.find_attribute(name, profile_id).value
148
+ else
149
+ profile_context_owner.register_attribute(name, options)
150
+ end
142
151
  end
143
152
 
144
153
  define_method :skip_control do |id|
@@ -18,7 +18,7 @@ module Inspec
18
18
  class Cache
19
19
  attr_reader :path
20
20
  def initialize(path = nil)
21
- @path = path || File.join(Dir.home, '.inspec', 'cache')
21
+ @path = path || File.join(Inspec.config_dir, 'cache')
22
22
  FileUtils.mkdir_p(@path) unless File.directory?(@path)
23
23
  end
24
24
 
@@ -47,7 +47,7 @@ module Inspec
47
47
  end
48
48
 
49
49
  attr_reader :vendor_path
50
- attr_writer :dep_list
50
+ attr_accessor :dep_list
51
51
  # initialize
52
52
  #
53
53
  # @param cwd [String] current working directory for relative path includes
@@ -121,8 +121,9 @@ module Inspec
121
121
  if !@dependencies.nil? && !@dependencies.empty?
122
122
  opts[:dependencies] = Inspec::DependencySet.from_array(@dependencies, @cwd, @cache, @backend)
123
123
  end
124
+ opts[:profile_name] = @name
125
+ opts[:parent_profile] = @parent_profile
124
126
  @profile = Inspec::Profile.for_fetcher(fetcher, opts)
125
- @profile.parent_profile = @parent_profile
126
127
  @profile
127
128
  end
128
129
  end
@@ -11,4 +11,31 @@ module Inspec
11
11
  class DuplicateDep < Error; end
12
12
  class FetcherFailure < Error; end
13
13
  class ReporterError < Error; end
14
+ class ImpactError < Error; end
15
+
16
+ class Attribute
17
+ class Error < Inspec::Error; end
18
+ class ValidationError < Error
19
+ attr_accessor :attribute_name
20
+ attr_accessor :attribute_value
21
+ attr_accessor :attribute_type
22
+ end
23
+ class TypeError < Error
24
+ attr_accessor :attribute_type
25
+ end
26
+ class RequiredError < Error
27
+ attr_accessor :attribute_name
28
+ end
29
+ end
30
+
31
+ class AttributeRegistry
32
+ class Error < Inspec::Error; end
33
+ class ProfileError < Error
34
+ attr_accessor :profile_name
35
+ end
36
+ class AttributeError < Error
37
+ attr_accessor :profile_name
38
+ attr_accessor :attribute_name
39
+ end
40
+ end
14
41
  end
@@ -105,6 +105,22 @@ module Inspec
105
105
  end
106
106
  end
107
107
 
108
+ def extract(destination_path = '.')
109
+ FileUtils.mkdir_p(destination_path)
110
+
111
+ Zip::File.open(@path) do |archive|
112
+ archive.each do |file|
113
+ final_path = File.join(destination_path, file.name)
114
+
115
+ # This removes the top level directory (and any other files) to ensure
116
+ # extracted files do not conflict.
117
+ FileUtils.remove_entry(final_path) if File.exist?(final_path)
118
+
119
+ archive.extract(file, final_path)
120
+ end
121
+ end
122
+ end
123
+
108
124
  def read(file)
109
125
  @contents[file] ||= read_from_zip(file)
110
126
  end
@@ -150,6 +166,24 @@ module Inspec
150
166
  end
151
167
  end
152
168
 
169
+ def extract(destination_path = '.')
170
+ FileUtils.mkdir_p(destination_path)
171
+
172
+ walk_tar(@path) do |files|
173
+ files.each do |file|
174
+ next unless @files.include?(file.full_name)
175
+ final_path = File.join(destination_path, file.full_name)
176
+
177
+ # This removes the top level directory (and any other files) to ensure
178
+ # extracted files do not conflict.
179
+ FileUtils.remove_entry(final_path) if File.exist?(final_path)
180
+
181
+ FileUtils.mkdir_p(File.dirname(final_path))
182
+ File.open(final_path, 'wb') { |f| f.write(file.read) }
183
+ end
184
+ end
185
+ end
186
+
153
187
  def read(file)
154
188
  @contents[file] ||= read_from_tar(file)
155
189
  end
@@ -157,7 +191,10 @@ module Inspec
157
191
  private
158
192
 
159
193
  def walk_tar(path, &callback)
160
- Gem::Package::TarReader.new(Zlib::GzipReader.open(path), &callback)
194
+ tar_file = Zlib::GzipReader.open(path)
195
+ Gem::Package::TarReader.new(tar_file, &callback)
196
+ ensure
197
+ tar_file.close
161
198
  end
162
199
 
163
200
  def read_from_tar(file)
@@ -0,0 +1,5 @@
1
+ module Inspec
2
+ def self.config_dir
3
+ ENV['INSPEC_CONFIG_DIR'] ? ENV['INSPEC_CONFIG_DIR'] : File.join(Dir.home, '.inspec')
4
+ end
5
+ end
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ # Impact scores based off CVSS 3.0
4
+ module Inspec::Impact
5
+ IMPACT_SCORES = {
6
+ 'none' => 0.0,
7
+ 'low' => 0.01,
8
+ 'medium' => 0.4,
9
+ 'high' => 0.7,
10
+ 'critical' => 0.9,
11
+ }.freeze
12
+
13
+ def self.impact_from_string(value)
14
+ # return if its a number
15
+ return value if is_number?(value)
16
+ raise Inspec::ImpactError, "'#{value}' is not a valid impact name. Valid impact names: none, low, medium, high, critical." unless IMPACT_SCORES.key?(value.downcase)
17
+ IMPACT_SCORES[value]
18
+ end
19
+
20
+ def self.is_number?(value)
21
+ Float(value)
22
+ true
23
+ rescue
24
+ false
25
+ end
26
+
27
+ def self.string_from_impact(value)
28
+ value = value.to_f
29
+ raise Inspec::ImpactError, "'#{value}' is not a valid impact score. Valid impact scores: [0.0 - 1.0]." if value < 0 || value > 1
30
+ IMPACT_SCORES.reverse_each do |name, impact|
31
+ return name if value >= impact
32
+ end
33
+ end
34
+ end
@@ -3,7 +3,16 @@
3
3
  module Inspec
4
4
  class Attribute
5
5
  attr_accessor :name
6
- attr_writer :value
6
+
7
+ VALID_TYPES = %w{
8
+ String
9
+ Numeric
10
+ Regexp
11
+ Array
12
+ Hash
13
+ Boolean
14
+ Any
15
+ }.freeze
7
16
 
8
17
  DEFAULT_ATTRIBUTE = Class.new do
9
18
  def initialize(name)
@@ -16,7 +25,6 @@ module Inspec
16
25
  "Use --attrs to provide a value for '#{@name}' or specify a default "\
17
26
  "value with `attribute('#{@name}', default: 'somedefault', ...)`.",
18
27
  )
19
-
20
28
  self
21
29
  end
22
30
 
@@ -28,16 +36,22 @@ module Inspec
28
36
  def initialize(name, options = {})
29
37
  @name = name
30
38
  @opts = options
39
+ validate_value_type(default) if @opts.key?(:type) && @opts.key?(:default)
31
40
  @value = nil
32
41
  end
33
42
 
34
- # implicit call is done by inspec to determine the value of an attribute
35
- def value
36
- @value.nil? ? default : @value
43
+ def value=(new_value)
44
+ validate_value_type(new_value) if @opts.key?(:type)
45
+ @value = new_value
37
46
  end
38
47
 
39
- def default
40
- @opts.key?(:default) ? @opts[:default] : DEFAULT_ATTRIBUTE.new(@name)
48
+ def value
49
+ if @value.nil?
50
+ validate_required(@value) if @opts[:required] == true
51
+ @value = default
52
+ else
53
+ @value
54
+ end
41
55
  end
42
56
 
43
57
  def title
@@ -71,5 +85,76 @@ module Inspec
71
85
  def to_s
72
86
  "Attribute #{@name} with #{@value}"
73
87
  end
88
+
89
+ private
90
+
91
+ def validate_required(value)
92
+ # value will be set already if a secrets file was passed in
93
+ if (!@opts.key?(:default) && value.nil?) || (@opts[:default].nil? && value.nil?)
94
+ error = Inspec::Attribute::RequiredError.new
95
+ error.attribute_name = @name
96
+ raise error, "Attribute '#{error.attribute_name}' is required and does not have a value."
97
+ end
98
+ end
99
+
100
+ def validate_type(type)
101
+ type = type.capitalize
102
+ abbreviations = {
103
+ 'Num' => 'Numeric',
104
+ 'Regex' => 'Regexp',
105
+ }
106
+ type = abbreviations[type] if abbreviations.key?(type)
107
+ if !VALID_TYPES.include?(type)
108
+ error = Inspec::Attribute::TypeError.new
109
+ error.attribute_type = type
110
+ raise error, "Type '#{error.attribute_type}' is not a valid attribute type."
111
+ end
112
+ type
113
+ end
114
+
115
+ def valid_numeric?(value)
116
+ Float(value)
117
+ true
118
+ rescue
119
+ false
120
+ end
121
+
122
+ def valid_regexp?(value)
123
+ # check for invalid regex syntex
124
+ Regexp.new(value)
125
+ true
126
+ rescue
127
+ false
128
+ end
129
+
130
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
131
+ def validate_value_type(value)
132
+ type = validate_type(@opts[:type])
133
+ return if type == 'Any'
134
+
135
+ invalid_type = false
136
+ if type == 'Regexp'
137
+ invalid_type = true if !value.is_a?(String) || !valid_regexp?(value)
138
+ elsif type == 'Numeric'
139
+ invalid_type = true if !valid_numeric?(value)
140
+ elsif type == 'Boolean'
141
+ invalid_type = true if ![true, false].include?(value)
142
+ elsif value.is_a?(Module.const_get(type)) == false
143
+ invalid_type = true
144
+ end
145
+
146
+ if invalid_type == true
147
+ error = Inspec::Attribute::ValidationError.new
148
+ error.attribute_name = @name
149
+ error.attribute_value = value
150
+ error.attribute_type = type
151
+ raise error, "Attribute '#{error.attribute_name}' with value '#{error.attribute_value}' does not validate to type '#{error.attribute_type}'."
152
+ end
153
+ end
154
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
155
+
156
+ def default
157
+ @opts.key?(:default) ? @opts[:default] : DEFAULT_ATTRIBUTE.new(@name)
158
+ end
74
159
  end
75
160
  end