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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -8
- data/lib/bundles/inspec-compliance/api.rb +3 -353
- data/lib/bundles/inspec-compliance/configuration.rb +3 -102
- data/lib/bundles/inspec-compliance/http.rb +3 -115
- data/lib/bundles/inspec-compliance/support.rb +3 -35
- data/lib/bundles/inspec-compliance/target.rb +3 -142
- data/lib/inspec/base_cli.rb +4 -1
- data/lib/inspec/cli.rb +1 -1
- data/lib/inspec/control_eval_context.rb +2 -2
- data/lib/inspec/version.rb +1 -1
- data/lib/matchers/matchers.rb +3 -3
- data/lib/{bundles → plugins}/inspec-compliance/README.md +0 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance.rb +12 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/api.rb +358 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/api/login.rb +192 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +266 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/configuration.rb +103 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/http.rb +116 -0
- data/lib/{bundles → plugins/inspec-compliance/lib}/inspec-compliance/images/cc-token.png +0 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/support.rb +36 -0
- data/lib/plugins/inspec-compliance/lib/inspec-compliance/target.rb +143 -0
- data/lib/plugins/inspec-compliance/test/functional/inspec_compliance_test.rb +43 -0
- data/lib/{bundles → plugins}/inspec-compliance/test/integration/default/cli.rb +0 -0
- data/lib/plugins/inspec-compliance/test/unit/api/login_test.rb +190 -0
- data/lib/plugins/inspec-compliance/test/unit/api_test.rb +385 -0
- data/lib/plugins/inspec-compliance/test/unit/target_test.rb +155 -0
- data/lib/resources/processes.rb +19 -3
- metadata +17 -10
- data/lib/bundles/inspec-compliance.rb +0 -16
- data/lib/bundles/inspec-compliance/.kitchen.yml +0 -20
- data/lib/bundles/inspec-compliance/api/login.rb +0 -193
- data/lib/bundles/inspec-compliance/bootstrap.sh +0 -41
- data/lib/bundles/inspec-compliance/cli.rb +0 -276
@@ -1,116 +1,4 @@
|
|
1
|
-
#
|
2
|
-
#
|
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 '
|
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
|
-
#
|
2
|
-
#
|
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
|
-
|
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
|
-
#
|
2
|
-
#
|
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 '
|
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'
|
data/lib/inspec/base_cli.rb
CHANGED
@@ -292,7 +292,10 @@ module Inspec
|
|
292
292
|
end
|
293
293
|
|
294
294
|
# check for compliance settings
|
295
|
-
|
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
|
data/lib/inspec/cli.rb
CHANGED
@@ -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
|
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.
|
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)
|
data/lib/inspec/version.rb
CHANGED
data/lib/matchers/matchers.rb
CHANGED
@@ -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))
|
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
|
File without changes
|
@@ -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
|