inspec 2.3.5 → 2.3.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -8
  3. data/Rakefile +1 -2
  4. data/lib/bundles/inspec-compliance/api.rb +3 -353
  5. data/lib/bundles/inspec-compliance/configuration.rb +3 -102
  6. data/lib/bundles/inspec-compliance/http.rb +3 -115
  7. data/lib/bundles/inspec-compliance/support.rb +3 -35
  8. data/lib/bundles/inspec-compliance/target.rb +3 -142
  9. data/lib/inspec/base_cli.rb +4 -1
  10. data/lib/inspec/cli.rb +1 -1
  11. data/lib/inspec/control_eval_context.rb +2 -2
  12. data/lib/inspec/version.rb +1 -1
  13. data/lib/matchers/matchers.rb +3 -3
  14. data/lib/{bundles → plugins}/inspec-compliance/README.md +0 -0
  15. data/lib/plugins/inspec-compliance/lib/inspec-compliance.rb +12 -0
  16. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api.rb +358 -0
  17. data/lib/plugins/inspec-compliance/lib/inspec-compliance/api/login.rb +192 -0
  18. data/lib/plugins/inspec-compliance/lib/inspec-compliance/cli.rb +266 -0
  19. data/lib/plugins/inspec-compliance/lib/inspec-compliance/configuration.rb +103 -0
  20. data/lib/plugins/inspec-compliance/lib/inspec-compliance/http.rb +116 -0
  21. data/lib/{bundles → plugins/inspec-compliance/lib}/inspec-compliance/images/cc-token.png +0 -0
  22. data/lib/plugins/inspec-compliance/lib/inspec-compliance/support.rb +36 -0
  23. data/lib/plugins/inspec-compliance/lib/inspec-compliance/target.rb +143 -0
  24. data/lib/plugins/inspec-compliance/test/functional/inspec_compliance_test.rb +43 -0
  25. data/lib/{bundles → plugins}/inspec-compliance/test/integration/default/cli.rb +0 -0
  26. data/lib/plugins/inspec-compliance/test/unit/api/login_test.rb +190 -0
  27. data/lib/plugins/inspec-compliance/test/unit/api_test.rb +385 -0
  28. data/lib/plugins/inspec-compliance/test/unit/target_test.rb +155 -0
  29. data/lib/resources/processes.rb +19 -3
  30. metadata +17 -10
  31. data/lib/bundles/inspec-compliance.rb +0 -16
  32. data/lib/bundles/inspec-compliance/.kitchen.yml +0 -20
  33. data/lib/bundles/inspec-compliance/api/login.rb +0 -193
  34. data/lib/bundles/inspec-compliance/bootstrap.sh +0 -41
  35. 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