diff --git a/.eslintrc b/.eslintrc index a632094ee..10e128828 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,6 +14,7 @@ "advisor", "ansible", "camelcase", + "cdn", "checkbox", "csrf", "dropdown", @@ -22,6 +23,7 @@ "href", "ips", "ipv4", + "dropdown", "jed", "katello", "knowledgebase", @@ -30,15 +32,15 @@ "nowrap", "pid", "redhat", + "redux", "remediate", "remediations", "repo", "rhc", + "scalprum", "theforeman", "tooltip", - "unmount", - "redux", - "dropdown" + "unmount" ], "minLength": 3 } diff --git a/README.md b/README.md index d7a1136a4..67659997b 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ [](https://github.com/theforeman/foreman_rh_cloud/actions/workflows/ruby_tests.yml) [](https://github.com/theforeman/foreman_rh_cloud/actions/workflows/js_tests.yml) +[](https://deepwiki.com/theforeman/foreman_rh_cloud) # ForemanRhCloud -*Introduction here* - ## Installation See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin) for how to install Foreman plugins -## Usage - -*Usage here* +## Project overview +See our [wiki](https://deepwiki.com/theforeman/foreman_rh_cloud) ### In Satellite diff --git a/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb b/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb new file mode 100644 index 000000000..2acfc690b --- /dev/null +++ b/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb @@ -0,0 +1,33 @@ +module InsightsCloud + module PackageProfileUploadExtensions + extend ActiveSupport::Concern + + included do + # This method explicitly listens on Katello actions + # rubocop:disable Rails/LexicallyScopedActionFilter + after_action :generate_host_report, only: [:upload_package_profile, :upload_profiles] + # rubocop:enable Rails/LexicallyScopedActionFilter + end + + def generate_host_report + return unless ForemanRhCloud.with_local_advisor_engine? + + logger.debug("Generating host-specific report for host #{@host.name}") + + ForemanTasks.async_task( + ForemanInventoryUpload::Async::GenerateReportJob, + ForemanInventoryUpload.generated_reports_folder, + @host.organization_id, + false, + "id=#{@host.id}" + ) + + # in IoP case, the hosts are identified by the sub-man ID, and we can assume they already + # exist in the local inventory. This will also handle facet creation for new hosts. + return if @host.insights + + insights_facet = @host.build_insights(uuid: @host.subscription_facet.uuid) + insights_facet.save + end + end +end diff --git a/app/controllers/insights_cloud/api/machine_telemetries_controller.rb b/app/controllers/insights_cloud/api/machine_telemetries_controller.rb index c73014f4f..6612c4a8b 100644 --- a/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +++ b/app/controllers/insights_cloud/api/machine_telemetries_controller.rb @@ -17,7 +17,7 @@ class MachineTelemetriesController < ::Api::V2::BaseController def forward_request certs = candlepin_id_cert @organization begin - @cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs) + @cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs, @host) rescue RestClient::Exceptions::Timeout => e response_obj = e.response.presence || e.exception return render json: { message: response_obj.to_s, error: response_obj.to_s }, status: :gateway_timeout diff --git a/app/services/foreman_rh_cloud/cert_auth.rb b/app/services/foreman_rh_cloud/cert_auth.rb index 59bdc67d1..55db61858 100644 --- a/app/services/foreman_rh_cloud/cert_auth.rb +++ b/app/services/foreman_rh_cloud/cert_auth.rb @@ -10,7 +10,8 @@ def cert_auth_available?(organization) end def execute_cloud_request(params) - certs = candlepin_id_cert(params.delete(:organization)) + organization = params.delete(:organization) + certs = ForemanRhCloud.with_local_advisor_engine? ? foreman_certificate : candlepin_id_cert(organization) final_params = { ssl_client_cert: OpenSSL::X509::Certificate.new(certs[:cert]), ssl_client_key: OpenSSL::PKey.read(certs[:key]), @@ -18,5 +19,12 @@ def execute_cloud_request(params) super(final_params) end + + def foreman_certificate + @foreman_certificate ||= { + cert: File.read(Setting[:ssl_certificate]), + key: File.read(Setting[:ssl_priv_key]), + } + end end end diff --git a/app/services/foreman_rh_cloud/cloud_request_forwarder.rb b/app/services/foreman_rh_cloud/cloud_request_forwarder.rb index 4ba84993b..aa697c837 100644 --- a/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +++ b/app/services/foreman_rh_cloud/cloud_request_forwarder.rb @@ -4,7 +4,7 @@ module ForemanRhCloud class CloudRequestForwarder include ForemanRhCloud::CloudRequest - def forward_request(original_request, controller_name, branch_id, certs) + def forward_request(original_request, controller_name, branch_id, certs, host) forward_params = prepare_forward_params(original_request, branch_id) logger.debug("Request parameters for telemetry request: #{forward_params}") @@ -12,14 +12,14 @@ def forward_request(original_request, controller_name, branch_id, certs) logger.debug("User agent for telemetry is: #{http_user_agent original_request}") - request_opts = prepare_request_opts(original_request, forward_payload, forward_params, certs) + request_opts = prepare_request_opts(original_request, forward_payload, forward_params, certs, host) logger.debug("Sending request to: #{request_opts[:url]}") execute_cloud_request(request_opts) end - def prepare_request_opts(original_request, forward_payload, forward_params, certs) + def prepare_request_opts(original_request, forward_payload, forward_params, certs, host) base_params = { method: original_request.method, payload: forward_payload, @@ -28,6 +28,7 @@ def prepare_request_opts(original_request, forward_payload, forward_params, cert params: forward_params, user_agent: http_user_agent(original_request), content_type: original_request.media_type.presence || original_request.format.to_s, + Forwarded: prepare_forwarded_header(host), } ), } @@ -105,6 +106,10 @@ def original_headers(original_request) headers end + def prepare_forwarded_header(host) + "for=\"_#{host.subscription_facet.uuid}\"" + end + def lightspeed? ->(request_path) { request_path.include? '/lightspeed' } end diff --git a/lib/foreman_inventory_upload.rb b/lib/foreman_inventory_upload.rb index cde9f5cf0..ce345adc6 100644 --- a/lib/foreman_inventory_upload.rb +++ b/lib/foreman_inventory_upload.rb @@ -52,8 +52,8 @@ def self.upload_script_file 'uploader.sh' end - def self.facts_archive_name(organization) - "report_for_#{organization}.tar.xz" + def self.facts_archive_name(organization, filter = nil) + "report_for_#{organization}#{filter.empty? ? nil : "[#{filter.to_s.parameterize}]"}.tar.xz" end def self.upload_url diff --git a/lib/foreman_inventory_upload/async/generate_report_job.rb b/lib/foreman_inventory_upload/async/generate_report_job.rb index 89dd0e32c..7eb5f4544 100644 --- a/lib/foreman_inventory_upload/async/generate_report_job.rb +++ b/lib/foreman_inventory_upload/async/generate_report_job.rb @@ -5,18 +5,19 @@ def self.output_label(label) "report_for_#{label}" end - def plan(base_folder, organization_id, disconnected) + def plan(base_folder, organization_id, disconnected, hosts_filter = nil) sequence do super( - GenerateReportJob.output_label(organization_id), + GenerateReportJob.output_label("#{organization_id}#{hosts_filter.empty? ? nil : "[#{hosts_filter.to_s.parameterize}]"}"), organization_id: organization_id, - base_folder: base_folder + base_folder: base_folder, + hosts_filter: hosts_filter ) plan_action( QueueForUploadJob, base_folder, - ForemanInventoryUpload.facts_archive_name(organization_id), + ForemanInventoryUpload.facts_archive_name(organization_id, hosts_filter), organization_id, disconnected ) @@ -34,7 +35,8 @@ def command def env super.merge( 'target' => base_folder, - 'organization_id' => organization_id + 'organization_id' => organization_id, + 'hosts_filter' => hosts_filter ) end @@ -45,6 +47,10 @@ def base_folder def organization_id input[:organization_id] end + + def hosts_filter + input[:hosts_filter] + end end end end diff --git a/lib/foreman_inventory_upload/async/upload_report_job.rb b/lib/foreman_inventory_upload/async/upload_report_job.rb index f45ea2560..48fd3f7f9 100644 --- a/lib/foreman_inventory_upload/async/upload_report_job.rb +++ b/lib/foreman_inventory_upload/async/upload_report_job.rb @@ -33,8 +33,8 @@ def try_execute end Tempfile.create([organization.name, '.pem']) do |cer_file| - cer_file.write(rh_credentials[:cert]) - cer_file.write(rh_credentials[:key]) + cer_file.write(certificate[:cert]) + cer_file.write(certificate[:key]) cer_file.flush @cer_path = cer_file.path super @@ -59,8 +59,12 @@ def env env_vars end - def rh_credentials - @rh_credentials ||= begin + def certificate + ForemanRhCloud.with_local_advisor_engine? ? foreman_certificate : manifest_certificate + end + + def manifest_certificate + @manifest_certificate ||= begin candlepin_id_certificate = organization.owner_details['upstreamConsumer']['idCert'] { cert: candlepin_id_certificate['cert'], @@ -69,6 +73,13 @@ def rh_credentials end end + def foreman_certificate + @foreman_certificate ||= { + cert: File.read(Setting[:ssl_certificate]), + key: File.read(Setting[:ssl_priv_key]), + } + end + def filename input[:filename] end diff --git a/lib/foreman_inventory_upload/generators/archived_report.rb b/lib/foreman_inventory_upload/generators/archived_report.rb index cce883909..ff93743b3 100644 --- a/lib/foreman_inventory_upload/generators/archived_report.rb +++ b/lib/foreman_inventory_upload/generators/archived_report.rb @@ -6,10 +6,10 @@ def initialize(target, logger = Rails.logger) @logger = logger end - def render(organization:) + def render(organization:, filter: nil) Dir.mktmpdir do |tmpdir| @logger.info "Started generating hosts report in #{tmpdir}" - host_batches = ForemanInventoryUpload::Generators::Queries.for_org(organization) + host_batches = ForemanInventoryUpload::Generators::Queries.for_org(organization, hosts_query: filter || '') File.open(File.join(tmpdir, 'metadata.json'), 'w') do |metadata_out| metadata_generator = ForemanInventoryUpload::Generators::Metadata.new(metadata_out) metadata_generator.render do |inner_generator| diff --git a/lib/foreman_inventory_upload/generators/fact_helpers.rb b/lib/foreman_inventory_upload/generators/fact_helpers.rb index 8f3db4094..0fb51d9b8 100644 --- a/lib/foreman_inventory_upload/generators/fact_helpers.rb +++ b/lib/foreman_inventory_upload/generators/fact_helpers.rb @@ -57,17 +57,39 @@ def cloud_provider(host) end def obfuscate_hostname?(host) + # Returns true if hostname obfuscation should be applied for a given host, based on hierarchy: + # 1. Global setting for hostname obfuscation. + return true if Setting[:obfuscate_inventory_hostnames] + insights_client_setting = fact_value(host, 'insights_client::obfuscate_hostname_enabled') insights_client_setting = ActiveModel::Type::Boolean.new.cast(insights_client_setting) - return insights_client_setting unless insights_client_setting.nil? - Setting[:obfuscate_inventory_hostnames] + # 2. host fact reported by insights_client + # 3. if neither of the above, don't obfuscate. + insights_client_setting.nil? ? false : insights_client_setting end def fqdn(host) - return host.fqdn unless obfuscate_hostname?(host) - - fact_value(host, 'insights_client::hostname') || obfuscate_fqdn(host.fqdn) + if obfuscate_hostname?(host) + # If obfuscation is enabled, attempt to retrieve an already obfuscated hostname + # from the 'insights_client::obfuscated_hostname' fact. + # Example format of `parsed_insights_array`: + # [{"original"=>"host.example.com", "obfuscated"=>"0dd449d0a027.example.com"}, + # {"original"=>"satellite.example.com", "obfuscated"=>"host2.example.com"}] + begin + parsed_insights_array = JSON.parse(fact_value(host, 'insights_client::obfuscated_hostname') || '[]') + rescue JSON::ParserError + parsed_insights_array = [] + end + # Obfuscate using the following hierarchy: + # 1. the obfuscated_hostname fact sent by insights_client + parsed_insights_item = parsed_insights_array.find { |item| item['original'] == host.fqdn } + # 2. our own helper method + parsed_insights_item&.[]('obfuscated') || obfuscate_fqdn(host.fqdn) + else + # If hostname obfuscation is not enabled for this host, return the host's original FQDN. + host.fqdn + end end def obfuscate_fqdn(fqdn) @@ -75,35 +97,65 @@ def obfuscate_fqdn(fqdn) end def obfuscate_ips?(host) - insights_client_setting = fact_value(host, 'insights_client::obfuscate_ip_enabled') - insights_client_setting = ActiveModel::Type::Boolean.new.cast(insights_client_setting) - return insights_client_setting unless insights_client_setting.nil? + # Returns true if IP obfuscation should be applied for a given host, based on hierarchy: + # 1. Global setting for IP obfuscation. + return true if Setting[:obfuscate_inventory_ips] - Setting[:obfuscate_inventory_ips] + insights_client_ipv4_setting = fact_value(host, 'insights_client::obfuscate_ipv4_enabled') + insights_client_ipv6_setting = fact_value(host, 'insights_client::obfuscate_ipv6_enabled') + + cast_ipv4_setting = ActiveModel::Type::Boolean.new.cast(insights_client_ipv4_setting) + cast_ipv6_setting = ActiveModel::Type::Boolean.new.cast(insights_client_ipv6_setting) + + # 2. The host's IPv4 or IPv6 obfuscation fact value is true + # 3. If neither of the above, don't obfuscate. + cast_ipv4_setting || cast_ipv6_setting || false end def host_ips(host) + # Determines and returns the IP addresses associated with a host, applying obfuscation if enabled. + + # If IP obfuscation is enabled for the host return a representation of obfuscated IP addresses. return obfuscated_ips(host) if obfuscate_ips?(host) - # return a pass through proxy hash in case no obfuscation needed + # If IP obfuscation is NOT needed, return a special kind of Hash. + # where when you try to access a key in it + # if the key doesn't exist, it simply returns the key itself. + # This is useful because it means if you try to get an IP from this hash, + # you'll just get the original IP back. It allows the calling code to + # use the same interface whether obfuscation is applied or not. Hash.new { |h, k| k } end def obfuscated_ips(host) - insights_client_ips = JSON.parse(fact_value(host, 'insights_client::ips') || '[]') + # Example format of `parsed_insights_array`: + # [{"original": "192.168.1.10", "obfuscated": "10.230.230.1"}, + # {"original": "192.168.1.11", "obfuscated": "10.230.230.2"}] + begin + parsed_insights_array = JSON.parse(fact_value(host, 'insights_client::obfuscated_ipv4') || '[]') + rescue JSON::ParserError + parsed_insights_array = [] + end + # Create a new Hash to store the mapping from original IP addresses to their obfuscated versions. + # where the 'original' IP is the key and the 'obfuscated' IP is the value. obfuscated_ips = Hash[ - insights_client_ips.map { |ip_record| [ip_record['original'], ip_record['obfuscated']] } + parsed_insights_array.map { |ip_record| [ip_record['original'], ip_record['obfuscated']] } ] + # Sets a default proc for the obfuscated_ips hash. + # When a key is accessed that does not exist in the hash, this proc is called. + # It assigns the result of obfuscate_ip(key, hash) to the missing key in the hash. + # This ensures that any missing IP address key will be obfuscated and stored automatically. obfuscated_ips.default_proc = proc do |hash, key| hash[key] = obfuscate_ip(key, hash) end - obfuscated_ips end def obfuscate_ip(ip, ips_dict) + # Produce a new, unique obfuscated IP that is + # numerically one greater than the highest existing obfuscated IP max_obfuscated = ips_dict.values.map { |v| IPAddr.new(v).to_i }.max || IPAddr.new('10.230.230.0').to_i IPAddr.new(max_obfuscated + 1, Socket::AF_INET).to_s diff --git a/lib/foreman_inventory_upload/generators/queries.rb b/lib/foreman_inventory_upload/generators/queries.rb index 2cd052b06..d274b7b83 100644 --- a/lib/foreman_inventory_upload/generators/queries.rb +++ b/lib/foreman_inventory_upload/generators/queries.rb @@ -26,9 +26,11 @@ def self.fact_names 'dmi::system::product_name', 'dmi::chassis::asset_tag', 'insights_client::obfuscate_hostname_enabled', - 'insights_client::obfuscate_ip_enabled', - 'insights_client::hostname', - 'insights_client::ips', + 'insights_client::obfuscate_ipv4_enabled', + 'insights_client::obfuscate_ipv6_enabled', + 'insights_client::obfuscated_ipv4', + 'insights_client::obfuscated_ipv6', + 'insights_client::obfuscated_hostname', 'insights_id', 'conversions::activity', 'conversions::packages::0::nevra', @@ -58,8 +60,8 @@ def self.for_slice(base) ) end - def self.for_org(organization_id, use_batches: true) - base_query = for_slice(Host.unscoped.where(organization_id: organization_id)) + def self.for_org(organization_id, use_batches: true, hosts_query: '') + base_query = for_slice(Host.unscoped.where(organization_id: organization_id).search_for(hosts_query)) use_batches ? base_query.in_batches(of: ForemanInventoryUpload.slice_size) : base_query end end diff --git a/lib/foreman_inventory_upload/generators/slice.rb b/lib/foreman_inventory_upload/generators/slice.rb index d5dda7bad..63fc7b962 100644 --- a/lib/foreman_inventory_upload/generators/slice.rb +++ b/lib/foreman_inventory_upload/generators/slice.rb @@ -190,7 +190,6 @@ def report_system_profile(host, host_ips_cache) ) { |v| os_release_value(*v) } @stream.simple_field('os_kernel_version', fact_value(host, 'uname::release')) @stream.simple_field('arch', host.architecture&.name) - @stream.simple_field('katello_agent_running', false) @stream.simple_field( 'infrastructure_type', ActiveModel::Type::Boolean.new.cast(fact_value(host, 'virt::is_guest')) ? 'virtual' : 'physical' diff --git a/lib/foreman_rh_cloud/engine.rb b/lib/foreman_rh_cloud/engine.rb index f20269c9a..a34d94e6a 100644 --- a/lib/foreman_rh_cloud/engine.rb +++ b/lib/foreman_rh_cloud/engine.rb @@ -114,8 +114,7 @@ def self.register_scheduled_task(task_class, cronline) caption: N_('Inventory Upload'), url: '/foreman_rh_cloud/inventory_upload', url_hash: { controller: :react, action: :index }, - parent: :insights_menu, - if: -> { !ForemanRhCloud.with_local_advisor_engine? } + parent: :insights_menu menu :top_menu, :insights_hits, caption: N_('Recommendations'), url: '/foreman_rh_cloud/insights_cloud', url_hash: { controller: :react, action: :index }, parent: :insights_menu menu :top_menu, :insights_vulnerability, @@ -165,6 +164,8 @@ def self.register_scheduled_task(task_class, cronline) ::Katello::UINotifications::Subscriptions::ManifestImportSuccess.include ForemanInventoryUpload::Notifications::ManifestImportSuccessNotificationOverride if defined?(Katello) ::Host::Managed.include RhCloudHost + + ::Katello::Api::Rhsm::CandlepinDynflowProxyController.include InsightsCloud::PackageProfileUploadExtensions end end diff --git a/lib/foreman_rh_cloud/version.rb b/lib/foreman_rh_cloud/version.rb index 9d9c6cf39..4c2af550e 100644 --- a/lib/foreman_rh_cloud/version.rb +++ b/lib/foreman_rh_cloud/version.rb @@ -1,3 +1,3 @@ module ForemanRhCloud - VERSION = '12.1.2'.freeze + VERSION = '12.1.3'.freeze end diff --git a/lib/inventory_sync/async/inventory_hosts_sync.rb b/lib/inventory_sync/async/inventory_hosts_sync.rb index 224a036d8..950bd7e03 100644 --- a/lib/inventory_sync/async/inventory_hosts_sync.rb +++ b/lib/inventory_sync/async/inventory_hosts_sync.rb @@ -8,6 +8,8 @@ class InventoryHostsSync < QueryInventoryJob set_callback :step, :around, :create_missing_hosts def plan(organizations) + # Do not run for local advisor, since we use sub-man id to identify hosts. + return if ForemanRhCloud.with_local_advisor_engine? # by default the tasks will be executed concurrently super(organizations) plan_self_host_sync diff --git a/lib/tasks/rh_cloud_inventory.rake b/lib/tasks/rh_cloud_inventory.rake index 243cc257c..f2342927c 100644 --- a/lib/tasks/rh_cloud_inventory.rake +++ b/lib/tasks/rh_cloud_inventory.rake @@ -26,6 +26,7 @@ namespace :rh_cloud_inventory do task generate: :environment do organizations = [ENV['organization_id']] base_folder = ENV['target'] || Dir.pwd + filter = ENV['hosts_filter'] unless File.writable?(base_folder) puts "#{base_folder} is not writable by the current process" @@ -40,9 +41,9 @@ namespace :rh_cloud_inventory do User.as_anonymous_admin do organizations.each do |organization| - target = File.join(base_folder, ForemanInventoryUpload.facts_archive_name(organization)) + target = File.join(base_folder, ForemanInventoryUpload.facts_archive_name(organization, filter)) archived_report_generator = ForemanInventoryUpload::Generators::ArchivedReport.new(target, Logger.new(STDOUT)) - archived_report_generator.render(organization: organization) + archived_report_generator.render(organization: organization, filter: filter) puts "Successfully generated #{target} for organization id #{organization}" end end diff --git a/package.json b/package.json index b5ef3226e..7fb551e16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foreman_rh_cloud", - "version": "12.1.2", + "version": "12.1.3", "description": "Inventory Upload =============", "main": "index.js", "scripts": { @@ -21,6 +21,10 @@ "peerDependencies": { "@theforeman/vendor": ">= 15.0.1" }, + "dependencies": { + "@scalprum/react-core": "^0.9.3", + "@scalprum/core": "^0.8.1" + }, "devDependencies": { "@babel/core": "^7.7.0", "@theforeman/builder": ">= 15.0.1", diff --git a/test/unit/archived_report_generator_test.rb b/test/unit/archived_report_generator_test.rb index 4bdde5791..104a4085d 100644 --- a/test/unit/archived_report_generator_test.rb +++ b/test/unit/archived_report_generator_test.rb @@ -50,7 +50,7 @@ def fact_names batches = Host.where(id: @host.id).in_batches test_org = FactoryBot.create(:organization) - ForemanInventoryUpload::Generators::Queries.expects(:for_org).with(test_org.id).returns(batches) + ForemanInventoryUpload::Generators::Queries.expects(:for_org).with(test_org.id, hosts_query: '').returns(batches) ForemanInventoryUpload::Generators::Slice.any_instance.stubs(:golden_ticket?).returns(false) Dir.mktmpdir do |tmpdir| target = File.join(tmpdir, 'test.tar.gz') diff --git a/test/unit/fact_helpers_test.rb b/test/unit/fact_helpers_test.rb index 692f6f3da..983372139 100644 --- a/test/unit/fact_helpers_test.rb +++ b/test/unit/fact_helpers_test.rb @@ -1,4 +1,5 @@ require 'test_plugin_helper' +require 'digest' class FactHelpersTest < ActiveSupport::TestCase class FactsHelpersTestStub @@ -29,7 +30,7 @@ class FactsHelpersTestStub test 'obfuscates ips with insights-client data' do host = mock('host') - @instance.expects(:fact_value).with(host, 'insights_client::ips').returns( + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns( '[{"obfuscated": "10.230.230.1", "original": "224.0.0.1"}, {"obfuscated": "10.230.230.255", "original": "224.0.0.251"}]' ) @@ -41,11 +42,275 @@ class FactsHelpersTestStub test 'obfuscates ips without insights-client data' do host = mock('host') - @instance.expects(:fact_value).with(host, 'insights_client::ips').returns(nil) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns(nil) actual = @instance.obfuscated_ips(host) assert_equal '10.230.230.1', actual['224.0.0.1'] assert_equal '10.230.230.2', actual['224.0.0.2'] end + + describe 'obfuscate_hostname?' do + test 'returns true when global setting is enabled' do + Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(true) + host = mock('host') + + result = @instance.obfuscate_hostname?(host) + + assert result + end + + test 'returns false when global setting is disabled and no host-specific setting' do + Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns(nil) + + result = @instance.obfuscate_hostname?(host) + + refute result + end + + test 'returns true when host-specific setting is enabled' do + Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns('true') + + result = @instance.obfuscate_hostname?(host) + + assert result + end + + test 'returns false when host-specific setting is disabled' do + Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns('false') + + result = @instance.obfuscate_hostname?(host) + + refute result + end + end + + describe 'fqdn' do + test 'returns original fqdn when obfuscation is disabled' do + host = mock('host') + host.expects(:fqdn).returns('test.example.com') + @instance.expects(:obfuscate_hostname?).with(host).returns(false) + + result = @instance.fqdn(host) + + assert_equal 'test.example.com', result + end + + test 'returns obfuscated hostname from insights_client fact when available' do + host = mock('host') + host.expects(:fqdn).returns('test.example.com').once + @instance.expects(:obfuscate_hostname?).with(host).returns(true) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns( + '[{"original": "test.example.com", "obfuscated": "abc123.example.com"}]' + ) + + result = @instance.fqdn(host) + + assert_equal 'abc123.example.com', result + end + + test 'returns dynamically obfuscated hostname when insights_client fact is not available' do + host = mock('host') + host.stubs(:fqdn).returns('test.example.com') + @instance.expects(:obfuscate_hostname?).with(host).returns(true) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns(nil) + + result = @instance.fqdn(host) + + expected = "#{Digest::SHA1.hexdigest('test.example.com')}.example.com" + assert_equal expected, result + end + + test 'returns dynamically obfuscated hostname when insights_client fact does not contain matching host' do + host = mock('host') + host.expects(:fqdn).returns('test.example.com').twice + @instance.expects(:obfuscate_hostname?).with(host).returns(true) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns( + '[{"original": "other.example.com", "obfuscated": "abc123.example.com"}]' + ) + @instance.expects(:obfuscate_fqdn).with('test.example.com').returns('dynamically_obfuscated.example.com') + + result = @instance.fqdn(host) + + assert_equal 'dynamically_obfuscated.example.com', result + end + + test 'handles invalid JSON in insights_client fact gracefully' do + host = mock('host') + host.stubs(:fqdn).returns('test.example.com') + @instance.expects(:obfuscate_hostname?).with(host).returns(true) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns('invalid json') + + result = @instance.fqdn(host) + + expected = "#{Digest::SHA1.hexdigest('test.example.com')}.example.com" + assert_equal expected, result + end + + test 'handles empty insights_client fact' do + host = mock('host') + host.stubs(:fqdn).returns('test.example.com') + @instance.expects(:obfuscate_hostname?).with(host).returns(true) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns('[]') + + result = @instance.fqdn(host) + + expected = "#{Digest::SHA1.hexdigest('test.example.com')}.example.com" + assert_equal expected, result + end + end + + describe 'obfuscate_ips?' do + test 'returns true when global setting is enabled' do + Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(true) + host = mock('host') + + result = @instance.obfuscate_ips?(host) + + assert result + end + + test 'returns false when global setting is disabled and no host-specific settings' do + Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns(nil) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns(nil) + + result = @instance.obfuscate_ips?(host) + + refute result + end + + test 'returns true when host-specific IPv4 setting is enabled' do + Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns('true') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns(nil) + + result = @instance.obfuscate_ips?(host) + + assert result + end + + test 'returns true when host-specific IPv6 setting is enabled' do + Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns(nil) + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns('true') + + result = @instance.obfuscate_ips?(host) + + assert result + end + + test 'returns true when both IPv4 and IPv6 settings are enabled' do + Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns('true') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns('true') + + result = @instance.obfuscate_ips?(host) + + assert result + end + + test 'returns false when both IPv4 and IPv6 settings are disabled' do + Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false) + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns('false') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns('false') + + result = @instance.obfuscate_ips?(host) + + refute result + end + end + + describe 'obfuscate_ip' do + test 'generates first IP when no existing obfuscated IPs' do + ips_dict = {} + + result = @instance.obfuscate_ip('192.168.1.1', ips_dict) + + assert_equal '10.230.230.1', result + end + + test 'generates next sequential IP when existing obfuscated IPs present' do + ips_dict = { '192.168.1.1' => '10.230.230.5', '192.168.1.2' => '10.230.230.10' } + + result = @instance.obfuscate_ip('192.168.1.3', ips_dict) + + assert_equal '10.230.230.11', result + end + + test 'handles mixed IP ranges correctly' do + ips_dict = { '192.168.1.1' => '10.230.230.255', '192.168.1.2' => '10.230.230.1' } + + result = @instance.obfuscate_ip('192.168.1.3', ips_dict) + + assert_equal '10.230.231.0', result + end + + test 'generates valid IP addresses' do + ips_dict = {} + + result = @instance.obfuscate_ip('any.ip.address', ips_dict) + + assert_match(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/, result) + assert_nothing_raised { IPAddr.new(result) } + end + end + + describe 'obfuscated_ips' do + test 'handles invalid JSON in insights_client fact gracefully' do + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns('invalid json') + + result = @instance.obfuscated_ips(host) + + assert_equal '10.230.230.1', result['192.168.1.1'] + end + + test 'handles empty insights_client fact' do + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns('[]') + + result = @instance.obfuscated_ips(host) + + assert_equal '10.230.230.1', result['192.168.1.1'] + end + + test 'preserves existing obfuscated IPs and generates new ones' do + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns( + '[{"original": "192.168.1.1", "obfuscated": "10.230.230.5"}]' + ) + + result = @instance.obfuscated_ips(host) + + assert_equal '10.230.230.5', result['192.168.1.1'] + assert_equal '10.230.230.6', result['192.168.1.2'] + end + + test 'default_proc generates unique sequential IPs' do + host = mock('host') + @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns(nil) + + result = @instance.obfuscated_ips(host) + + ip1 = result['192.168.1.1'] + ip2 = result['192.168.1.2'] + ip3 = result['192.168.1.3'] + + assert_equal '10.230.230.1', ip1 + assert_equal '10.230.230.2', ip2 + assert_equal '10.230.230.3', ip3 + end + end end diff --git a/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb b/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb index 57ea9c430..a62cf32e8 100644 --- a/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +++ b/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb @@ -3,6 +3,7 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase include MockCerts + include KatelloCVEHelper setup do @forwarder = ::ForemanRhCloud::CloudRequestForwarder.new @@ -10,6 +11,22 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase ForemanRhCloud.stubs(:base_url).returns('https://cloud.example.com') ForemanRhCloud.stubs(:cert_base_url).returns('https://cert.cloud.example.com') ForemanRhCloud.stubs(:legacy_insights_url).returns('https://cert-api.access.example.com') + + UpstreamOnlySettingsTestHelper.set_if_available('allow_multiple_content_views') + env = FactoryBot.create(:katello_k_t_environment) + env2 = FactoryBot.create(:katello_k_t_environment, organization: env.organization) + + @host = FactoryBot.create( + :host, + :with_subscription, + :with_content, + :with_hostgroup, + :with_parameter, + content_view_environments: [make_cve(lifecycle_environment: env), make_cve(lifecycle_environment: env2)], + organization: env.organization + ) + + @host.subscription_facet.pools << FactoryBot.create(:katello_pool, account_number: '5678', cp_id: 1) end test 'should prepare correct cloud url' do @@ -150,7 +167,7 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase 'action_dispatch.request.query_parameters' => params ) - actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash) + actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash, @host) assert_match /foo/, actual[:headers][:user_agent] assert_match /bar/, actual[:headers][:user_agent] @@ -175,7 +192,7 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase 'action_dispatch.request.query_parameters' => params ) - actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash) + actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash, @host) assert_match /text\/html/, actual[:headers][:content_type] end diff --git a/test/unit/slice_generator_test.rb b/test/unit/slice_generator_test.rb index 3030f5bd5..f05efb78b 100644 --- a/test/unit/slice_generator_test.rb +++ b/test/unit/slice_generator_test.rb @@ -64,9 +64,10 @@ def interesting_facts 'dmi::system::product_name', 'dmi::chassis::asset_tag', 'insights_client::obfuscate_hostname_enabled', + 'insights_client::obfuscated_hostname', + 'insights_client::obfuscate_ipv4_enabled', + 'insights_client::obfuscated_ipv4', 'insights_client::hostname', - 'insights_client::obfuscate_ip_enabled', - 'insights_client::ips', 'insights_id', ] end @@ -418,14 +419,38 @@ def fact_names assert_equal 1, generator.hosts_count end - test 'generates obfuscated ip_address fields with inisghts-client' do + test 'does not obfuscate fqdn when insights_client obfuscate_hostname_enabled fact is missing and obfuscate_inventory_hostnames setting is false' do + # Create a host and obfuscated_hostname fact, but do NOT create the obfuscate_hostname_enabled fact + obfuscated_hostname_data = [ + { 'original' => @host.fqdn, 'obfuscated' => '0dd449d0a027.example.com' }, + ] + obfuscated_hostname_value = JSON.generate(obfuscated_hostname_data) + FactoryBot.create(:fact_value, + fact_name: fact_names['insights_client::obfuscated_hostname'], + value: obfuscated_hostname_value, + host: @host) + # Do NOT create the 'insights_client::obfuscate_hostname_enabled' fact + + batch = Host.where(id: @host.id).in_batches.first + generator = create_generator(batch) + + json_str = generator.render + actual = JSON.parse(json_str.join("\n")) + + assert_not_nil(actual_host = actual['hosts'].first) + assert_equal @host.fqdn, actual_host['fqdn'], "FQDN should not be obfuscated when obfuscate_hostname_enabled is missing and setting is false" + assert_not_nil(actual_facts = actual_host['facts'].first['facts']) + assert_not_equal true, actual_facts['is_hostname_obfuscated'] + end + + test 'generates obfuscated ip_address fields when insights-client facts are present' do nic = FactoryBot.build(:nic_managed) @host.interfaces << nic - FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_ip_enabled'], value: 'true', host: @host) + FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_ipv4_enabled'], value: 'true', host: @host) FactoryBot.create( :fact_value, - fact_name: fact_names['insights_client::ips'], + fact_name: fact_names['insights_client::obfuscated_ipv4'], value: "[{\"obfuscated\": \"10.230.230.100\", \"original\": \"#{nic.ip}\"}]", host: @host ) @@ -448,9 +473,17 @@ def fact_names assert_equal 1, generator.hosts_count end - test 'obfuscates fqdn when instructed by insights-client' do + test 'obfuscates fqdn when insights-client facts are present' do + obfuscated_hostname_data = [ + { 'original' => @host.fqdn, 'obfuscated' => '0dd449d0a027.example.com' }, + { 'original' => 'satellite.theforeman.org', 'obfuscated' => 'host2.example.com' }, + ] + obfuscated_hostname_value = JSON.generate(obfuscated_hostname_data) + FactoryBot.create(:fact_value, + fact_name: fact_names['insights_client::obfuscated_hostname'], + value: obfuscated_hostname_value, + host: @host) FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_hostname_enabled'], value: 'true', host: @host) - FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::hostname'], value: 'obfuscated_name', host: @host) batch = Host.where(id: @host.id).in_batches.first generator = create_generator(batch) @@ -460,7 +493,7 @@ def fact_names assert_equal '00000000-0000-0000-0000-000000000000', actual['report_slice_id'] assert_not_nil(actual_host = actual['hosts'].first) - assert_equal 'obfuscated_name', actual_host['fqdn'] + assert_equal obfuscated_hostname_data.first['obfuscated'], actual_host['fqdn'] assert_equal '1234', actual_host['account'] assert_not_nil(actual_facts = actual_host['facts'].first['facts']) assert_equal true, actual_facts['is_hostname_obfuscated'] @@ -487,9 +520,35 @@ def fact_names assert_equal 1, generator.hosts_count end - test 'does not obfuscate fqdn when insights-client sets to false' do + test 'obfuscates host fqdn with insights-client when setting set' do + Setting[:obfuscate_inventory_hostnames] = true + FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::hostname'], value: @host.fqdn, host: @host) + + batch = Host.where(id: @host.id).in_batches.first + generator = create_generator(batch) + + json_str = generator.render + actual = JSON.parse(json_str.join("\n")) + + obfuscated_fqdn = Digest::SHA1.hexdigest(@host.fqdn) + '.example.com' + + assert_equal '00000000-0000-0000-0000-000000000000', actual['report_slice_id'] + assert_not_nil(actual_host = actual['hosts'].first) + assert_equal obfuscated_fqdn, actual_host['fqdn'] + assert_equal '1234', actual_host['account'] + assert_not_nil(actual_facts = actual_host['facts'].first['facts']) + assert_equal true, actual_facts['is_hostname_obfuscated'] + assert_equal 1, generator.hosts_count + end + + test 'does not obfuscate fqdn when host fact from insights-client has a value of false' do + obfuscated_hostname_data = [ + { 'original' => @host.fqdn, 'obfuscated' => '0dd449d0a027.example.com' }, + { 'original' => 'satellite.theforeman.org', 'obfuscated' => 'host2.example.com' }, + ] + obfuscated_hostname_value = JSON.generate(obfuscated_hostname_data) FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_hostname_enabled'], value: 'false', host: @host) - FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::hostname'], value: 'obfuscated_name', host: @host) + FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscated_hostname'], value: obfuscated_hostname_value, host: @host) batch = Host.where(id: @host.id).in_batches.first generator = create_generator(batch) diff --git a/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js b/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js index 3241c5c9f..811acb9fc 100644 --- a/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +++ b/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js @@ -1,17 +1,31 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { translate as __ } from 'foremanReact/common/I18n'; +import PageLayout from 'foremanReact/routes/common/PageLayout/PageLayout'; +import { ScalprumComponent } from '@scalprum/react-core'; +import { ScalprumContextWrapper } from '../common/ScalprumModule/ScalprumContext'; -const CVEsHostDetailsTab = ({ hostName }) => ( -
+ This page is under development. Please check back soon for updates. +
+This page is under development. Please check back soon for updates.
+const InsightsVulnerability = () => { + const scope = 'vulnerability'; + const module = './CveListPage'; + return ( ++ This page is under development. Please check back soon for updates. +
+