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
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module InspecPlugins
|
4
|
+
module Compliance
|
5
|
+
# stores configuration on local filesystem
|
6
|
+
class Configuration
|
7
|
+
def initialize
|
8
|
+
@config_path = File.join(Dir.home, '.inspec', 'compliance')
|
9
|
+
# ensure the directory is available
|
10
|
+
unless File.directory?(@config_path)
|
11
|
+
FileUtils.mkdir_p(@config_path)
|
12
|
+
end
|
13
|
+
# set config file path
|
14
|
+
@config_file = File.join(@config_path, '/config.json')
|
15
|
+
@config = {}
|
16
|
+
|
17
|
+
# load the data
|
18
|
+
get
|
19
|
+
end
|
20
|
+
|
21
|
+
# direct access to config
|
22
|
+
def [](key)
|
23
|
+
@config[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, value)
|
27
|
+
@config[key] = value
|
28
|
+
end
|
29
|
+
|
30
|
+
def key?(key)
|
31
|
+
@config.key?(key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def clean
|
35
|
+
@config = {}
|
36
|
+
end
|
37
|
+
|
38
|
+
# return the json data
|
39
|
+
def get
|
40
|
+
if File.exist?(@config_file)
|
41
|
+
file = File.read(@config_file)
|
42
|
+
@config = JSON.parse(file)
|
43
|
+
end
|
44
|
+
@config
|
45
|
+
end
|
46
|
+
|
47
|
+
# stores a hash to json
|
48
|
+
def store
|
49
|
+
File.open(@config_file, 'w') do |f|
|
50
|
+
f.chmod(0600)
|
51
|
+
f.write(@config.to_json)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# deletes data
|
56
|
+
def destroy
|
57
|
+
if File.exist?(@config_file)
|
58
|
+
File.delete(@config_file)
|
59
|
+
else
|
60
|
+
true
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# return if the (stored) api version does not support a certain feature
|
65
|
+
def supported?(feature)
|
66
|
+
sup = version_with_support(feature)
|
67
|
+
|
68
|
+
# we do not know the version, therefore we do not know if its possible to use the feature
|
69
|
+
return if self['version'].nil? || self['version']['version'].nil?
|
70
|
+
|
71
|
+
if sup.is_a?(Array)
|
72
|
+
Gem::Version.new(self['version']['version']) >= sup[0] &&
|
73
|
+
Gem::Version.new(self['version']['version']) < sup[1]
|
74
|
+
else
|
75
|
+
Gem::Version.new(self['version']['version']) >= sup
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# exit 1 if the version of compliance that we're working with doesn't support odic
|
80
|
+
def legacy_check!(feature)
|
81
|
+
return if supported?(feature)
|
82
|
+
|
83
|
+
puts "This feature (#{feature}) is not available for legacy installations."
|
84
|
+
puts 'Please upgrade to a recent version of Chef Compliance.'
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# for a feature, returns either:
|
91
|
+
# - a version v0: v supports v0 iff v0 <= v
|
92
|
+
# - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
|
93
|
+
def version_with_support(feature)
|
94
|
+
case feature.to_sym
|
95
|
+
when :oidc
|
96
|
+
Gem::Version.new('0.16.19')
|
97
|
+
else
|
98
|
+
Gem::Version.new('0.0.0')
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'net/http/post/multipart'
|
5
|
+
require 'uri'
|
6
|
+
|
7
|
+
module InspecPlugins
|
8
|
+
module Compliance
|
9
|
+
# implements a simple http abstraction on top of Net::HTTP
|
10
|
+
class HTTP
|
11
|
+
# generic get requires
|
12
|
+
def self.get(url, headers = nil, insecure)
|
13
|
+
uri = _parse_url(url)
|
14
|
+
req = Net::HTTP::Get.new(uri.path)
|
15
|
+
headers&.each do |key, value|
|
16
|
+
req.add_field(key, value)
|
17
|
+
end
|
18
|
+
send_request(uri, req, insecure)
|
19
|
+
end
|
20
|
+
|
21
|
+
# generic post request
|
22
|
+
def self.post(url, token, insecure, basic_auth = false)
|
23
|
+
# form request
|
24
|
+
uri = _parse_url(url)
|
25
|
+
req = Net::HTTP::Post.new(uri.path)
|
26
|
+
if basic_auth
|
27
|
+
req.basic_auth token, ''
|
28
|
+
else
|
29
|
+
req['Authorization'] = "Bearer #{token}"
|
30
|
+
end
|
31
|
+
req.form_data={}
|
32
|
+
|
33
|
+
send_request(uri, req, insecure)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.post_with_headers(url, headers, body, insecure)
|
37
|
+
uri = _parse_url(url)
|
38
|
+
req = Net::HTTP::Post.new(uri.path)
|
39
|
+
req.body = body unless body.nil?
|
40
|
+
headers&.each do |key, value|
|
41
|
+
req.add_field(key, value)
|
42
|
+
end
|
43
|
+
send_request(uri, req, insecure)
|
44
|
+
end
|
45
|
+
|
46
|
+
# post a file
|
47
|
+
def self.post_file(url, headers, file_path, insecure)
|
48
|
+
uri = _parse_url(url)
|
49
|
+
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
51
|
+
|
52
|
+
# set connection flags
|
53
|
+
http.use_ssl = (uri.scheme == 'https')
|
54
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
|
55
|
+
|
56
|
+
req = Net::HTTP::Post.new(uri.path)
|
57
|
+
headers.each do |key, value|
|
58
|
+
req.add_field(key, value)
|
59
|
+
end
|
60
|
+
|
61
|
+
req.body_stream=File.open(file_path, 'rb')
|
62
|
+
req.add_field('Content-Length', File.size(file_path))
|
63
|
+
req.add_field('Content-Type', 'application/x-gzip')
|
64
|
+
|
65
|
+
boundary = 'INSPEC-PROFILE-UPLOAD'
|
66
|
+
req.add_field('session', boundary)
|
67
|
+
res=http.request(req)
|
68
|
+
res
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.post_multipart_file(url, headers, file_path, insecure)
|
72
|
+
uri = _parse_url(url)
|
73
|
+
raise "Unable to parse URL: #{url}" if uri.nil? || uri.host.nil?
|
74
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
75
|
+
|
76
|
+
# set connection flags
|
77
|
+
http.use_ssl = (uri.scheme == 'https')
|
78
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if insecure
|
79
|
+
|
80
|
+
File.open(file_path) do |tar|
|
81
|
+
req = Net::HTTP::Post::Multipart.new(uri, 'file' => UploadIO.new(tar, 'application/x-gzip', File.basename(file_path)))
|
82
|
+
headers.each do |key, value|
|
83
|
+
req.add_field(key, value)
|
84
|
+
end
|
85
|
+
res = http.request(req)
|
86
|
+
return res
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# sends a http requests
|
91
|
+
def self.send_request(uri, req, insecure)
|
92
|
+
opts = {
|
93
|
+
use_ssl: uri.scheme == 'https',
|
94
|
+
}
|
95
|
+
opts[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if insecure
|
96
|
+
|
97
|
+
raise "Unable to parse URI: #{uri}" if uri.nil? || uri.host.nil?
|
98
|
+
res = Net::HTTP.start(uri.host, uri.port, opts) { |http|
|
99
|
+
http.request(req)
|
100
|
+
}
|
101
|
+
res
|
102
|
+
rescue OpenSSL::SSL::SSLError => e
|
103
|
+
raise e unless e.message.include? 'certificate verify failed'
|
104
|
+
|
105
|
+
puts "Error: Failed to connect to #{uri}."
|
106
|
+
puts 'If the server uses a self-signed certificate, please re-run the login command with the --insecure option.'
|
107
|
+
exit 1
|
108
|
+
end
|
109
|
+
|
110
|
+
def self._parse_url(url)
|
111
|
+
url = "https://#{url}" if URI.parse(url).scheme.nil?
|
112
|
+
URI.parse(url)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
File without changes
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module InspecPlugins
|
4
|
+
module Compliance
|
5
|
+
# is a helper that provides information which version of compliance supports
|
6
|
+
# which feature
|
7
|
+
class Support
|
8
|
+
# for a feature, returns either:
|
9
|
+
# - a version v0: v supports v0 iff v0 <= v
|
10
|
+
# - an array [v0, v1] of two versions: v supports [v0, v1] iff v0 <= v < v1
|
11
|
+
def self.version_with_support(feature)
|
12
|
+
case feature.to_sym
|
13
|
+
when :oidc # open id connect authentication
|
14
|
+
Gem::Version.new('0.16.19')
|
15
|
+
else
|
16
|
+
Gem::Version.new('0.0.0')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# determines if the given version support a certain feature
|
21
|
+
def self.supported?(feature, version)
|
22
|
+
sup = version_with_support(feature)
|
23
|
+
|
24
|
+
if sup.is_a?(Array)
|
25
|
+
Gem::Version.new(version) >= sup[0] &&
|
26
|
+
Gem::Version.new(version) < sup[1]
|
27
|
+
else
|
28
|
+
Gem::Version.new(version) >= sup
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# we do not know the version, therefore we do not know if its possible to use the feature
|
33
|
+
# return if self['version'].nil? || self['version']['version'].nil?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'inspec/fetcher'
|
5
|
+
require 'inspec/errors'
|
6
|
+
|
7
|
+
# InSpec Target Helper for Chef Compliance
|
8
|
+
# reuses UrlHelper, but it knows the target server and the access token already
|
9
|
+
# similar to `inspec exec http://localhost:2134/owners/%base%/compliance/%ssh%/tar --user %token%`
|
10
|
+
module InspecPlugins
|
11
|
+
module Compliance
|
12
|
+
class Fetcher < Fetchers::Url
|
13
|
+
name 'compliance'
|
14
|
+
priority 500
|
15
|
+
attr_reader :upstream_sha256
|
16
|
+
|
17
|
+
def initialize(target, opts)
|
18
|
+
super(target, opts)
|
19
|
+
@upstream_sha256 = ''
|
20
|
+
if target.is_a?(Hash) && target.key?(:url)
|
21
|
+
@target = target[:url]
|
22
|
+
@upstream_sha256 = target[:sha256]
|
23
|
+
elsif target.is_a?(String)
|
24
|
+
@target = target
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def sha256
|
29
|
+
upstream_sha256.empty? ? super : upstream_sha256
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.check_compliance_token(uri, config)
|
33
|
+
if config['token'].nil? && config['refresh_token'].nil?
|
34
|
+
if config['server_type'] == 'automate'
|
35
|
+
server = 'automate'
|
36
|
+
msg = 'inspec compliance login https://your_automate_server --user USER --ent ENT --dctoken DCTOKEN or --token USERTOKEN'
|
37
|
+
elsif config['server_type'] == 'automate2'
|
38
|
+
server = 'automate2'
|
39
|
+
msg = 'inspec compliance login https://your_automate2_server --user USER --token APITOKEN'
|
40
|
+
else
|
41
|
+
server = 'compliance'
|
42
|
+
msg = "inspec compliance login https://your_compliance_server --user admin --insecure --token 'PASTE TOKEN HERE' "
|
43
|
+
end
|
44
|
+
raise Inspec::FetcherFailure, <<~EOF
|
45
|
+
|
46
|
+
Cannot fetch #{uri} because your #{server} token has not been
|
47
|
+
configured.
|
48
|
+
|
49
|
+
Please login using
|
50
|
+
|
51
|
+
#{msg}
|
52
|
+
EOF
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.get_target_uri(target)
|
57
|
+
if target.is_a?(String) && URI(target).scheme == 'compliance'
|
58
|
+
URI(target)
|
59
|
+
elsif target.respond_to?(:key?) && target.key?(:compliance)
|
60
|
+
URI("compliance://#{target[:compliance]}")
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.resolve(target)
|
65
|
+
uri = get_target_uri(target)
|
66
|
+
return nil if uri.nil?
|
67
|
+
|
68
|
+
config = InspecPlugins::Compliance::Configuration.new
|
69
|
+
profile = InspecPlugins::Compliance::API.sanitize_profile_name(uri)
|
70
|
+
profile_fetch_url = InspecPlugins::Compliance::API.target_url(config, profile)
|
71
|
+
# we have detailed information available in our lockfile, no need to ask the server
|
72
|
+
if target.respond_to?(:key?) && target.key?(:sha256)
|
73
|
+
profile_checksum = target[:sha256]
|
74
|
+
else
|
75
|
+
check_compliance_token(uri, config)
|
76
|
+
# verifies that the target e.g base/ssh exists
|
77
|
+
# Call profiles directly instead of exist? to capture the results
|
78
|
+
# so we can access the upstream sha256 from the results.
|
79
|
+
_msg, profile_result = InspecPlugins::Compliance::API.profiles(config, profile)
|
80
|
+
if profile_result.empty?
|
81
|
+
raise Inspec::FetcherFailure, "The compliance profile #{profile} was not found on the configured compliance server"
|
82
|
+
else
|
83
|
+
# Guarantee sorting by verison and grab the latest.
|
84
|
+
# If version was specified, it will be the first and only result.
|
85
|
+
# Note we are calling the sha256 as a string, not a symbol since
|
86
|
+
# it was returned as json from the Compliance API.
|
87
|
+
profile_info = profile_result.sort_by { |x| Gem::Version.new(x['version']) }[0]
|
88
|
+
profile_checksum = profile_info.key?('sha256') ? profile_info['sha256'] : ''
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# We need to pass the token to the fetcher
|
92
|
+
config['token'] = InspecPlugins::Compliance::API.get_token(config)
|
93
|
+
|
94
|
+
# Needed for automate2 post request
|
95
|
+
profile_stub = profile || target[:compliance]
|
96
|
+
config['profile'] = InspecPlugins::Compliance::API.profile_split(profile_stub)
|
97
|
+
|
98
|
+
new({ url: profile_fetch_url, sha256: profile_checksum }, config)
|
99
|
+
rescue URI::Error => _e
|
100
|
+
nil
|
101
|
+
end
|
102
|
+
|
103
|
+
# We want to save compliance: in the lockfile rather than url: to
|
104
|
+
# make sure we go back through the Compliance API handling.
|
105
|
+
def resolved_source
|
106
|
+
@resolved_source ||= {
|
107
|
+
compliance: compliance_profile_name,
|
108
|
+
url: @target,
|
109
|
+
sha256: sha256,
|
110
|
+
}
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_s
|
114
|
+
'Chef Compliance Profile Loader'
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
# determine the owner_id and the profile name from the url
|
120
|
+
def compliance_profile_name
|
121
|
+
m = if InspecPlugins::Compliance::API.is_automate_server_pre_080?(@config)
|
122
|
+
%r{^#{@config['server']}/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
|
123
|
+
elsif InspecPlugins::Compliance::API.is_automate_server_080_and_later?(@config)
|
124
|
+
%r{^#{@config['server']}/profiles/(?<owner>[^/]+)/(?<id>[^/]+)/tar$}
|
125
|
+
else
|
126
|
+
%r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}
|
127
|
+
end.match(@target)
|
128
|
+
|
129
|
+
if InspecPlugins::Compliance::API.is_automate2_server?(@config)
|
130
|
+
m = {}
|
131
|
+
m[:owner] = @config['profile'][0]
|
132
|
+
m[:id] = @config['profile'][1]
|
133
|
+
end
|
134
|
+
|
135
|
+
raise 'Unable to determine compliance profile name. This can be caused by ' \
|
136
|
+
'an incorrect server in your configuration. Try to login to compliance ' \
|
137
|
+
'via the `inspec compliance login` command.' if m.nil?
|
138
|
+
|
139
|
+
"#{m[:owner]}/#{m[:id]}"
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative '../../../shared/core_plugin_test_helper.rb'
|
4
|
+
|
5
|
+
class ComplianceCli < MiniTest::Test
|
6
|
+
include CorePluginFunctionalHelper
|
7
|
+
|
8
|
+
def test_help_output
|
9
|
+
out = run_inspec_process('compliance help')
|
10
|
+
assert_equal out.exit_status, 0
|
11
|
+
assert_includes out.stdout, 'inspec compliance exec PROFILE'
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_logout_command
|
15
|
+
out = run_inspec_process('compliance logout')
|
16
|
+
assert_equal out.exit_status, 0
|
17
|
+
assert_includes out.stdout, ''
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_error_login_with_invalid_url
|
21
|
+
out = run_inspec_process('compliance login')
|
22
|
+
assert_equal out.exit_status, 1
|
23
|
+
assert_includes out.stderr, 'ERROR: "inspec compliance login" was called with no arguments'
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_profile_list_without_auth
|
27
|
+
out = run_inspec_process('compliance profiles')
|
28
|
+
assert_equal out.exit_status, 0 # TODO: make this error
|
29
|
+
assert_includes out.stdout, 'You need to login first with `inspec compliance login`'
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_error_upload_without_args
|
33
|
+
out = run_inspec_process('compliance upload')
|
34
|
+
assert_equal out.exit_status, 1
|
35
|
+
assert_includes out.stderr, 'ERROR: "inspec compliance upload" was called with no arguments'
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_error_upload_with_fake_path
|
39
|
+
out = run_inspec_process('compliance upload /path/to/dir')
|
40
|
+
assert_equal out.exit_status, 0 # TODO: make this error
|
41
|
+
assert_includes out.stdout, 'You need to login first with `inspec compliance login`'
|
42
|
+
end
|
43
|
+
end
|
File without changes
|