inspec 1.17.0 → 1.18.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -101,3 +101,11 @@ The following examples show how to use this InSpec audit resource.
101
101
  it { should exist }
102
102
  it { should be_enabled }
103
103
  end
104
+
105
+ ### Test a particular repository configuration, such as its Base URL
106
+
107
+ describe yum.repo('mycompany-artifacts') do
108
+ it { should exist }
109
+ it { should be_enabled }
110
+ its('baseurl') { should include 'mycompany.biz' }
111
+ end
data/inspec.gemspec CHANGED
@@ -24,6 +24,8 @@ Gem::Specification.new do |spec|
24
24
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
25
25
  spec.require_paths = ['lib']
26
26
 
27
+ spec.required_ruby_version = '>= 2.1'
28
+
27
29
  spec.add_dependency 'train', '>=0.22.0', '<1.0'
28
30
  spec.add_dependency 'thor', '~> 0.19'
29
31
  spec.add_dependency 'json', '>= 1.8', '< 3.0'
@@ -40,4 +42,5 @@ Gem::Specification.new do |spec|
40
42
  spec.add_dependency 'nokogiri', '~> 1.6'
41
43
  spec.add_dependency 'faraday', '>=0.9.0'
42
44
  spec.add_dependency 'toml', '~> 0.1'
45
+ spec.add_dependency 'addressable', '~> 2.5'
43
46
  end
@@ -11,7 +11,6 @@ module Habitat
11
11
  option :output_dir, type: :string, required: false,
12
12
  desc: 'Directory in which to save the generated Habitat artifact. Default: current directory'
13
13
  def create(path)
14
- puts options
15
14
  Habitat::Profile.create(path, options)
16
15
  end
17
16
 
@@ -1,6 +1,7 @@
1
1
  # encoding: utf-8
2
2
  # author: Adam Leff
3
3
 
4
+ require 'inspec/profile_vendor'
4
5
  require 'mixlib/shellout'
5
6
  require 'toml'
6
7
 
@@ -37,6 +38,8 @@ module Habitat
37
38
  validate_habitat_installed
38
39
  validate_habitat_origin
39
40
  create_profile_object
41
+ verify_profile
42
+ vendor_profile_dependencies
40
43
  copy_profile_to_work_dir
41
44
  create_plan
42
45
  create_run_hook
@@ -79,7 +82,10 @@ module Habitat
79
82
  private
80
83
 
81
84
  def create_profile_object
82
- @profile = Inspec::Profile.for_target(path, backend: Inspec::Backend.create(target: 'mock://'))
85
+ @profile = Inspec::Profile.for_target(
86
+ path,
87
+ backend: Inspec::Backend.create(target: 'mock://'),
88
+ )
83
89
  end
84
90
 
85
91
  def verify_profile
@@ -92,6 +98,22 @@ module Habitat
92
98
  Habitat::Log.info('Profile is valid.')
93
99
  end
94
100
 
101
+ def vendor_profile_dependencies
102
+ profile_vendor = Inspec::ProfileVendor.new(path)
103
+ if profile_vendor.lockfile.exist? && profile_vendor.cache_path.exist?
104
+ Habitat::Log.info("Profile's dependencies are already vendored, skipping vendor process.")
105
+ else
106
+ Habitat::Log.info("Vendoring the profile's dependencies...")
107
+ profile_vendor.vendor!
108
+
109
+ Habitat::Log.info('Ensuring all vendored content has read permissions...')
110
+ profile_vendor.make_readable
111
+
112
+ # refresh the profile object since the profile now has new files
113
+ create_profile_object
114
+ end
115
+ end
116
+
95
117
  def validate_habitat_installed
96
118
  Habitat::Log.info('Checking to see if Habitat is installed...')
97
119
  cmd = Mixlib::ShellOut.new('hab --version')
@@ -4,6 +4,7 @@
4
4
  # author: Dominik Richter
5
5
 
6
6
  require 'net/http'
7
+ require 'addressable/uri'
7
8
 
8
9
  module Supermarket
9
10
  class API
@@ -26,9 +27,10 @@ module Supermarket
26
27
  end
27
28
 
28
29
  def self.profile_name(profile)
29
- uri = URI(profile)
30
+ # We use Addressable::URI here because URI has a bug in Ruby 2.1.x where it doesn't allow underscore in host
31
+ uri = Addressable::URI.parse profile
30
32
  [uri.host, uri.path[1..-1]]
31
- rescue URI::Error => _e
33
+ rescue
32
34
  nil
33
35
  end
34
36
 
@@ -50,8 +52,8 @@ module Supermarket
50
52
  supermarket_tool['tool_owner'] == tool_owner && supermarket_tool['tool'] == tool
51
53
  end
52
54
 
53
- def self.find(profile, supermarket_url)
54
- profiles = Supermarket::API.profiles(supermarket_url=SUPERMARKET_URL)
55
+ def self.find(profile, supermarket_url = SUPERMARKET_URL)
56
+ profiles = Supermarket::API.profiles(supermarket_url)
55
57
  if !profiles.empty?
56
58
  index = profiles.index { |t| same?(profile, t, supermarket_url) }
57
59
  # return profile or nil
data/lib/fetchers/git.rb CHANGED
@@ -24,7 +24,7 @@ module Fetchers
24
24
  # you got to this file during debugging, you may want to look at the
25
25
  # omnibus source for hints.
26
26
  #
27
- class Git < Inspec.fetcher(1)
27
+ class Git < Inspec.fetcher(1) # rubocop:disable ClassLength
28
28
  name 'git'
29
29
  priority 200
30
30
 
@@ -44,6 +44,8 @@ module Fetchers
44
44
 
45
45
  def fetch(dir)
46
46
  @repo_directory = dir
47
+ FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
48
+
47
49
  if cloned?
48
50
  checkout
49
51
  else
data/lib/fetchers/url.rb CHANGED
@@ -153,6 +153,7 @@ module Fetchers
153
153
  def download_archive(path)
154
154
  download_archive_to_temp
155
155
  final_path = "#{path}#{@archive_type}"
156
+ FileUtils.mkdir_p(File.dirname(final_path))
156
157
  FileUtils.mv(temp_archive_path, final_path)
157
158
  Inspec::Log.debug("Fetched archive moved to: #{final_path}")
158
159
  @temp_archive_path = nil
@@ -4,6 +4,7 @@
4
4
 
5
5
  require 'thor'
6
6
  require 'inspec/log'
7
+ require 'inspec/profile_vendor'
7
8
 
8
9
  module Inspec
9
10
  class BaseCLI < Thor # rubocop:disable Metrics/ClassLength
@@ -147,30 +148,16 @@ module Inspec
147
148
  end
148
149
 
149
150
  def vendor_deps(path, opts)
150
- path.nil? ? path = Pathname.new(Dir.pwd) : path = Pathname.new(path)
151
- cache_path = path.join('vendor')
152
- inspec_lock = path.join('inspec.lock')
151
+ profile_path = path || Dir.pwd
152
+ profile_vendor = Inspec::ProfileVendor.new(profile_path)
153
153
 
154
- if (cache_path.exist? || inspec_lock.exist?) && !opts[:overwrite]
154
+ if (profile_vendor.cache_path.exist? || profile_vendor.lockfile.exist?) && !opts[:overwrite]
155
155
  puts 'Profile is already vendored. Use --overwrite.'
156
156
  return false
157
157
  end
158
158
 
159
- # remove existing
160
- FileUtils.rm_rf(cache_path) if cache_path.exist?
161
- File.delete(inspec_lock) if inspec_lock.exist?
162
-
163
- puts "Vendor dependencies of #{path} into #{cache_path}"
164
- opts[:logger] = Logger.new(STDOUT)
165
- opts[:logger].level = get_log_level(opts.log_level)
166
- opts[:cache] = Inspec::Cache.new(cache_path.to_s)
167
- opts[:backend] = Inspec::Backend.create(target: 'mock://')
168
- configure_logger(opts)
169
-
170
- # vendor dependencies and generate lockfile
171
- profile = Inspec::Profile.for_target(path.to_s, opts)
172
- lockfile = profile.generate_lockfile
173
- File.write(inspec_lock, lockfile.to_yaml)
159
+ profile_vendor.vendor!
160
+ puts "Dependencies for profile #{profile_path} successfully vendored to #{profile_vendor.cache_path}"
174
161
  rescue StandardError => e
175
162
  pretty_handle_exception(e)
176
163
  end
data/lib/inspec/cli.rb CHANGED
@@ -14,6 +14,7 @@ require 'inspec/base_cli'
14
14
  require 'inspec/plugins'
15
15
  require 'inspec/runner_mock'
16
16
  require 'inspec/env_printer'
17
+ require 'inspec/schema'
17
18
 
18
19
  class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
19
20
  class_option :log_level, aliases: :l, type: :string,
@@ -221,6 +222,14 @@ class Inspec::InspecCLI < Inspec::BaseCLI # rubocop:disable Metrics/ClassLength
221
222
  pretty_handle_exception(e)
222
223
  end
223
224
 
225
+ desc 'schema NAME', 'print the JSON schema', hide: true
226
+ def schema(name)
227
+ puts Inspec::Schema.json(name)
228
+ rescue StandardError => e
229
+ puts e
230
+ puts "Valid schemas are #{Inspec::Schema.names.join(', ')}"
231
+ end
232
+
224
233
  desc 'version', 'prints the version of this tool'
225
234
  def version
226
235
  puts Inspec::VERSION
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Inspec
4
4
  autoload :Attribute, 'inspec/objects/attribute'
5
+ autoload :Tag, 'inspec/objects/tag'
5
6
  autoload :Control, 'inspec/objects/control'
6
7
  autoload :EachLoop, 'inspec/objects/each_loop'
7
8
  autoload :List, 'inspec/objects/list'
@@ -2,17 +2,22 @@
2
2
 
3
3
  module Inspec
4
4
  class Control
5
- attr_accessor :id, :title, :desc, :impact, :tests
5
+ attr_accessor :id, :title, :desc, :impact, :tests, :tags
6
6
  def initialize
7
7
  @tests = []
8
+ @tags = []
8
9
  end
9
10
 
10
11
  def add_test(t)
11
12
  @tests.push(t)
12
13
  end
13
14
 
15
+ def add_tag(t)
16
+ @tags.push(t)
17
+ end
18
+
14
19
  def to_hash
15
- { id: id, title: title, desc: desc, impact: impact, tests: tests.map(&:to_hash) }
20
+ { id: id, title: title, desc: desc, impact: impact, tests: tests.map(&:to_hash), tags: tags.map(&:to_hash) }
16
21
  end
17
22
 
18
23
  def to_ruby
@@ -20,6 +25,7 @@ module Inspec
20
25
  res.push " title #{title.inspect}" unless title.to_s.empty?
21
26
  res.push " desc #{desc.inspect}" unless desc.to_s.empty?
22
27
  res.push " impact #{impact}" unless impact.nil?
28
+ tags.each { |t| res.push(indent(t.to_ruby, 2)) }
23
29
  tests.each { |t| res.push(indent(t.to_ruby, 2)) }
24
30
  res.push 'end'
25
31
  res.join("\n")
@@ -0,0 +1,27 @@
1
+ # encoding:utf-8
2
+
3
+ module Inspec
4
+ class Tag
5
+ attr_accessor :key, :value
6
+
7
+ def initialize(key, value)
8
+ @key = key
9
+ @value = value
10
+ end
11
+
12
+ def to_hash
13
+ {
14
+ name: key,
15
+ value: value,
16
+ }
17
+ end
18
+
19
+ def to_ruby
20
+ "tag #{key.inspect}: #{value.inspect}"
21
+ end
22
+
23
+ def to_s
24
+ "Tag #{key} with #{value}"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,66 @@
1
+ # encoding: utf-8
2
+ # author: Adam Leff
3
+
4
+ require 'inspec/profile'
5
+
6
+ module Inspec
7
+ class ProfileVendor
8
+ attr_reader :profile_path
9
+
10
+ def initialize(path)
11
+ @profile_path = Pathname.new(path)
12
+ end
13
+
14
+ def vendor!
15
+ vendor_dependencies
16
+ end
17
+
18
+ # The URL fetcher uses a Tempfile to retrieve the vendored
19
+ # profile, which creates a file that is only readable by
20
+ # the current user. In most circumstances, this is likely OK.
21
+ # However, in environments like a Habitat package, these files
22
+ # need to be readable by all users or the Habitat Supervisor
23
+ # may not be able to start InSpec correctly.
24
+ #
25
+ # This method makes sure all vendored files are mode 644 for this
26
+ # use case. This method is not called by default - the caller
27
+ # vendoring the profile must make the decision as to whether this
28
+ # is necessary.
29
+ def make_readable
30
+ Dir.glob("#{cache_path}/**/*") do |e|
31
+ FileUtils.chmod(0644, e) if File.file?(e)
32
+ end
33
+ end
34
+
35
+ def cache_path
36
+ profile_path.join('vendor')
37
+ end
38
+
39
+ def lockfile
40
+ profile_path.join('inspec.lock')
41
+ end
42
+
43
+ private
44
+
45
+ def profile
46
+ @profile ||= Inspec::Profile.for_target(profile_path.to_s, profile_opts)
47
+ end
48
+
49
+ def profile_opts
50
+ {
51
+ cache: Inspec::Cache.new(cache_path.to_s),
52
+ backend: Inspec::Backend.create(target: 'mock://'),
53
+ }
54
+ end
55
+
56
+ def vendor_dependencies
57
+ delete_vendored_data
58
+ File.write(lockfile, profile.generate_lockfile.to_yaml)
59
+ end
60
+
61
+ def delete_vendored_data
62
+ FileUtils.rm_rf(cache_path) if cache_path.exist?
63
+ File.delete(lockfile) if lockfile.exist?
64
+ end
65
+ end
66
+ end
@@ -95,6 +95,7 @@ require 'resources/iptables'
95
95
  require 'resources/json'
96
96
  require 'resources/kernel_module'
97
97
  require 'resources/kernel_parameter'
98
+ require 'resources/key_rsa'
98
99
  require 'resources/limits_conf'
99
100
  require 'resources/login_def'
100
101
  require 'resources/mount'
@@ -131,6 +132,7 @@ require 'resources/windows_feature'
131
132
  require 'resources/windows_task'
132
133
  require 'resources/xinetd'
133
134
  require 'resources/wmi'
135
+ require 'resources/x509_certificate'
134
136
  require 'resources/yum'
135
137
  require 'resources/zfs_dataset'
136
138
  require 'resources/zfs_pool'
@@ -0,0 +1,174 @@
1
+ # encoding: utf-8
2
+ require 'json'
3
+
4
+ module Inspec
5
+ class Schema # rubocop:disable Metrics/ClassLength
6
+ STATISTICS = {
7
+ 'type' => 'object',
8
+ 'additionalProperties' => false,
9
+ 'properties' => {
10
+ 'duration' => { 'type' => 'number' },
11
+ },
12
+ }.freeze
13
+
14
+ # Tags are open right, with simple key-value associations and not restrictions
15
+ TAGS = { 'type' => 'object' }.freeze
16
+
17
+ RESULT = {
18
+ 'type' => 'object',
19
+ 'additionalProperties' => false,
20
+ 'properties' => {
21
+ 'status' => { 'type' => 'string' },
22
+ 'code_desc' => { 'type' => 'string' },
23
+ 'run_time' => { 'type' => 'number' },
24
+ 'start_time' => { 'type' => 'string' },
25
+ 'skip_message' => { 'type' => 'string', 'optional' => true },
26
+ 'resource' => { 'type' => 'string', 'optional' => true },
27
+ },
28
+ }.freeze
29
+
30
+ REF = {
31
+ 'type' => 'object',
32
+ 'additionalProperties' => false,
33
+ 'properties' => {
34
+ 'ref' => { 'type' => 'string' },
35
+ # TODO: One of these needs to be deprecated
36
+ 'uri' => { 'type' => 'string', 'optional' => true },
37
+ 'url' => { 'type' => 'string', 'optional' => true },
38
+ },
39
+ }.freeze
40
+ REFS = { 'type' => 'array', 'items' => REF }.freeze
41
+
42
+ CONTROL = {
43
+ 'type' => 'object',
44
+ 'additionalProperties' => false,
45
+ 'properties' => {
46
+ 'id' => { 'type' => 'string' },
47
+ 'title' => { 'type' => %w{string null} },
48
+ 'desc' => { 'type' => %w{string null} },
49
+ 'impact' => { 'type' => 'number' },
50
+ 'refs' => REFS,
51
+ 'tags' => TAGS,
52
+ 'code' => { 'type' => 'string' },
53
+ 'source_location' => {
54
+ 'type' => 'object',
55
+ 'properties' => {
56
+ 'ref' => { 'type' => 'string' },
57
+ 'line' => { 'type' => 'number' },
58
+ },
59
+ },
60
+ 'results' => { 'type' => 'array', 'items' => RESULT },
61
+ },
62
+ }.freeze
63
+
64
+ SUPPORTS = {
65
+ 'type' => 'object',
66
+ 'additionalProperties' => false,
67
+ 'properties' => {
68
+ 'os-family' => { 'type' => 'string', 'optional' => true },
69
+ },
70
+ }.freeze
71
+
72
+ CONTROL_GROUP = {
73
+ 'type' => 'object',
74
+ 'additionalProperties' => false,
75
+ 'properties' => {
76
+ 'id' => { 'type' => 'string' },
77
+ 'title' => { 'type' => 'string', 'optional' => true },
78
+ 'controls' => { 'type' => 'array', 'items' => { 'type' => 'string' } },
79
+ },
80
+ }.freeze
81
+
82
+ PROFILE = {
83
+ 'type' => 'object',
84
+ 'additionalProperties' => false,
85
+ 'properties' => {
86
+ 'name' => { 'type' => 'string' },
87
+ 'version' => { 'type' => 'string', 'optional' => true },
88
+
89
+ 'title' => { 'type' => 'string', 'optional' => true },
90
+ 'maintainer' => { 'type' => 'string', 'optional' => true },
91
+ 'copyright' => { 'type' => 'string', 'optional' => true },
92
+ 'copyright_email' => { 'type' => 'string', 'optional' => true },
93
+ 'license' => { 'type' => 'string', 'optional' => true },
94
+ 'summary' => { 'type' => 'string', 'optional' => true },
95
+
96
+ 'supports' => {
97
+ 'type' => 'array',
98
+ 'items' => SUPPORTS,
99
+ 'optional' => true,
100
+ },
101
+ 'controls' => {
102
+ 'type' => 'array',
103
+ 'items' => CONTROL,
104
+ },
105
+ 'groups' => {
106
+ 'type' => 'array',
107
+ 'items' => CONTROL_GROUP,
108
+ },
109
+ 'attributes' => {
110
+ 'type' => 'array',
111
+ # TODO: more detailed specification needed
112
+ },
113
+ },
114
+ }.freeze
115
+
116
+ EXEC_JSON = {
117
+ 'type' => 'object',
118
+ 'additionalProperties' => false,
119
+ 'properties' => {
120
+ 'profiles' => {
121
+ 'type' => 'array',
122
+ 'items' => PROFILE,
123
+ },
124
+ 'statistics' => STATISTICS,
125
+ 'version' => { 'type' => 'string' },
126
+
127
+ # DEPRECATED PROPERTIES!! These will be removed with the next major version bump
128
+ 'controls' => 'array',
129
+ 'other_checks' => 'array',
130
+ },
131
+ }.freeze
132
+
133
+ MIN_CONTROL = {
134
+ 'type' => 'object',
135
+ 'additionalProperties' => false,
136
+ 'properties' => {
137
+ 'id' => { 'type' => 'string' },
138
+ 'profile_id' => { 'type' => %w{string null} },
139
+ 'status' => { 'type' => 'string' },
140
+ 'code_desc' => { 'type' => 'string' },
141
+ 'skip_message' => { 'type' => 'string', 'optional' => true },
142
+ 'resource' => { 'type' => 'string', 'optional' => true },
143
+ },
144
+ }.freeze
145
+
146
+ EXEC_JSONMIN = {
147
+ 'type' => 'object',
148
+ 'additionalProperties' => false,
149
+ 'properties' => {
150
+ 'statistics' => STATISTICS,
151
+ 'version' => { 'type' => 'string' },
152
+ 'controls' => {
153
+ 'type' => 'array',
154
+ 'items' => MIN_CONTROL,
155
+ },
156
+ },
157
+ }.freeze
158
+
159
+ LIST = {
160
+ 'exec-json' => EXEC_JSON,
161
+ 'exec-jsonmin' => EXEC_JSONMIN,
162
+ }.freeze
163
+
164
+ def self.names
165
+ LIST.keys
166
+ end
167
+
168
+ def self.json(name)
169
+ v = LIST[name] ||
170
+ raise("Cannot find schema #{name.inspect}.")
171
+ JSON.dump(v)
172
+ end
173
+ end
174
+ end