chef 16.7.61 → 16.8.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -2
  3. data/README.md +1 -1
  4. data/chef.gemspec +2 -1
  5. data/lib/chef/application/base.rb +1 -1
  6. data/lib/chef/client.rb +3 -0
  7. data/lib/chef/compliance/default_attributes.rb +89 -0
  8. data/lib/chef/compliance/fetcher/automate.rb +69 -0
  9. data/lib/chef/compliance/fetcher/chef_server.rb +134 -0
  10. data/lib/chef/compliance/reporter/automate.rb +202 -0
  11. data/lib/chef/compliance/reporter/chef_server_automate.rb +92 -0
  12. data/lib/chef/compliance/reporter/compliance_enforcer.rb +20 -0
  13. data/lib/chef/compliance/reporter/json_file.rb +19 -0
  14. data/lib/chef/compliance/runner.rb +250 -0
  15. data/lib/chef/cookbook_manifest.rb +1 -0
  16. data/lib/chef/encrypted_data_bag_item/assertions.rb +1 -1
  17. data/lib/chef/exceptions.rb +4 -0
  18. data/lib/chef/http/ssl_policies.rb +6 -0
  19. data/lib/chef/knife/bootstrap/train_connector.rb +1 -1
  20. data/lib/chef/knife/core/ui.rb +4 -1
  21. data/lib/chef/knife/ssh.rb +1 -1
  22. data/lib/chef/mixin/powershell_exec.rb +3 -1
  23. data/lib/chef/platform/query_helpers.rb +4 -4
  24. data/lib/chef/powershell.rb +2 -0
  25. data/lib/chef/provider/dsc_resource.rb +12 -24
  26. data/lib/chef/provider/dsc_script.rb +16 -20
  27. data/lib/chef/provider/git.rb +5 -5
  28. data/lib/chef/resource/chef_client_config.rb +1 -1
  29. data/lib/chef/resource/dsc_script.rb +8 -1
  30. data/lib/chef/resource/hostname.rb +3 -3
  31. data/lib/chef/resource/template.rb +2 -2
  32. data/lib/chef/resource/windows_certificate.rb +7 -1
  33. data/lib/chef/resource_collection/resource_set.rb +1 -1
  34. data/lib/chef/util/dsc/configuration_generator.rb +52 -11
  35. data/lib/chef/util/dsc/lcm_output_parser.rb +3 -4
  36. data/lib/chef/util/dsc/local_configuration_manager.rb +17 -14
  37. data/lib/chef/util/dsc/resource_store.rb +5 -11
  38. data/lib/chef/version.rb +1 -1
  39. data/lib/chef/win32/api/file.rb +4 -0
  40. data/spec/functional/resource/dsc_script_spec.rb +3 -6
  41. data/spec/integration/client/client_spec.rb +2 -1
  42. data/spec/integration/compliance/compliance_spec.rb +81 -0
  43. data/spec/integration/recipes/recipe_dsl_spec.rb +1 -0
  44. data/spec/spec_helper.rb +1 -1
  45. data/spec/unit/client_spec.rb +1 -0
  46. data/spec/unit/compliance/fetcher/automate_spec.rb +134 -0
  47. data/spec/unit/compliance/fetcher/chef_server_spec.rb +93 -0
  48. data/spec/unit/compliance/reporter/automate_spec.rb +427 -0
  49. data/spec/unit/compliance/reporter/chef_server_automate_spec.rb +177 -0
  50. data/spec/unit/compliance/reporter/compliance_enforcer_spec.rb +48 -0
  51. data/spec/unit/compliance/runner_spec.rb +113 -0
  52. data/spec/unit/http/ssl_policies_spec.rb +11 -0
  53. data/spec/unit/knife/core/node_editor_spec.rb +1 -1
  54. data/spec/unit/mixin/powershell_exec_spec.rb +1 -1
  55. data/spec/unit/platform/query_helpers_spec.rb +11 -12
  56. data/spec/unit/provider/dsc_resource_spec.rb +10 -27
  57. data/spec/unit/provider/dsc_script_spec.rb +1 -1
  58. data/spec/unit/provider/mount/windows_spec.rb +1 -0
  59. data/spec/unit/provider/systemd_unit_spec.rb +1 -1
  60. data/spec/unit/resource/windows_certificate_spec.rb +12 -0
  61. data/spec/unit/util/dsc/configuration_generator_spec.rb +79 -0
  62. data/spec/unit/util/dsc/local_configuration_manager_spec.rb +27 -35
  63. metadata +37 -12
  64. data/lib/chef/util/powershell/cmdlet.rb +0 -169
  65. data/lib/chef/util/powershell/cmdlet_result.rb +0 -61
  66. data/spec/functional/util/powershell/cmdlet_spec.rb +0 -111
  67. data/spec/unit/util/powershell/cmdlet_spec.rb +0 -106
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 45d0c49c6265534a875b8044449678109b0c11a22b83027f28826e3ea14ee792
4
- data.tar.gz: a6375a107a0ece5bbd866f42c0d415e582135d24512d8815b894786e64c685b9
3
+ metadata.gz: c13a95870faf2ddc93377295ea1821d3ee962c422af54147a9a5852266ef9abc
4
+ data.tar.gz: 06d9ed38997dd63cef3a9d54cf7ffb064c5ca13b668cbea8ee42173ba757d880
5
5
  SHA512:
6
- metadata.gz: 8cf6a1c8dd0316944eaa700f6c788e2ff6843dc9bd9252b2710e2465498041e1eb8a6990ace8bbef2bb265e3254e1460a56e9f85489d133a0dffbc03b140c928
7
- data.tar.gz: e57ee2f117d322936e53b384202691b7d9be946b4be9b4cc697d162163f3b65885109ef5977448861fa6f8db3cafbe364294ab2e30d78b61f7e51d8fc7eed16a
6
+ metadata.gz: ed8ed48612596fdd94dfab56edda9b66ba3adff6bcca4475bf4d4a9b5ab9dc51f59b3db9d54aa6f468ab33aa4a8f2821a5ab9942379c0d70f641a04f8bfb48d0
7
+ data.tar.gz: 8f1ebbbdbfe60ed00c66edab06a41821f5d8e9f62ab74d4c322b187b75e4f894e0f59cdf90c61b034a8496ece30e0a6eb71c3c91336a40e966fb6f9001e26d9a
data/Gemfile CHANGED
@@ -27,8 +27,7 @@ gem "chef-telemetry", ">=1.0.8" # 1.0.8 removes the http dep
27
27
  group(:omnibus_package) do
28
28
  gem "appbundler"
29
29
  gem "rb-readline"
30
- gem "inspec-core", "~> 4.18"
31
- gem "inspec-core-bin", "~> 4.18" # need to provide the binaries for inspec
30
+ gem "inspec-core-bin", "~> 4.24" # need to provide the binaries for inspec
32
31
  gem "chef-vault"
33
32
  end
34
33
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  [![Code Climate](https://codeclimate.com/github/chef/chef.svg)](https://codeclimate.com/github/chef/chef)
3
3
  [![Build Status](https://badge.buildkite.com/c82093430ceec7d27af05febb9dcafe3aa331fff9d74c0ab9d.svg?branch=master)](https://buildkite.com/chef-oss/chef-chef-master-verify)
4
4
  [![Gem Version](https://badge.fury.io/rb/chef.svg)](https://badge.fury.io/rb/chef)
5
- [![](https://img.shields.io/badge/Release%20Policy-Cadence%20Release-brightgreen.svg)](https://github.com/chef/chef/blob/v15.2.21/docs/dev/design_documents/client_release_cadence.md)
5
+ [![](https://img.shields.io/badge/Release%20Policy-Cadence%20Release-brightgreen.svg)](https://github.com/chef/chef/blob/master/docs/dev/design_documents/client_release_cadence.md)
6
6
 
7
7
  **Umbrella Project**: [Chef Infra](https://github.com/chef/chef-oss-practices/blob/master/projects/chef-infra.md)
8
8
 
@@ -27,10 +27,11 @@ Gem::Specification.new do |s|
27
27
  s.add_dependency "mixlib-shellout", ">= 3.1.1", "< 4.0"
28
28
  s.add_dependency "mixlib-archive", ">= 0.4", "< 2.0"
29
29
  s.add_dependency "ohai", "~> 16.0"
30
+ s.add_dependency "inspec-core", "~> 4.23"
30
31
 
31
32
  s.add_dependency "ffi", ">= 1.9.25"
32
33
  s.add_dependency "ffi-yajl", "~> 2.2"
33
- s.add_dependency "net-ssh", ">= 4.2", "< 7"
34
+ s.add_dependency "net-ssh", ">= 5.1", "< 7"
34
35
  s.add_dependency "net-ssh-multi", "~> 1.2", ">= 1.2.1"
35
36
  s.add_dependency "net-sftp", ">= 2.1.2", "< 4.0"
36
37
  s.add_dependency "ed25519", "~> 1.2" # ed25519 ssh key support
@@ -366,7 +366,7 @@ class Chef::Application::Base < Chef::Application
366
366
  Chef::Log.trace("Download recipes tarball from #{url} to #{path}")
367
367
  if File.exist?(url)
368
368
  FileUtils.cp(url, path)
369
- elsif URI.regexp.match?(url)
369
+ elsif URI::DEFAULT_PARSER.make_regexp.match?(url)
370
370
  File.open(path, "wb") do |f|
371
371
  open(url) do |r|
372
372
  f.write(r.read)
@@ -57,6 +57,8 @@ require "ohai" unless defined?(Ohai::System)
57
57
  require "rbconfig" unless defined?(RbConfig)
58
58
  require "forwardable" unless defined?(Forwardable)
59
59
 
60
+ require_relative "compliance/runner"
61
+
60
62
  class Chef
61
63
  # == Chef::Client
62
64
  # The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
@@ -235,6 +237,7 @@ class Chef
235
237
 
236
238
  events.register(Chef::DataCollector::Reporter.new(events))
237
239
  events.register(Chef::ActionCollection.new(events))
240
+ events.register(Chef::Compliance::Runner.new)
238
241
 
239
242
  run_status.run_id = request_id = Chef::RequestID.instance.request_id
240
243
 
@@ -0,0 +1,89 @@
1
+ # Author:: Stephan Renatus <srenatus@chef.io>
2
+ # Copyright:: (c) 2016-2019, Chef Software Inc. <legal@chef.io>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require "chef/node/attribute_collections" # for VividMash
17
+ require "chef/util/path_helper"
18
+
19
+ class Chef
20
+ module Compliance
21
+ DEFAULT_ATTRIBUTES = Chef::Node::VividMash.new(
22
+ # If enabled, a cache is built for all backend calls. This should only be
23
+ # disabled if you are expecting unique results from the same backend call.
24
+ # Under the covers, this controls :command and :file caching on Chef InSpec's
25
+ # Train connection.
26
+ "inspec_backend_cache" => true,
27
+
28
+ # Controls what is done with the resulting report after the Chef InSpec run.
29
+ # Accepts a single string value or an array of multiple values.
30
+ # Accepted values: 'chef-server-automate', 'chef-automate', 'json-file', 'audit-enforcer'
31
+ "reporter" => "json-file",
32
+
33
+ # Controls if Chef InSpec profiles should be fetched from Chef Automate or Chef Infra Server
34
+ # in addition to the default fetch locations provided by Chef Inspec.
35
+ # Accepted values: nil, 'chef-server', 'chef-automate'
36
+ "fetcher" => nil,
37
+
38
+ # Allow for connections to HTTPS endpoints using self-signed ssl certificates.
39
+ "insecure" => nil,
40
+
41
+ # Controls verbosity of Chef InSpec runner.
42
+ "quiet" => true,
43
+
44
+ # Chef Inspec Compliance profiles to be used for scan of node.
45
+ # See README.md for details
46
+ "profiles" => {},
47
+
48
+ # Extra inputs passed to Chef InSpec to allow finer-grained control over behavior.
49
+ # These are mapped to Chef InSpec's inputs, but are named attributes here for legacy reasons.
50
+ # See Chef Inspec's documentation for more information: https://docs.chef.io/inspec/inputs/
51
+ "attributes" => {},
52
+
53
+ # A string path or an array of paths to Chef InSpec waiver files.
54
+ # See Chef Inspec's documentation for more information: https://docs.chef.io/inspec/waivers/
55
+ "waiver_file" => nil,
56
+
57
+ "json_file" => {
58
+ # The location on disk that Chef InSpec's json reports are saved to when using the
59
+ # 'json-file' reporter. Defaults to:
60
+ # <chef_cache_path>/compliance_reports/compliance-<timestamp>.json
61
+ "location" => Chef::Util::PathHelper.join(
62
+ Chef::Config[:cache_path],
63
+ "compliance_reports",
64
+ Time.now.utc.strftime("compliance-%Y%m%d%H%M%S.json")
65
+ ),
66
+ },
67
+
68
+ # Control results that have a `run_time` below this limit will
69
+ # be stripped of the `start_time` and `run_time` fields to
70
+ # reduce the size of the reports being sent to Chef Automate.
71
+ "run_time_limit" => 1.0,
72
+
73
+ # A control result message that exceeds this character limit will be truncated.
74
+ # This helps keep reports to a reasonable size. On rare occasions, we've seen messages exceeding 9 MB in size,
75
+ # causing the report to not be ingested in the backend because of the 4 MB report size rpc limitation.
76
+ # Chef InSpec will append this text at the end of any truncated messages: `[Truncated to 10000 characters]`
77
+ "result_message_limit" => 10000,
78
+
79
+ # When a Chef InSpec resource throws an exception, results will contain a short error message and a
80
+ # detailed ruby stacktrace of the error. This attribute instructs Chef InSpec not to include the detailed stacktrace in order
81
+ # to keep the overall report to a manageable size.
82
+ "result_include_backtrace" => false,
83
+
84
+ # The array of results per control will be truncated at this limit to avoid large reports that cannot be
85
+ # processed by Chef Automate. A summary of removed results will be sent with each impacted control.
86
+ "control_results_limit" => 50
87
+ )
88
+ end
89
+ end
@@ -0,0 +1,69 @@
1
+ require "uri" unless defined?(URI)
2
+ require "plugins/inspec-compliance/lib/inspec-compliance"
3
+
4
+ class Chef
5
+ module Compliance
6
+ module Fetcher
7
+ class Automate < ::InspecPlugins::Compliance::Fetcher
8
+ name "chef-automate"
9
+
10
+ # it positions itself before `compliance` fetcher
11
+ # only load it, if you want to use audit cookbook in Chef Solo with Chef Automate
12
+ priority 502
13
+
14
+ CONFIG = {
15
+ "insecure" => true,
16
+ "token" => nil,
17
+ "server_type" => "automate",
18
+ "automate" => {
19
+ "ent" => "default",
20
+ "token_type" => "dctoken",
21
+ },
22
+ }.freeze
23
+
24
+ def self.resolve(target)
25
+ uri = get_target_uri(target)
26
+ return nil if uri.nil?
27
+
28
+ config = CONFIG.dup
29
+
30
+ # we have detailed information available in our lockfile, no need to ask the server
31
+ if target.respond_to?(:key?) && target.key?(:url)
32
+ profile_fetch_url = target[:url]
33
+ else
34
+ # verifies that the target e.g base/ssh exists
35
+ base_path = "/compliance/profiles/#{uri.host}#{uri.path}"
36
+
37
+ profile_path = if target.respond_to?(:key?) && target.key?(:version)
38
+ "#{base_path}/version/#{target[:version]}/tar"
39
+ else
40
+ "#{base_path}/tar"
41
+ end
42
+
43
+ url = URI(Chef::Config[:data_collector][:server_url])
44
+ url.path = profile_path
45
+ profile_fetch_url = url.to_s
46
+
47
+ config["token"] = Chef::Config[:data_collector][:token]
48
+
49
+ if config["token"].nil?
50
+ raise Inspec::FetcherFailure,
51
+ "No data-collector token set, which is required by the chef-automate fetcher. " \
52
+ "Set the `data_collector.token` configuration parameter in your client.rb " \
53
+ 'or use the "chef-server-automate" reporter which does not require any ' \
54
+ "data-collector settings and uses #{ChefUtils::Dist::Server::PRODUCT} to fetch profiles."
55
+ end
56
+ end
57
+
58
+ new(profile_fetch_url, config)
59
+ rescue URI::Error => _e
60
+ nil
61
+ end
62
+
63
+ def to_s
64
+ "#{ChefUtils::Dist::Automate::PRODUCT} for #{ChefUtils::Dist::Solo::PRODUCT} Fetcher"
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,134 @@
1
+ require "uri" unless defined?(URI)
2
+ require "plugins/inspec-compliance/lib/inspec-compliance"
3
+
4
+ # This class implements an InSpec fetcher for Chef Server. The implementation
5
+ # is based on the Chef Compliance fetcher and only adapts the calls to redirect
6
+ # the requests via Chef Server.
7
+ #
8
+ # This implementation depends on chef-client runtime, therefore it is only executable
9
+ # inside of a chef-client run
10
+
11
+ class Chef
12
+ module Compliance
13
+ module Fetcher
14
+ class ChefServer < ::InspecPlugins::Compliance::Fetcher
15
+ name "chef-server"
16
+
17
+ # it positions itself before `compliance` fetcher
18
+ # only load it, if the Chef Server is integrated with Chef Compliance
19
+ priority 501
20
+
21
+ CONFIG = { "insecure" => true }.freeze
22
+
23
+ # Accepts URLs to compliance profiles in one of two forms:
24
+ # * a String URL with a compliance scheme, like "compliance://namespace/profile_name"
25
+ # * a Hash with a key of `compliance` and a value like "compliance/profile_name" and optionally a `version` key with a String value
26
+ def self.resolve(target)
27
+ profile_uri = get_target_uri(target)
28
+ return nil if profile_uri.nil?
29
+
30
+ organization = Chef::Config[:chef_server_url].split("/").last
31
+ owner = profile_uri.user ? "#{profile_uri.user}@#{profile_uri.host}" : profile_uri.host
32
+ version = target[:version] if target.respond_to?(:key?)
33
+
34
+ path_parts = [""]
35
+ path_parts << "compliance" if chef_server_reporter? || chef_server_fetcher?
36
+ path_parts << "organizations"
37
+ path_parts << organization
38
+ path_parts << "owners"
39
+ path_parts << owner
40
+ path_parts << "compliance"
41
+ path_parts << profile_uri.path
42
+ path_parts << "version/#{version}" if version
43
+ path_parts << "tar"
44
+
45
+ target_url = URI(Chef::Config[:chef_server_url])
46
+ target_url.path = File.join(path_parts)
47
+ Chef::Log.info("Fetching profile from: #{target_url}")
48
+
49
+ new(target_url, CONFIG)
50
+ rescue URI::Error => _e
51
+ nil
52
+ end
53
+
54
+ #
55
+ # We want to save compliance: in the lockfile rather than url: to
56
+ # make sure we go back through the ComplianceAPI handling.
57
+ #
58
+ def resolved_source
59
+ { compliance: chef_server_url }
60
+ end
61
+
62
+ # Downloads archive to temporary file using a Chef::ServerAPI
63
+ # client so that Chef Server's header-based authentication can be
64
+ # used.
65
+ def download_archive_to_temp
66
+ return @temp_archive_path unless @temp_archive_path.nil?
67
+
68
+ rest = Chef::ServerAPI.new(@target, Chef::Config.merge(ssl_verify_mode: :verify_none))
69
+ archive = with_http_rescue do
70
+ rest.streaming_request(@target)
71
+ end
72
+ @archive_type = ".tar.gz"
73
+
74
+ if archive.nil?
75
+ path = @target.respond_to?(:path) ? @target.path : path
76
+ raise Inspec::FetcherFailure, "Unable to find requested profile on path: '#{path}' on the #{ChefUtils::Dist::Automate::PRODUCT} system."
77
+ end
78
+
79
+ Inspec::Log.debug("Archive stored at temporary location: #{archive.path}")
80
+ @temp_archive_path = archive.path
81
+ end
82
+
83
+ def with_http_rescue
84
+ response = yield
85
+ if response.respond_to?(:code)
86
+ # handle non 200 error codes, they are not raised as Net::HTTPClientException
87
+ handle_http_error_code(response.code) if response.code.to_i >= 300
88
+ end
89
+ response
90
+ rescue Net::HTTPClientException => e
91
+ Chef::Log.error e
92
+ handle_http_error_code(e.response.code)
93
+ end
94
+
95
+ def handle_http_error_code(code)
96
+ case code
97
+ when /401|403/
98
+ Chef::Log.error "Auth issue: see audit cookbook TROUBLESHOOTING.md"
99
+ when /404/
100
+ Chef::Log.error "Object does not exist on remote server."
101
+ when /413/
102
+ Chef::Log.error "You most likely hit the erchef request size in #{ChefUtils::Dist::Server::PRODUCT} that defaults to ~2MB. To increase this limit see audit cookbook TROUBLESHOOTING.md OR https://docs.chef.io/config_rb_server.html"
103
+ when /429/
104
+ Chef::Log.error "This error typically means the data sent was larger than #{ChefUtils::Dist::Automate::PRODUCT}'s limit (4 MB). Run InSpec locally to identify any controls producing large diffs."
105
+ end
106
+ msg = "Received HTTP error #{code}"
107
+ Chef::Log.error msg
108
+ raise Inspec::FetcherFailure, msg
109
+ end
110
+
111
+ def to_s
112
+ "#{ChefUtils::Dist::Server::PRODUCT}/Compliance Profile Loader"
113
+ end
114
+
115
+ CHEF_SERVER_REPORTERS = %w{chef-server chef-server-compliance chef-server-visibility chef-server-automate}.freeze
116
+ def self.chef_server_reporter?
117
+ (Array(Chef.node.attributes["audit"]["reporter"]) & CHEF_SERVER_REPORTERS).any?
118
+ end
119
+
120
+ CHEF_SERVER_FETCHERS = %w{chef-server chef-server-compliance chef-server-visibility chef-server-automate}.freeze
121
+ def self.chef_server_fetcher?
122
+ CHEF_SERVER_FETCHERS.include?(Chef.node.attributes["audit"]["fetcher"])
123
+ end
124
+
125
+ private
126
+
127
+ def chef_server_url
128
+ m = %r{^#{@config['server']}/owners/(?<owner>[^/]+)/compliance/(?<id>[^/]+)/tar$}.match(@target)
129
+ "#{m[:owner]}/#{m[:id]}"
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,202 @@
1
+ class Chef
2
+ module Compliance
3
+ module Reporter
4
+ #
5
+ # Used to send inspec reports to Chef Automate via the data_collector service
6
+ #
7
+ class Automate
8
+ def initialize(opts)
9
+ @entity_uuid = opts[:entity_uuid]
10
+ @run_id = opts[:run_id]
11
+ @node_name = opts[:node_info][:node]
12
+ @environment = opts[:node_info][:environment]
13
+ @roles = opts[:node_info][:roles]
14
+ @recipes = opts[:node_info][:recipes]
15
+ @insecure = opts[:insecure]
16
+ @chef_tags = opts[:node_info][:chef_tags]
17
+ @policy_group = opts[:node_info][:policy_group]
18
+ @policy_name = opts[:node_info][:policy_name]
19
+ @source_fqdn = opts[:node_info][:source_fqdn]
20
+ @organization_name = opts[:node_info][:organization_name]
21
+ @ipaddress = opts[:node_info][:ipaddress]
22
+ @fqdn = opts[:node_info][:fqdn]
23
+ @run_time_limit = opts[:run_time_limit]
24
+ @control_results_limit = opts[:control_results_limit]
25
+ @timestamp = opts.fetch(:timestamp) { Time.now }
26
+
27
+ @url = Chef::Config[:data_collector][:server_url]
28
+ @token = Chef::Config[:data_collector][:token]
29
+ end
30
+
31
+ # Method used in order to send the inspec report to the data_collector server
32
+ def send_report(report)
33
+ unless @entity_uuid && @run_id
34
+ Chef::Log.error "entity_uuid(#{@entity_uuid}) or run_id(#{@run_id}) can't be nil, not sending report to #{ChefUtils::Dist::Automate::PRODUCT}"
35
+ return false
36
+ end
37
+
38
+ unless @url && @token
39
+ Chef::Log.warn "data_collector.token and data_collector.server_url must be defined in client.rb!"
40
+ Chef::Log.warn "Further information: https://github.com/chef-cookbooks/audit#direct-reporting-to-chef-automate"
41
+ return false
42
+ end
43
+
44
+ headers = {
45
+ "Content-Type" => "application/json",
46
+ "x-data-collector-auth" => "version=1.0",
47
+ "x-data-collector-token" => @token,
48
+ }
49
+
50
+ all_report_shas = report[:profiles].map { |p| p[:sha256] }
51
+ missing_report_shas = missing_automate_profiles(headers, all_report_shas)
52
+
53
+ full_report = truncate_controls_results(enriched_report(report), @control_results_limit)
54
+ full_report = strip_profiles_meta(full_report, missing_report_shas, @run_time_limit)
55
+ json_report = Chef::JSONCompat.to_json(full_report, validate_utf8: false)
56
+
57
+ # Automate GRPC currently has a message limit of ~4MB
58
+ # https://github.com/chef/automate/issues/1417#issuecomment-541908157
59
+ if json_report.bytesize > 4 * 1024 * 1024
60
+ Chef::Log.warn "Generated report size is #{(json_report.bytesize / (1024 * 1024.0)).round(2)} MB. #{ChefUtils::Dist::Automate::PRODUCT} has an internal 4MB limit that is not currently configurable."
61
+ end
62
+
63
+ unless json_report
64
+ Chef::Log.warn "Something went wrong, report can't be nil"
65
+ return false
66
+ end
67
+
68
+ begin
69
+ Chef::Log.info "Report to #{ChefUtils::Dist::Automate::PRODUCT}: #{@url}"
70
+ Chef::Log.debug "Compliance Report: #{json_report}"
71
+ http_client.post(nil, json_report, headers)
72
+ true
73
+ rescue => e
74
+ Chef::Log.error "send_report: POST to #{@url} returned: #{e.message}"
75
+ false
76
+ end
77
+ end
78
+
79
+ def http_client(url = @url)
80
+ if @insecure
81
+ Chef::HTTP.new(url, ssl_verify_mode: :verify_none)
82
+ else
83
+ Chef::HTTP.new(url)
84
+ end
85
+ end
86
+
87
+ def enriched_report(final_report)
88
+ final_report[:profiles].compact!
89
+
90
+ # Label this content as an inspec_report
91
+ final_report[:type] = "inspec_report"
92
+
93
+ final_report[:node_name] = @node_name
94
+ final_report[:end_time] = @timestamp.utc.strftime("%FT%TZ")
95
+ final_report[:node_uuid] = @entity_uuid
96
+ final_report[:environment] = @environment
97
+ final_report[:roles] = @roles
98
+ final_report[:recipes] = @recipes
99
+ final_report[:report_uuid] = @run_id
100
+ final_report[:source_fqdn] = @source_fqdn
101
+ final_report[:organization_name] = @organization_name
102
+ final_report[:policy_group] = @policy_group
103
+ final_report[:policy_name] = @policy_name
104
+ final_report[:chef_tags] = @chef_tags
105
+ final_report[:ipaddress] = @ipaddress
106
+ final_report[:fqdn] = @fqdn
107
+
108
+ final_report
109
+ end
110
+
111
+ CONTROL_RESULT_SORT_ORDER = %w{ failed skipped passed }.freeze
112
+
113
+ # Truncates the number of results per control in the report when they exceed max_results.
114
+ # The truncation prioritizes failed and skipped results over passed ones.
115
+ # Controls where results have been truncated will get a new object 'removed_results_counts'
116
+ # with the status counts of the truncated results
117
+ def truncate_controls_results(report, max_results)
118
+ return report unless max_results.is_a?(Integer) && max_results > 0
119
+
120
+ report.fetch(:profiles, []).each do |profile|
121
+ profile.fetch(:controls, []).each do |control|
122
+ # Only bother with truncation if the number of results exceed max_results
123
+ next unless control[:results].length > max_results
124
+
125
+ res = control[:results]
126
+ res.sort_by! { |r| CONTROL_RESULT_SORT_ORDER.index(r[:status]) }
127
+
128
+ # Count the results that will be truncated
129
+ truncated = { failed: 0, skipped: 0, passed: 0 }
130
+ (max_results..res.length - 1).each do |i|
131
+ case res[i][:status]
132
+ when "failed"
133
+ truncated[:failed] += 1
134
+ when "skipped"
135
+ truncated[:skipped] += 1
136
+ when "passed"
137
+ truncated[:passed] += 1
138
+ end
139
+ end
140
+ # Truncate the results array now
141
+ control[:results] = res[0..max_results - 1]
142
+ control[:removed_results_counts] = truncated
143
+ end
144
+ end
145
+ report
146
+ end
147
+
148
+ # Contacts the metasearch Automate API to check which of the inspec profile sha256 ids
149
+ # passed in via `report_shas` are missing from the Automate profiles metadata database.
150
+ def missing_automate_profiles(headers, report_shas)
151
+ Chef::Log.debug "Checking the #{ChefUtils::Dist::Automate::PRODUCT} profiles metadata for: #{report_shas}"
152
+ meta_url = URI(@url)
153
+ meta_url.path = "/compliance/profiles/metasearch"
154
+ response_str = http_client(meta_url.to_s).post(nil, "{\"sha256\": #{report_shas}}", headers)
155
+ missing_shas = Chef::JSONCompat.parse(response_str)["missing_sha256"]
156
+ unless missing_shas.empty?
157
+ Chef::Log.info "#{ChefUtils::Dist::Automate::PRODUCT} is missing metadata for the following profile ids: #{missing_shas}"
158
+ end
159
+ missing_shas
160
+ rescue => e
161
+ Chef::Log.error "missing_automate_profiles error: #{e.message}"
162
+ # If we get an error it's safer to assume none of the profile shas exist in Automate
163
+ report_shas
164
+ end
165
+
166
+ # Profile 'name' is a required property.
167
+ # By not sending it in the report, we make it clear to the ingestion backend that the profile metadata has been stripped from this profile in the report.
168
+ # Profile 'title' and 'version' are still kept for troubleshooting purposes in the backend.
169
+ SEEN_PROFILE_UNNECESSARY_FIELDS = %i{ copyright copyright_email groups license maintainer name summary supports}.freeze
170
+
171
+ SEEN_PROFILE_UNNECESSARY_CONTROL_FIELDS = %i{ code desc descriptions impact refs source_location tags title }.freeze
172
+
173
+ # TODO: This mutates the report and probably doesn't need to.
174
+ def strip_profiles_meta(report, missing_report_shas, run_time_limit)
175
+ report[:profiles].each do |p|
176
+ next if missing_report_shas.include?(p[:sha256])
177
+
178
+ p.delete_if { |f| SEEN_PROFILE_UNNECESSARY_FIELDS.include?(f) }
179
+
180
+ next unless p[:controls].is_a?(Array)
181
+
182
+ p[:controls].each do |c|
183
+ c.delete_if { |f| SEEN_PROFILE_UNNECESSARY_CONTROL_FIELDS.include?(f) }
184
+ c.delete(:waiver_data) if c[:waiver_data] == {}
185
+
186
+ next unless c[:results].is_a?(Array)
187
+
188
+ c[:results].each do |r|
189
+ if r[:run_time].is_a?(Float) && r[:run_time] < run_time_limit
190
+ r.delete(:start_time)
191
+ r.delete(:run_time)
192
+ end
193
+ end
194
+ end
195
+ end
196
+ report[:run_time_limit] = run_time_limit
197
+ report
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end