inspec 0.33.2 → 0.34.0

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