inspec 1.17.0 → 1.18.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.
@@ -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