inspec-core 2.2.78 → 2.2.101

Sign up to get free protection for your applications and to get access to all the features.
@@ -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