inspec-core 2.3.5 → 2.3.10

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -8
  3. data/lib/bundles/inspec-compliance/api.rb +3 -353
  4. data/lib/bundles/inspec-compliance/configuration.rb +3 -102
  5. data/lib/bundles/inspec-compliance/http.rb +3 -115
  6. data/lib/bundles/inspec-compliance/support.rb +3 -35
  7. data/lib/bundles/inspec-compliance/target.rb +3 -142
  8. data/lib/inspec/base_cli.rb +4 -1
  9. data/lib/inspec/cli.rb +1 -1
  10. data/lib/inspec/control_eval_context.rb +2 -2
  11. data/lib/inspec/version.rb +1 -1
  12. data/lib/matchers/matchers.rb +3 -3
  13. data/lib/{bundles → plugins}/inspec-compliance/README.md +0 -0
  14. data/lib/plugins/inspec-compliance/lib/inspec-compliance.rb +12 -0
  15. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api.rb +358 -0
  16. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api/login.rb +192 -0
  17. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +266 -0
  18. data/lib/plugins/inspec-compliance/lib/inspec-compliance/configuration.rb +103 -0
  19. data/lib/plugins/inspec-compliance/lib/inspec-compliance/http.rb +116 -0
  20. data/lib/{bundles → plugins/inspec-compliance/lib}/inspec-compliance/images/cc-token.png +0 -0
  21. data/lib/plugins/inspec-compliance/lib/inspec-compliance/support.rb +36 -0
  22. data/lib/plugins/inspec-compliance/lib/inspec-compliance/target.rb +143 -0
  23. data/lib/plugins/inspec-compliance/test/functional/inspec_compliance_test.rb +43 -0
  24. data/lib/{bundles → plugins}/inspec-compliance/test/integration/default/cli.rb +0 -0
  25. data/lib/plugins/inspec-compliance/test/unit/api/login_test.rb +190 -0
  26. data/lib/plugins/inspec-compliance/test/unit/api_test.rb +385 -0
  27. data/lib/plugins/inspec-compliance/test/unit/target_test.rb +155 -0
  28. data/lib/resources/processes.rb +19 -3
  29. metadata +17 -10
  30. data/lib/bundles/inspec-compliance.rb +0 -16
  31. data/lib/bundles/inspec-compliance/.kitchen.yml +0 -20
  32. data/lib/bundles/inspec-compliance/api/login.rb +0 -193
  33. data/lib/bundles/inspec-compliance/bootstrap.sh +0 -41
  34. data/lib/bundles/inspec-compliance/cli.rb +0 -276
@@ -1,116 +1,4 @@
1
- # encoding: utf-8
2
- # author: Christoph Hartmann
3
- # author: Dominik Richter
1
+ # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
2
+ # TODO: Remove in inspec 4.0
4
3
 
5
- require 'net/http'
6
- require 'net/http/post/multipart'
7
- require 'uri'
8
-
9
- module Compliance
10
- # implements a simple http abstraction on top of Net::HTTP
11
- class HTTP
12
- # generic get requires
13
- def self.get(url, headers = nil, insecure)
14
- uri = _parse_url(url)
15
- req = Net::HTTP::Get.new(uri.path)
16
- headers&.each do |key, value|
17
- req.add_field(key, value)
18
- end
19
- send_request(uri, req, insecure)
20
- end
21
-
22
- # generic post request
23
- def self.post(url, token, insecure, basic_auth = false)
24
- # form request
25
- uri = _parse_url(url)
26
- req = Net::HTTP::Post.new(uri.path)
27
- if basic_auth
28
- req.basic_auth token, ''
29
- else
30
- req['Authorization'] = "Bearer #{token}"
31
- end
32
- req.form_data={}
33
-
34
- send_request(uri, req, insecure)
35
- end
36
-
37
- def self.post_with_headers(url, headers, body, insecure)
38
- uri = _parse_url(url)
39
- req = Net::HTTP::Post.new(uri.path)
40
- req.body = body unless body.nil?
41
- headers&.each do |key, value|
42
- req.add_field(key, value)
43
- end
44
- send_request(uri, req, insecure)
45
- end
46
-
47
- # post a file
48
- def self.post_file(url, headers, file_path, insecure)
49
- uri = _parse_url(url)
50
- raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
51
- http = Net::HTTP.new(uri.host, uri.port)
52
-
53
- # set connection flags
54
- http.use_ssl = (uri.scheme == 'https')
55
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
56
-
57
- req = Net::HTTP::Post.new(uri.path)
58
- headers.each do |key, value|
59
- req.add_field(key, value)
60
- end
61
-
62
- req.body_stream=File.open(file_path, 'rb')
63
- req.add_field('Content-Length', File.size(file_path))
64
- req.add_field('Content-Type', 'application/x-gzip')
65
-
66
- boundary = 'INSPEC-PROFILE-UPLOAD'
67
- req.add_field('session', boundary)
68
- res=http.request(req)
69
- res
70
- end
71
-
72
- def self.post_multipart_file(url, headers, file_path, insecure)
73
- uri = _parse_url(url)
74
- raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
75
- http = Net::HTTP.new(uri.host, uri.port)
76
-
77
- # set connection flags
78
- http.use_ssl = (uri.scheme == 'https')
79
- http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
80
-
81
- File.open(file_path) do |tar|
82
- req = Net::HTTP::Post::Multipart.new(uri, 'file' => UploadIO.new(tar, 'application/x-gzip', File.basename(file_path)))
83
- headers.each do |key, value|
84
- req.add_field(key, value)
85
- end
86
- res = http.request(req)
87
- return res
88
- end
89
- end
90
-
91
- # sends a http requests
92
- def self.send_request(uri, req, insecure)
93
- opts = {
94
- use_ssl: uri.scheme == 'https',
95
- }
96
- opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
97
-
98
- raise "Unable to parse URI: #{uri}" if uri.nil? || uri.host.nil?
99
- res = Net::HTTP.start(uri.host, uri.port, opts) { |http|
100
- http.request(req)
101
- }
102
- res
103
- rescue OpenSSL::SSL::SSLError => e
104
- raise e unless e.message.include? 'certificate verify failed'
105
-
106
- puts "Error: Failed to connect to #{uri}."
107
- puts 'If the server uses a self-signed certificate, please re-run the login command with the --insecure option.'
108
- exit 1
109
- end
110
-
111
- def self._parse_url(url)
112
- url = "https://#{url}" if URI.parse(url).scheme.nil?
113
- URI.parse(url)
114
- end
115
- end
116
- end
4
+ require 'plugins/inspec-compliance/lib/inspec-compliance/http'
@@ -1,36 +1,4 @@
1
- # encoding: utf-8
2
- # author: Christoph Hartmann
3
- # author: Dominik Richter
1
+ # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
2
+ # TODO: Remove in inspec 4.0
4
3
 
5
- module Compliance
6
- # is a helper that provides information which version of compliance supports
7
- # which feature
8
- class Support
9
- # for a feature, returns either:
10
- # - a version v0: v supports v0 iff v0 <= v
11
- # - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
12
- def self.version_with_support(feature)
13
- case feature.to_sym
14
- when :oidc # open id connect authentication
15
- Gem::Version.new('0.16.19')
16
- else
17
- Gem::Version.new('0.0.0')
18
- end
19
- end
20
-
21
- # determines if the given version support a certain feature
22
- def self.supported?(feature, version)
23
- sup = version_with_support(feature)
24
-
25
- if sup.is_a?(Array)
26
- Gem::Version.new(version) >= sup[0] &&
27
- Gem::Version.new(version) < sup[1]
28
- else
29
- Gem::Version.new(version) >= sup
30
- end
31
- end
32
-
33
- # we do not know the version, therefore we do not know if its possible to use the feature
34
- # return if self['version'].nil? || self['version']['version'].nil?
35
- end
36
- end
4
+ require 'plugins/inspec-compliance/lib/inspec-compliance/support'
@@ -1,143 +1,4 @@
1
- # encoding: utf-8
2
- # author: Christoph Hartmann
3
- # author: Dominik Richter
1
+ # This file has been moved to the v2.0 plugins. This redirect allows for legacy use.
2
+ # TODO: Remove in inspec 4.0
4
3
 
5
- require 'uri'
6
- require 'inspec/fetcher'
7
- require 'inspec/errors'
8
-
9
- # InSpec Target Helper for Chef Compliance
10
- # reuses UrlHelper, but it knows the target server and the access token already
11
- # similar to `inspec exec http://localhost:2134/owners/%base%/compliance/%ssh%/tar --user %token%`
12
- module Compliance
13
- class Fetcher < Fetchers::Url
14
- name 'compliance'
15
- priority 500
16
- attr_reader :upstream_sha256
17
-
18
- def initialize(target, opts)
19
- super(target, opts)
20
- @upstream_sha256 = ''
21
- if target.is_a?(Hash) && target.key?(:url)
22
- @target = target[:url]
23
- @upstream_sha256 = target[:sha256]
24
- elsif target.is_a?(String)
25
- @target = target
26
- end
27
- end
28
-
29
- def sha256
30
- upstream_sha256.empty? ? super : upstream_sha256
31
- end
32
-
33
- def self.check_compliance_token(uri, config)
34
- if config['token'].nil? && config['refresh_token'].nil?
35
- if config['server_type'] == 'automate'
36
- server = 'automate'
37
- msg = 'inspec compliance login https://your_automate_server --user USER --ent ENT --dctoken DCTOKEN or --token USERTOKEN'
38
- elsif config['server_type'] == 'automate2'
39
- server = 'automate2'
40
- msg = 'inspec compliance login https://your_automate2_server --user USER --token APITOKEN'
41
- else
42
- server = 'compliance'
43
- msg = "inspec compliance login https://your_compliance_server --user admin --insecure --token 'PASTE TOKEN HERE' "
44
- end
45
- raise Inspec::FetcherFailure, <<~EOF
46
-
47
- Cannot fetch #{uri} because your #{server} token has not been
48
- configured.
49
-
50
- Please login using
51
-
52
- #{msg}
53
- EOF
54
- end
55
- end
56
-
57
- def self.get_target_uri(target)
58
- if target.is_a?(String) && URI(target).scheme == 'compliance'
59
- URI(target)
60
- elsif target.respond_to?(:key?) && target.key?(:compliance)
61
- URI("compliance://#{target[:compliance]}")
62
- end
63
- end
64
-
65
- def self.resolve(target)
66
- uri = get_target_uri(target)
67
- return nil if uri.nil?
68
-
69
- config = Compliance::Configuration.new
70
- profile = Compliance::API.sanitize_profile_name(uri)
71
- profile_fetch_url = Compliance::API.target_url(config, profile)
72
- # we have detailed information available in our lockfile, no need to ask the server
73
- if target.respond_to?(:key?) && target.key?(:sha256)
74
- profile_checksum = target[:sha256]
75
- else
76
- check_compliance_token(uri, config)
77
- # verifies that the target e.g base/ssh exists
78
- # Call profiles directly instead of exist? to capture the results
79
- # so we can access the upstream sha256 from the results.
80
- _msg, profile_result = Compliance::API.profiles(config, profile)
81
- if profile_result.empty?
82
- raise Inspec::FetcherFailure, "The compliance profile #{profile} was not found on the configured compliance server"
83
- else
84
- # Guarantee sorting by verison and grab the latest.
85
- # If version was specified, it will be the first and only result.
86
- # Note we are calling the sha256 as a string, not a symbol since
87
- # it was returned as json from the Compliance API.
88
- profile_info = profile_result.sort_by { |x| Gem::Version.new(x['version']) }[0]
89
- profile_checksum = profile_info.key?('sha256') ? profile_info['sha256'] : ''
90
- end
91
- end
92
- # We need to pass the token to the fetcher
93
- config['token'] = Compliance::API.get_token(config)
94
-
95
- # Needed for automate2 post request
96
- profile_stub = profile || target[:compliance]
97
- config['profile'] = Compliance::API.profile_split(profile_stub)
98
-
99
- new({ url: profile_fetch_url, sha256: profile_checksum }, config)
100
- rescue URI::Error => _e
101
- nil
102
- end
103
-
104
- # We want to save compliance: in the lockfile rather than url: to
105
- # make sure we go back through the Compliance API handling.
106
- def resolved_source
107
- @resolved_source ||= {
108
- compliance: compliance_profile_name,
109
- url: @target,
110
- sha256: sha256,
111
- }
112
- end
113
-
114
- def to_s
115
- 'Chef Compliance Profile Loader'
116
- end
117
-
118
- private
119
-
120
- # determine the owner_id and the profile name from the url
121
- def compliance_profile_name
122
- m = if Compliance::API.is_automate_server_pre_080?(@config)
123
- %r{^#{@config['server']}/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
124
- elsif Compliance::API.is_automate_server_080_and_later?(@config)
125
- %r{^#{@config['server']}/profiles/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
126
- else
127
- %r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}
128
- end.match(@target)
129
-
130
- if Compliance::API.is_automate2_server?(@config)
131
- m = {}
132
- m[:owner] = @config['profile'][0]
133
- m[:id] = @config['profile'][1]
134
- end
135
-
136
- raise 'Unable to determine compliance profile name. This can be caused by ' \
137
- 'an incorrect server in your configuration. Try to login to compliance ' \
138
- 'via the `inspec compliance login` command.' if m.nil?
139
-
140
- "#{m[:owner]}/#{m[:id]}"
141
- end
142
- end
143
- end
4
+ require 'plugins/inspec-compliance/lib/inspec-compliance/target'
@@ -292,7 +292,10 @@ module Inspec
292
292
  end
293
293
 
294
294
  # check for compliance settings
295
- Compliance::API.login(o['compliance']) if o['compliance']
295
+ if o['compliance']
296
+ require 'plugins/inspec-compliance/lib/inspec-compliance/api'
297
+ InspecPlugins::Compliance::API.login(o['compliance'])
298
+ end
296
299
 
297
300
  o
298
301
  end
@@ -221,7 +221,7 @@ class Inspec::InspecCLI < Inspec::BaseCLI
221
221
  option :depends, type: :array, default: [],
222
222
  desc: 'A space-delimited list of local folders containing profiles whose libraries and resources will be loaded into the new shell'
223
223
  option :distinct_exit, type: :boolean, default: true,
224
- desc: 'Exit with code 101 if any tests fail, and 100 if any are skipped (default). If disabled, exit 0 on skips and 1 for failures.'
224
+ desc: 'Exit with code 100 if any tests fail, and 101 if any are skipped but none failed (default). If disabled, exit 0 on skips and 1 for failures.'
225
225
  def shell_func
226
226
  o = opts(:shell).dup
227
227
  diagnose(o)
@@ -142,8 +142,8 @@ module Inspec
142
142
  end
143
143
 
144
144
  # method for attributes; import attribute handling
145
- define_method :attribute do |name, options = {}|
146
- if options.empty?
145
+ define_method :attribute do |name, options = nil|
146
+ if options.nil?
147
147
  Inspec::AttributeRegistry.find_attribute(name, profile_id).value
148
148
  else
149
149
  profile_context_owner.register_attribute(name, options)
@@ -4,5 +4,5 @@
4
4
  # author: Christoph Hartmann
5
5
 
6
6
  module Inspec
7
- VERSION = '2.3.5'
7
+ VERSION = '2.3.10'
8
8
  end
@@ -294,13 +294,13 @@ RSpec::Matchers.define :cmp do |first_expected| # rubocop:disable Metrics/BlockL
294
294
  end
295
295
 
296
296
  failure_message do |actual|
297
- actual = ('0' + actual.to_s(8)).inspect if octal?(@expected)
298
- "\n" + format_expectation(false) + "\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
297
+ actual = ('0' + actual.to_s(8)) if octal?(@expected)
298
+ "\n" + format_expectation(false) + "\n got: #{actual.inspect}\n\n(compared using `cmp` matcher)\n"
299
299
  end
300
300
 
301
301
  failure_message_when_negated do |actual|
302
302
  actual = ('0' + actual.to_s(8)).inspect if octal?(@expected)
303
- "\n" + format_expectation(true) + "\n got: #{actual}\n\n(compared using `cmp` matcher)\n"
303
+ "\n" + format_expectation(true) + "\n got: #{actual.inspect}\n\n(compared using `cmp` matcher)\n"
304
304
  end
305
305
 
306
306
  description do
@@ -0,0 +1,12 @@
1
+ module InspecPlugins
2
+ module Compliance
3
+ class Plugin < Inspec.plugin(2)
4
+ plugin_name :'inspec-compliance'
5
+
6
+ cli_command :compliance do
7
+ require_relative 'inspec-compliance/cli'
8
+ InspecPlugins::Compliance::CLI
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,358 @@
1
+ # encoding: utf-8
2
+
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'json'
6
+
7
+ require_relative 'api/login'
8
+ require_relative 'configuration'
9
+ require_relative 'http'
10
+ require_relative 'target'
11
+ require_relative 'support'
12
+
13
+ module InspecPlugins
14
+ module Compliance
15
+ class ServerConfigurationMissing < StandardError; end
16
+
17
+ # API Implementation does not hold any state by itself,
18
+ # everything will be stored in local Configuration store
19
+ class API
20
+ extend InspecPlugins::Compliance::API::Login
21
+
22
+ # return all compliance profiles available for the user
23
+ # the user is either specified in the options hash or by default
24
+ # the username of the account is used that is logged in
25
+ def self.profiles(config, profile_filter = nil) # rubocop:disable PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize, Metrics/MethodLength
26
+ owner = config['owner'] || config['user']
27
+
28
+ # Chef Compliance
29
+ if is_compliance_server?(config)
30
+ url = "#{config['server']}/user/compliance"
31
+ # Chef Automate2
32
+ elsif is_automate2_server?(config)
33
+ url = "#{config['server']}/compliance/profiles/search"
34
+ # Chef Automate
35
+ elsif is_automate_server?(config)
36
+ url = "#{config['server']}/profiles/#{owner}"
37
+ else
38
+ raise ServerConfigurationMissing
39
+ end
40
+
41
+ headers = get_headers(config)
42
+ if profile_filter
43
+ _owner, id, ver = profile_split(profile_filter)
44
+ else
45
+ id, ver = nil
46
+ end
47
+
48
+ if is_automate2_server?(config)
49
+ body = { owner: owner, name: id }.to_json
50
+ response = InspecPlugins::Compliance::HTTP.post_with_headers(url, headers, body, config['insecure'])
51
+ else
52
+ response = InspecPlugins::Compliance::HTTP.get(url, headers, config['insecure'])
53
+ end
54
+ data = response.body
55
+ response_code = response.code
56
+ case response_code
57
+ when '200'
58
+ msg = 'success'
59
+ profiles = JSON.parse(data)
60
+ # iterate over profiles
61
+ if is_compliance_server?(config)
62
+ mapped_profiles = []
63
+ profiles.values.each { |org|
64
+ mapped_profiles += org.values
65
+ }
66
+ # Chef Automate pre 0.8.0
67
+ elsif is_automate_server_pre_080?(config)
68
+ mapped_profiles = profiles.values.flatten
69
+ elsif is_automate2_server?(config)
70
+ mapped_profiles = []
71
+ profiles['profiles'].each { |p|
72
+ mapped_profiles << p
73
+ }
74
+ else
75
+ mapped_profiles = profiles.map { |e|
76
+ e['owner_id'] = owner
77
+ e
78
+ }
79
+ end
80
+ # filter by name and version if they were specified in profile_filter
81
+ mapped_profiles.select! do |p|
82
+ (!ver || p['version'] == ver) && (!id || p['name'] == id)
83
+ end
84
+ return msg, mapped_profiles
85
+ when '401'
86
+ msg = '401 Unauthorized. Please check your token.'
87
+ return msg, []
88
+ else
89
+ msg = "An unexpected error occurred (HTTP #{response_code}): #{response.message}"
90
+ return msg, []
91
+ end
92
+ end
93
+
94
+ # return the server api version
95
+ # NB this method does not use Compliance::Configuration to allow for using
96
+ # it before we know the version (e.g. oidc or not)
97
+ def self.version(config)
98
+ url = config['server']
99
+ insecure = config['insecure']
100
+
101
+ raise ServerConfigurationMissing if url.nil?
102
+
103
+ headers = get_headers(config)
104
+ response = InspecPlugins::Compliance::HTTP.get(url+'/version', headers, insecure)
105
+ return {} if response.code == '404'
106
+
107
+ data = response.body
108
+ return {} if data.nil? || data.empty?
109
+
110
+ parsed = JSON.parse(data)
111
+ return {} unless parsed.key?('version') && !parsed['version'].empty?
112
+
113
+ parsed
114
+ end
115
+
116
+ # verifies that a profile exists
117
+ def self.exist?(config, profile)
118
+ _msg, profiles = InspecPlugins::Compliance::API.profiles(config, profile)
119
+ !profiles.empty?
120
+ end
121
+
122
+ def self.upload(config, owner, profile_name, archive_path)
123
+ # Chef Compliance
124
+ if is_compliance_server?(config)
125
+ url = "#{config['server']}/owners/#{owner}/compliance/#{profile_name}/tar"
126
+ # Chef Automate pre 0.8.0
127
+ elsif is_automate_server_pre_080?(config)
128
+ url = "#{config['server']}/#{owner}"
129
+ elsif is_automate2_server?(config)
130
+ url = "#{config['server']}/compliance/profiles?owner=#{owner}"
131
+ # Chef Automate
132
+ else
133
+ url = "#{config['server']}/profiles/#{owner}"
134
+ end
135
+
136
+ headers = get_headers(config)
137
+ if is_automate2_server?(config)
138
+ res = InspecPlugins::Compliance::HTTP.post_multipart_file(url, headers, archive_path, config['insecure'])
139
+ else
140
+ res = InspecPlugins::Compliance::HTTP.post_file(url, headers, archive_path, config['insecure'])
141
+ end
142
+
143
+ [res.is_a?(Net::HTTPSuccess), res.body]
144
+ end
145
+
146
+ # Use username and refresh_token to get an API access token
147
+ def self.get_token_via_refresh_token(url, refresh_token, insecure)
148
+ uri = URI.parse("#{url}/login")
149
+ req = Net::HTTP::Post.new(uri.path)
150
+ req.body = { token: refresh_token }.to_json
151
+ access_token = nil
152
+ response = InspecPlugins::Compliance::HTTP.send_request(uri, req, insecure)
153
+ data = response.body
154
+ if response.code == '200'
155
+ begin
156
+ tokendata = JSON.parse(data)
157
+ access_token = tokendata['access_token']
158
+ msg = 'Successfully fetched API access token'
159
+ success = true
160
+ rescue JSON::ParserError => e
161
+ success = false
162
+ msg = e.message
163
+ end
164
+ else
165
+ success = false
166
+ msg = "Failed to authenticate to #{url} \n\
167
+ Response code: #{response.code}\n Body: #{response.body}"
168
+ end
169
+
170
+ [success, msg, access_token]
171
+ end
172
+
173
+ # Use username and password to get an API access token
174
+ def self.get_token_via_password(url, username, password, insecure)
175
+ uri = URI.parse("#{url}/login")
176
+ req = Net::HTTP::Post.new(uri.path)
177
+ req.body = { userid: username, password: password }.to_json
178
+ access_token = nil
179
+ response = InspecPlugins::Compliance::HTTP.send_request(uri, req, insecure)
180
+ data = response.body
181
+ if response.code == '200'
182
+ access_token = data
183
+ msg = 'Successfully fetched an API access token valid for 12 hours'
184
+ success = true
185
+ else
186
+ success = false
187
+ msg = "Failed to authenticate to #{url} \n\
188
+ Response code: #{response.code}\n Body: #{response.body}"
189
+ end
190
+
191
+ [success, msg, access_token]
192
+ end
193
+
194
+ def self.get_headers(config)
195
+ token = get_token(config)
196
+ if is_automate_server?(config) || is_automate2_server?(config)
197
+ headers = { 'chef-delivery-enterprise' => config['automate']['ent'] }
198
+ if config['automate']['token_type'] == 'dctoken'
199
+ headers['x-data-collector-token'] = token
200
+ else
201
+ headers['chef-delivery-user'] = config['user']
202
+ headers['chef-delivery-token'] = token
203
+ end
204
+ else
205
+ headers = { 'Authorization' => "Bearer #{token}" }
206
+ end
207
+ headers
208
+ end
209
+
210
+ def self.get_token(config)
211
+ return config['token'] unless config['refresh_token']
212
+ _success, _msg, token = get_token_via_refresh_token(config['server'], config['refresh_token'], config['insecure'])
213
+ token
214
+ end
215
+
216
+ def self.target_url(config, profile)
217
+ owner, id, ver = profile_split(profile)
218
+
219
+ return "#{config['server']}/compliance/profiles/tar" if is_automate2_server?(config)
220
+ return "#{config['server']}/owners/#{owner}/compliance/#{id}/tar" unless is_automate_server?(config)
221
+
222
+ if ver.nil?
223
+ "#{config['server']}/profiles/#{owner}/#{id}/tar"
224
+ else
225
+ "#{config['server']}/profiles/#{owner}/#{id}/version/#{ver}/tar"
226
+ end
227
+ end
228
+
229
+ def self.profile_split(profile)
230
+ owner, id = profile.split('/')
231
+ id, version = id.split('#')
232
+ [owner, id, version]
233
+ end
234
+
235
+ # returns a parsed url for `admin/profile` or `compliance://admin/profile`
236
+ def self.sanitize_profile_name(profile)
237
+ if URI(profile).scheme == 'compliance'
238
+ uri = URI(profile)
239
+ else
240
+ uri = URI("compliance://#{profile}")
241
+ end
242
+ uri.to_s.sub(%r{^compliance:\/\/}, '')
243
+ end
244
+
245
+ def self.is_compliance_server?(config)
246
+ config['server_type'] == 'compliance'
247
+ end
248
+
249
+ def self.is_automate_server_pre_080?(config)
250
+ # Automate versions before 0.8.x do not have a valid version in the config
251
+ return false unless config['server_type'] == 'automate'
252
+ server_version_from_config(config).nil?
253
+ end
254
+
255
+ def self.is_automate_server_080_and_later?(config)
256
+ # Automate versions 0.8.x and later will have a "version" key in the config
257
+ # that is properly parsed out via server_version_from_config below
258
+ return false unless config['server_type'] == 'automate'
259
+ !server_version_from_config(config).nil?
260
+ end
261
+
262
+ def self.is_automate2_server?(config)
263
+ config['server_type'] == 'automate2'
264
+ end
265
+
266
+ def self.is_automate_server?(config)
267
+ config['server_type'] == 'automate'
268
+ end
269
+
270
+ def self.server_version_from_config(config)
271
+ # Automate versions 0.8.x and later will have a "version" key in the config
272
+ # that looks like: "version":{"api":"compliance","version":"0.8.24"}
273
+ return nil unless config.key?('version')
274
+ return nil unless config['version'].is_a?(Hash)
275
+ config['version']['version']
276
+ end
277
+
278
+ def self.determine_server_type(url, insecure)
279
+ if target_is_automate2_server?(url, insecure)
280
+ :automate2
281
+ elsif target_is_automate_server?(url, insecure)
282
+ :automate
283
+ elsif target_is_compliance_server?(url, insecure)
284
+ :compliance
285
+ else
286
+ Inspec::Log.debug('Could not determine server type using known endpoints')
287
+ nil
288
+ end
289
+ end
290
+
291
+ def self.target_is_automate2_server?(url, insecure)
292
+ automate_endpoint = '/dex/auth'
293
+ response = InspecPlugins::Compliance::HTTP.get(url + automate_endpoint, nil, insecure)
294
+ if response.code == '400'
295
+ Inspec::Log.debug(
296
+ "Received 400 from #{url}#{automate_endpoint} - " \
297
+ 'assuming target is a Chef Automate2 instance',
298
+ )
299
+ true
300
+ else
301
+ false
302
+ end
303
+ end
304
+
305
+ def self.target_is_automate_server?(url, insecure)
306
+ automate_endpoint = '/compliance/version'
307
+ response = InspecPlugins::Compliance::HTTP.get(url + automate_endpoint, nil, insecure)
308
+ case response.code
309
+ when '401'
310
+ Inspec::Log.debug(
311
+ "Received 401 from #{url}#{automate_endpoint} - " \
312
+ 'assuming target is a Chef Automate instance',
313
+ )
314
+ true
315
+ when '200'
316
+ # Chef Automate currently returns 401 for `/compliance/version` but some
317
+ # versions of OpsWorks Chef Automate return 200 and a Chef Manage page
318
+ # when unauthenticated requests are received.
319
+ if response.body.include?('Are You Looking For the Chef Server?')
320
+ Inspec::Log.debug(
321
+ "Received 200 from #{url}#{automate_endpoint} - " \
322
+ 'assuming target is an OpsWorks Chef Automate instance',
323
+ )
324
+ true
325
+ else
326
+ Inspec::Log.debug(
327
+ "Received 200 from #{url}#{automate_endpoint} " \
328
+ 'but did not receive the Chef Manage page - ' \
329
+ 'assuming target is not a Chef Automate instance',
330
+ )
331
+ false
332
+ end
333
+ else
334
+ Inspec::Log.debug(
335
+ "Received unexpected status code #{response.code} " \
336
+ "from #{url}#{automate_endpoint} - " \
337
+ 'assuming target is not a Chef Automate instance',
338
+ )
339
+ false
340
+ end
341
+ end
342
+
343
+ def self.target_is_compliance_server?(url, insecure)
344
+ # All versions of Chef Compliance return 200 for `/api/version`
345
+ compliance_endpoint = '/api/version'
346
+
347
+ response = InspecPlugins::Compliance::HTTP.get(url + compliance_endpoint, nil, insecure)
348
+ return false unless response.code == '200'
349
+
350
+ Inspec::Log.debug(
351
+ "Received 200 from #{url}#{compliance_endpoint} - " \
352
+ 'assuming target is a Compliance server',
353
+ )
354
+ true
355
+ end
356
+ end
357
+ end
358
+ end