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
@@ -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
@@ -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