inspec 2.3.5 → 2.3.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -8
- data/Rakefile +1 -2
- 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
|