inspec 0.33.2 → 0.34.0

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.
@@ -0,0 +1,219 @@
1
+ # encoding: utf-8
2
+ require 'rubygems/package'
3
+ require 'zlib'
4
+ require 'zip'
5
+
6
+ module Inspec
7
+ class FileProvider
8
+ def self.for_path(path)
9
+ if path.is_a?(Hash)
10
+ MockProvider.new(path)
11
+ elsif File.directory?(path)
12
+ DirProvider.new(path)
13
+ elsif File.exist?(path) && path.end_with?('.tar.gz', 'tgz')
14
+ TarProvider.new(path)
15
+ elsif File.exist?(path) && path.end_with?('.zip')
16
+ ZipProvider.new(path)
17
+ elsif File.exist?(path)
18
+ DirProvider.new(path)
19
+ else
20
+ fail "No file provider for the provided path: #{path}"
21
+ end
22
+ end
23
+
24
+ def initialize(_path)
25
+ end
26
+
27
+ def read(_file)
28
+ fail "#{self} does not implement `read(...)`. This is required."
29
+ end
30
+
31
+ def files
32
+ fail "Fetcher #{self} does not implement `files()`. This is required."
33
+ end
34
+
35
+ def relative_provider
36
+ RelativeFileProvider.new(self)
37
+ end
38
+ end
39
+
40
+ class MockProvider < FileProvider
41
+ attr_reader :files
42
+ def initialize(path)
43
+ @data = path[:mock]
44
+ @files = @data.keys
45
+ end
46
+
47
+ def read(file)
48
+ @data[file]
49
+ end
50
+ end
51
+
52
+ class DirProvider < FileProvider
53
+ attr_reader :files
54
+ def initialize(path)
55
+ @files = if File.file?(path)
56
+ [path]
57
+ else
58
+ Dir[File.join(path, '**', '*')]
59
+ end
60
+ @path = path
61
+ end
62
+
63
+ def read(file)
64
+ return nil unless files.include?(file)
65
+ return nil unless File.file?(file)
66
+ File.read(file)
67
+ end
68
+ end
69
+
70
+ class ZipProvider < FileProvider
71
+ attr_reader :files
72
+
73
+ def initialize(path)
74
+ @path = path
75
+ @contents = {}
76
+ @files = []
77
+ ::Zip::InputStream.open(@path) do |io|
78
+ while (entry = io.get_next_entry)
79
+ @files.push(entry.name.sub(%r{/+$}, ''))
80
+ end
81
+ end
82
+ end
83
+
84
+ def read(file)
85
+ @contents[file] ||= read_from_zip(file)
86
+ end
87
+
88
+ private
89
+
90
+ def read_from_zip(file)
91
+ return nil unless @files.include?(file)
92
+ res = nil
93
+ ::Zip::InputStream.open(@path) do |io|
94
+ while (entry = io.get_next_entry)
95
+ next unless file == entry.name
96
+ res = io.read
97
+ break
98
+ end
99
+ end
100
+ res
101
+ end
102
+ end
103
+
104
+ class TarProvider < FileProvider
105
+ attr_reader :files
106
+
107
+ def initialize(path)
108
+ @path = path
109
+ @contents = {}
110
+ @files = []
111
+ Gem::Package::TarReader.new(Zlib::GzipReader.open(@path)) do |tar|
112
+ @files = tar.map(&:full_name)
113
+ end
114
+ end
115
+
116
+ def read(file)
117
+ @contents[file] ||= read_from_tar(file)
118
+ end
119
+
120
+ private
121
+
122
+ def read_from_tar(file)
123
+ return nil unless @files.include?(file)
124
+ res = nil
125
+ # NB `TarReader` includes `Enumerable` beginning with Ruby 2.x
126
+ Gem::Package::TarReader.new(Zlib::GzipReader.open(@path)) do |tar|
127
+ tar.each do |entry|
128
+ next unless entry.file? && file == entry.full_name
129
+ res = entry.read
130
+ break
131
+ end
132
+ end
133
+ res
134
+ end
135
+ end
136
+
137
+ class RelativeFileProvider
138
+ BLACKLIST_FILES = [
139
+ '/pax_global_header',
140
+ 'pax_global_header',
141
+ ].freeze
142
+
143
+ attr_reader :files
144
+ attr_reader :prefix
145
+ attr_reader :parent
146
+
147
+ def initialize(parent_provider)
148
+ @parent = parent_provider
149
+ @prefix = get_prefix(parent.files)
150
+ if @prefix.nil?
151
+ fail "Could not determine path prefix for #{parent}"
152
+ end
153
+ @files = parent.files.find_all { |x| x.start_with?(prefix) && x != prefix }
154
+ .map { |x| x[prefix.length..-1] }
155
+ end
156
+
157
+ def abs_path(file)
158
+ return nil if file.nil?
159
+ prefix + file
160
+ end
161
+
162
+ def read(file)
163
+ parent.read(abs_path(file))
164
+ end
165
+
166
+ private
167
+
168
+ def get_prefix(fs)
169
+ return '' if fs.empty?
170
+
171
+ # filter backlisted files
172
+ fs -= BLACKLIST_FILES
173
+
174
+ sorted = fs.sort_by(&:length)
175
+ get_folder_prefix(sorted)
176
+ end
177
+
178
+ def prefix_candidate_for(file)
179
+ if file.end_with?(File::SEPARATOR)
180
+ file
181
+ else
182
+ file + File::SEPARATOR
183
+ end
184
+ end
185
+
186
+ def get_folder_prefix(fs)
187
+ return get_files_prefix(fs) if fs.length == 1
188
+ first, *rest = fs
189
+ pre = prefix_candidate_for(first)
190
+
191
+ if rest.all? { |i| i.start_with? pre }
192
+ return get_folder_prefix(rest)
193
+ end
194
+ get_files_prefix(fs)
195
+ end
196
+
197
+ def get_files_prefix(fs)
198
+ return '' if fs.empty?
199
+
200
+ file = fs[0]
201
+ bn = File.basename(file)
202
+ # no more prefixes
203
+ return '' if bn == file
204
+
205
+ i = file.rindex(bn)
206
+ pre = file[0..i-1]
207
+
208
+ rest = fs.find_all { |f| !f.start_with?(pre) }
209
+ return pre if rest.empty?
210
+
211
+ new_pre = get_prefix(rest)
212
+ return new_pre if pre.start_with? new_pre
213
+ # edge case: completely different prefixes; retry prefix detection
214
+ a = File.dirname(pre + 'a')
215
+ b = File.dirname(new_pre + 'b')
216
+ get_prefix([a, b])
217
+ end
218
+ end
219
+ end
@@ -1,106 +1,85 @@
1
1
  # encoding: utf-8
2
2
  # author: Dominik Richter
3
3
  # author: Christoph Hartmann
4
-
5
4
  require 'utils/plugin_registry'
5
+ require 'inspec/file_provider'
6
+ require 'digest'
6
7
 
7
8
  module Inspec
8
9
  module Plugins
10
+ #
11
+ # An Inspec::Plugins::Fetcher is responsible for fetching a remote
12
+ # source to a local directory or file provided by the user.
13
+ #
14
+ # In general, there are two kinds of fetchers. (1) Fetchers that
15
+ # implement this entire API (see the Git or Url fetchers for
16
+ # examples), and (2) fetchers that only implement self.resolve and
17
+ # then call the resolve_next method with a modified target hash.
18
+ # Fetchers in (2) do not need to implement the functions in this
19
+ # class because the caller will never actually get an instance of
20
+ # those fetchers.
21
+ #
9
22
  class Fetcher < PluginRegistry::Plugin
10
23
  def self.plugin_registry
11
24
  Inspec::Fetcher
12
25
  end
13
26
 
14
- # Provide a list of files that are available to this fetcher.
15
27
  #
16
- # @return [Array[String]] A list of filenames
17
- def files
18
- fail "Fetcher #{self} does not implement `files()`. This is required."
19
- end
20
-
21
- # Read a file using this fetcher. The name must correspond to a file
22
- # available to this fetcher. Use #files to retrieve the list of
23
- # files.
28
+ # The path to the archive on disk. This can be passed to a
29
+ # FileProvider to get access to the files in the fetched
30
+ # profile.
24
31
  #
25
- # @param [String] _file The filename you are interested in
26
- # @return [String] The file's contents
27
- def read(_file)
28
- fail "Fetcher #{self} does not implement `read(...)`. This is required."
29
- end
30
-
31
- def relative_target
32
- RelFetcher.new(self)
33
- end
34
- end
35
-
36
- BLACKLIST_FILES = [
37
- '/pax_global_header',
38
- 'pax_global_header',
39
- ].freeze
40
-
41
- class RelFetcher < Fetcher
42
- attr_reader :files
43
- attr_reader :prefix
44
-
45
- def initialize(fetcher)
46
- @parent = fetcher
47
- @prefix = get_prefix(fetcher.files)
48
- @files = fetcher.files.find_all { |x| x.start_with? prefix }
49
- .map { |x| x[prefix.length..-1] }
32
+ def archive_path
33
+ fail "Fetcher #{self} does not implement `archive_path()`. This is required."
50
34
  end
51
35
 
52
- def abs_path(file)
53
- return nil if file.nil?
54
- prefix + file
36
+ #
37
+ # Fetches the remote source to a local source, using the
38
+ # provided path as a partial filename. That is, if you pass
39
+ # /foo/bar/baz, the fetcher can create:
40
+ #
41
+ # /foo/bar/baz/: A profile directory, or
42
+ # /foo/bar/baz.tar.gz: A profile tarball, or
43
+ # /foo/bar/baz.zip
44
+ #
45
+ def fetch(_path)
46
+ fail "Fetcher #{self} does not implement `fetch()`. This is required."
55
47
  end
56
48
 
57
- def read(file)
58
- @parent.read(abs_path(file))
49
+ #
50
+ # The full specification of the remote source, with any
51
+ # ambigious references provided by the user resolved to an exact
52
+ # reference where possible. For example, in the Git provide, a
53
+ # tag will be resolved to an exact revision.
54
+ #
55
+ def resolved_source
56
+ fail "Fetcher #{self} does not implement `resolved_source()`. This is required for terminal fetchers."
59
57
  end
60
58
 
61
- private
62
-
63
- def get_prefix(fs)
64
- return '' if fs.empty?
65
-
66
- # filter backlisted files
67
- fs -= BLACKLIST_FILES
68
-
69
- sorted = fs.sort_by(&:length)
70
- get_folder_prefix(sorted)
59
+ #
60
+ # relative_target is provided to keep compatibility with 3rd
61
+ # party plugins.
62
+ #
63
+ # Deprecated: This function may be removed in future versions of
64
+ # Inspec, don't depend on it in new plugins.
65
+ #
66
+ # @returns [Inspec::RelativeFileProvider]
67
+ #
68
+ def relative_target
69
+ file_provider = Inspec::FileProvider.for_path(archive_path)
70
+ file_provider.relative_provider
71
71
  end
72
72
 
73
- def get_folder_prefix(fs, first_iteration = true)
74
- return get_files_prefix(fs) if fs.length == 1
75
- pre = fs[0] + File::SEPARATOR
76
- rest = fs[1..-1]
77
- if rest.all? { |i| i.start_with? pre }
78
- return get_folder_prefix(rest, false)
73
+ #
74
+ # A string based on the components of the resolved source,
75
+ # suitable for constructing per-source file names.
76
+ #
77
+ def cache_key
78
+ key = ''
79
+ resolved_source.each do |k, v|
80
+ key << "#{k}:#{v}"
79
81
  end
80
- return get_files_prefix(fs) if first_iteration
81
- fs
82
- end
83
-
84
- def get_files_prefix(fs)
85
- return '' if fs.empty?
86
-
87
- file = fs[0]
88
- bn = File.basename(file)
89
- # no more prefixes
90
- return '' if bn == file
91
-
92
- i = file.rindex(bn)
93
- pre = file[0..i-1]
94
-
95
- rest = fs.find_all { |f| !f.start_with?(pre) }
96
- return pre if rest.empty?
97
-
98
- new_pre = get_prefix(rest)
99
- return new_pre if pre.start_with? new_pre
100
- # edge case: completely different prefixes; retry prefix detection
101
- a = File.dirname(pre + 'a')
102
- b = File.dirname(new_pre + 'b')
103
- get_prefix([a, b])
82
+ Digest::SHA256.hexdigest key
104
83
  end
105
84
  end
106
85
  end
@@ -6,11 +6,14 @@
6
6
  require 'forwardable'
7
7
  require 'inspec/polyfill'
8
8
  require 'inspec/fetcher'
9
+ require 'inspec/file_provider'
9
10
  require 'inspec/source_reader'
10
11
  require 'inspec/metadata'
11
12
  require 'inspec/backend'
12
13
  require 'inspec/rule'
14
+ require 'inspec/log'
13
15
  require 'inspec/profile_context'
16
+ require 'inspec/dependencies/vendor_index'
14
17
  require 'inspec/dependencies/lockfile'
15
18
  require 'inspec/dependencies/dependency_set'
16
19
 
@@ -18,24 +21,34 @@ module Inspec
18
21
  class Profile # rubocop:disable Metrics/ClassLength
19
22
  extend Forwardable
20
23
 
21
- def self.resolve_target(target)
22
- # Fetchers retrieve file contents
24
+ def self.resolve_target(target, cache = nil)
25
+ cache ||= VendorIndex.new
23
26
  fetcher = Inspec::Fetcher.resolve(target)
24
27
  if fetcher.nil?
25
28
  fail("Could not fetch inspec profile in #{target.inspect}.")
26
29
  end
27
- # Source readers understand the target's structure and provide
28
- # access to tests, libraries, and metadata
29
- reader = Inspec::SourceReader.resolve(fetcher.relative_target)
30
+
31
+ if cache.exists?(fetcher.cache_key)
32
+ Inspec::Log.debug "Using cached dependency for #{target}"
33
+ cache.prefered_entry_for(fetcher.cache_key)
34
+ else
35
+ fetcher.fetch(cache.base_path_for(fetcher.cache_key))
36
+ fetcher.archive_path
37
+ end
38
+ end
39
+
40
+ def self.for_path(path, opts)
41
+ file_provider = FileProvider.for_path(path)
42
+ reader = Inspec::SourceReader.resolve(file_provider.relative_provider)
30
43
  if reader.nil?
31
- fail("Don't understand inspec profile in #{target.inspect}, it "\
44
+ fail("Don't understand inspec profile in #{path}, it " \
32
45
  "doesn't look like a supported profile structure.")
33
46
  end
34
- reader
47
+ new(reader, opts)
35
48
  end
36
49
 
37
50
  def self.for_target(target, opts = {})
38
- new(resolve_target(target), opts.merge(target: target))
51
+ for_path(resolve_target(target, opts[:cache]), opts.merge(target: target))
39
52
  end
40
53
 
41
54
  attr_reader :source_reader
@@ -46,18 +59,18 @@ module Inspec
46
59
 
47
60
  # rubocop:disable Metrics/AbcSize
48
61
  def initialize(source_reader, options = {})
49
- @options = options
50
- @target = @options.delete(:target)
51
- @logger = @options[:logger] || Logger.new(nil)
52
- @source_reader = source_reader
53
- if options[:dependencies]
54
- @locked_dependencies = options[:dependencies]
55
- end
62
+ @target = options.delete(:target)
63
+ @logger = options[:logger] || Logger.new(nil)
64
+ @locked_dependencies = options[:dependencies]
56
65
  @controls = options[:controls] || []
57
- @profile_id = @options[:id]
58
- @backend = @options[:backend] || Inspec::Backend.create(options)
66
+ @profile_id = options[:id]
67
+ @backend = options[:backend] || Inspec::Backend.create(options)
68
+ @source_reader = source_reader
69
+ @tests_collected = false
59
70
  Metadata.finalize(@source_reader.metadata, @profile_id)
60
- @runner_context = @options[:profile_context] || Inspec::ProfileContext.for_profile(self, @backend, @options[:attributes])
71
+ @runner_context = options[:profile_context] || Inspec::ProfileContext.for_profile(self,
72
+ @backend,
73
+ options[:attributes])
61
74
  end
62
75
 
63
76
  def name
@@ -106,7 +106,8 @@ require 'resources/service'
106
106
  require 'resources/shadow'
107
107
  require 'resources/ssl'
108
108
  require 'resources/ssh_conf'
109
- require 'resources/user'
109
+ require 'resources/sys_info'
110
+ require 'resources/users'
110
111
  require 'resources/vbscript'
111
112
  require 'resources/windows_feature'
112
113
  require 'resources/xinetd'
@@ -11,9 +11,6 @@ module Inspec
11
11
  class SourceReaderRegistry < PluginRegistry
12
12
  def resolve(target)
13
13
  return nil if target.nil?
14
- unless target.is_a? Inspec::Plugins::Fetcher
15
- fail "SourceReader cannot resolve targets that aren't Fetchers: #{target.class}"
16
- end
17
14
  super(target)
18
15
  end
19
16
  end
@@ -4,5 +4,5 @@
4
4
  # author: Christoph Hartmann
5
5
 
6
6
  module Inspec
7
- VERSION = '0.33.2'.freeze
7
+ VERSION = '0.34.0'.freeze
8
8
  end
@@ -13,72 +13,101 @@
13
13
  # All local GPO parameters can be examined via Registry, but not all security
14
14
  # parameters. Therefore we need a combination of Registry and secedit output
15
15
 
16
+ require 'hashie'
17
+
16
18
  module Inspec::Resources
17
19
  class SecurityPolicy < Inspec.resource(1)
18
20
  name 'security_policy'
19
21
  desc 'Use the security_policy InSpec audit resource to test security policies on the Microsoft Windows platform.'
20
22
  example "
21
23
  describe security_policy do
22
- its('SeNetworkLogonRight') { should eq '*S-1-5-11' }
24
+ its('SeNetworkLogonRight') { should include 'S-1-5-11' }
23
25
  end
24
26
  "
25
- def initialize
26
- @loaded = false
27
- @policy = nil
28
- @exit_status = nil
27
+
28
+ def content
29
+ read_content
30
+ end
31
+
32
+ def params(*opts)
33
+ opts.inject(read_params) do |res, nxt|
34
+ res.respond_to?(:key) ? res[nxt] : nil
35
+ end
36
+ end
37
+
38
+ def method_missing(name)
39
+ params = read_params
40
+ return nil if params.nil?
41
+
42
+ # deep search for hash key
43
+ params.extend Hashie::Extensions::DeepFind
44
+ res = params.deep_find(name.to_s)
45
+ res
46
+ end
47
+
48
+ def to_s
49
+ 'Security Policy'
29
50
  end
30
51
 
31
- # load security content
32
- def load
52
+ private
53
+
54
+ def read_content
55
+ return @content if defined?(@content)
56
+
33
57
  # export the security policy
34
58
  cmd = inspec.command('secedit /export /cfg win_secpol.cfg')
35
59
  return nil if cmd.exit_status.to_i != 0
36
60
 
37
61
  # store file content
38
62
  cmd = inspec.command('Get-Content win_secpol.cfg')
39
- @exit_status = cmd.exit_status.to_i
40
- return nil if @exit_status != 0
41
- @policy = cmd.stdout
42
- @loaded = true
43
-
44
- # returns self
45
- self
63
+ return skip_resource "Can't read security policy" if cmd.exit_status.to_i != 0
64
+ @content = cmd.stdout
46
65
 
66
+ if @content.empty? && file.size > 0
67
+ return skip_resource "Can't read security policy"
68
+ end
69
+ @content
47
70
  ensure
48
71
  # delete temp file
49
72
  inspec.command('Remove-Item win_secpol.cfg').exit_status.to_i
50
73
  end
51
74
 
52
- def method_missing(method)
53
- # load data if needed
54
- if @loaded == false
55
- load
56
- end
57
-
58
- # find line with key
59
- key = Regexp.escape(method.to_s)
60
- target = ''
61
- @policy.each_line {|s|
62
- target = s.strip if s =~ /^\s*#{key}\s*=\s*(.*)\b/
63
- }
75
+ def read_params
76
+ return @params if defined?(@params)
77
+ return @params = {} if read_content.nil?
64
78
 
65
- # extract variable value
66
- result = target.match(/[=]{1}\s*(?<value>.*)/)
79
+ conf = SimpleConfig.new(
80
+ @content,
81
+ assignment_re: /^\s*(.*)=\s*(\S*)\s*$/,
82
+ )
83
+ @params = convert_hash(conf.params)
84
+ end
67
85
 
68
- if !result.nil?
69
- val = result[:value]
70
- val = val.to_i if val =~ /^\d+$/
86
+ # extracts the values, this methods detects:
87
+ # numbers and SIDs and optimizes them for further usage
88
+ def extract_value(val)
89
+ if val =~ /^\d+$/
90
+ val.to_i
91
+ # special handling for SID array
92
+ elsif val =~ /^\*\S/
93
+ val.split(',').map { |v|
94
+ v.sub('*S', 'S')
95
+ }
96
+ # special handling for string values with "
97
+ elsif !(m = /^\"(.*)\"$/.match(val)).nil?
98
+ m[1]
71
99
  else
72
- # TODO: we may need to return skip or failure if the
73
- # requested value is not available
74
- val = nil
100
+ val
75
101
  end
76
-
77
- val
78
102
  end
79
103
 
80
- def to_s
81
- 'Security Policy'
104
+ def convert_hash(hash)
105
+ new_hash = {}
106
+ hash.each do |k, v|
107
+ v.is_a?(Hash) ? value = convert_hash(v) : value = extract_value(v)
108
+ new_hash[k.strip] = value
109
+ end
110
+ new_hash
82
111
  end
83
112
  end
84
113
  end
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ module Inspec::Resources
3
+ # this resource returns additional system informatio
4
+ class System < Inspec.resource(1)
5
+ name 'sys_info'
6
+
7
+ desc 'Use the user InSpec system resource to test for operating system properties.'
8
+ example "
9
+ describe sysinfo do
10
+ its('hostname') { should eq 'example.com' }
11
+ end
12
+ "
13
+
14
+ # returns the hostname of the local system
15
+ def hostname
16
+ os = inspec.os
17
+ if os.linux?
18
+ inspec.command('hostname').stdout.chomp
19
+ elsif os.windows?
20
+ inspec.powershell('$env:computername').stdout.chomp
21
+ else
22
+ skip_resource 'The `sys_info.hostname` resource is not supported on your OS yet.'
23
+ end
24
+ end
25
+ end
26
+ end