foreman_cve_scanner 0.5.0 → 0.6.0

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -5
  3. data/app/controllers/api/v2/cve_scans_controller.rb +170 -1
  4. data/app/models/foreman_cve_scanner/cve_scan.rb +15 -2
  5. data/app/models/host_status/cve_status.rb +3 -1
  6. data/app/services/concerns/foreman_cve_scanner/profiles_uploader.rb +25 -0
  7. data/app/services/foreman_cve_scanner/cve_report_scanner.rb +5 -8
  8. data/app/services/foreman_cve_scanner/scan_cleanup.rb +34 -0
  9. data/app/services/foreman_cve_scanner/scan_comparison.rb +145 -0
  10. data/app/services/foreman_cve_scanner/scan_importer.rb +17 -15
  11. data/app/views/api/v2/cve_scans/base.json.rabl +1 -1
  12. data/app/views/api/v2/cve_scans/main.json.rabl +1 -1
  13. data/app/views/foreman_cve_scanner/job_templates/install_cve_scanners.erb +22 -9
  14. data/app/views/foreman_cve_scanner/job_templates/run_cve_scanner.erb +5 -3
  15. data/config/routes.rb +6 -1
  16. data/db/migrate/20260514080000_add_source_and_scanned_at_to_foreman_cve_scanner_cve_scans.rb +26 -0
  17. data/lib/foreman_cve_scanner/engine.rb +68 -17
  18. data/lib/foreman_cve_scanner/template_helpers.rb +28 -0
  19. data/lib/foreman_cve_scanner/version.rb +1 -1
  20. data/lib/tasks/foreman_cve_scanner_tasks.rake +11 -0
  21. data/package.json +1 -1
  22. data/test/controllers/api/v2/cve_scans_controller_test.rb +260 -5
  23. data/test/lib/foreman_cve_scanner/profiles_uploader_test.rb +84 -0
  24. data/test/lib/foreman_cve_scanner/template_helpers_test.rb +29 -0
  25. data/test/models/foreman_cve_scanner/cve_scan_test.rb +120 -0
  26. data/test/models/host_status/cve_status_test.rb +12 -3
  27. data/test/services/foreman_cve_scanner/scan_cleanup_test.rb +69 -0
  28. data/test/services/foreman_cve_scanner/scan_comparison_test.rb +84 -0
  29. data/test/services/foreman_cve_scanner/scan_importer_test.rb +68 -5
  30. data/webpack/components/CveCompareModal.js +298 -0
  31. data/webpack/components/CveDetailsCard.js +141 -121
  32. data/webpack/components/CveFindingsModal.js +131 -111
  33. data/webpack/components/CveScansReports.js +227 -0
  34. data/webpack/components/CveScansTab.js +122 -119
  35. data/webpack/components/CveTrendChart.js +264 -0
  36. data/webpack/components/__tests__/CveCompareModal.test.js +104 -0
  37. data/webpack/components/__tests__/CveDetailsCard.test.js +106 -20
  38. data/webpack/components/__tests__/CveFindingsModal.test.js +54 -2
  39. data/webpack/components/__tests__/CveScansTab.test.js +185 -5
  40. data/webpack/components/__tests__/CveTrendChart.test.js +122 -0
  41. data/webpack/components/__tests__/cve_helpers.test.js +18 -0
  42. data/webpack/components/cve_helpers.js +139 -0
  43. data/webpack/components/cve_scans.scss +464 -9
  44. data/webpack/components/useModalScan.js +26 -0
  45. metadata +24 -3
  46. data/webpack/components/CveHistoryTable.js +0 -58
  47. data/webpack/components/CveOverviewCard.js +0 -67
@@ -9,7 +9,7 @@ module ForemanCveScanner
9
9
 
10
10
  def import_for_host!(host)
11
11
  scan_json = format_output(@job_output)
12
- return nil if scan_json.nil?
12
+ raise ::Foreman::Exception, _('CVE scan output did not contain JSON content') if scan_json.nil?
13
13
 
14
14
  scanner_name = ::ForemanCveScanner::CveReportScanner.detect_scanner(scan_json)
15
15
  persist_scan!(host, scanner_name, scan_json)
@@ -32,12 +32,14 @@ module ForemanCveScanner
32
32
  end
33
33
 
34
34
  def extract_json(output_source)
35
- output = output_source.to_s.each_line(chomp: true)
36
- .drop_while { |line| !line.start_with?('===START') }
37
- .drop(1)
38
- .take_while { |line| !line.start_with?('===END') }
39
- .reject(&:empty?)
40
- .join
35
+ lines = output_source.to_s.each_line(chomp: true).to_a
36
+ return nil unless lines.include?('===START') && lines.include?('===END')
37
+
38
+ output = lines.drop_while { |line| !line.start_with?('===START') }
39
+ .drop(1)
40
+ .take_while { |line| !line.start_with?('===END') }
41
+ .reject(&:empty?)
42
+ .join
41
43
  output.strip
42
44
  end
43
45
 
@@ -63,6 +65,7 @@ module ForemanCveScanner
63
65
  build_scan_attributes(host, scanner_name, scan_json, metrics, scanner)
64
66
  )
65
67
  refresh_host_status(host)
68
+ cleanup_old_scans(host)
66
69
  scan
67
70
  end
68
71
 
@@ -73,18 +76,13 @@ module ForemanCveScanner
73
76
  Rails.logger.error("CVE status refresh failed for host_id=#{host.id}: #{e}")
74
77
  end
75
78
 
76
- def worst_severity(metrics)
77
- %w[critical high medium low].each do |severity|
78
- return severity if metrics[severity].to_i.positive?
79
- end
80
- 'none'
81
- end
82
-
83
79
  def build_scan_attributes(host, scanner_name, scan_json, metrics, scanner)
84
- summary = metrics.merge('worst' => worst_severity(metrics))
80
+ summary = metrics.merge('worst' => ::ForemanCveScanner::CveScan.worst_severity(metrics))
85
81
  {
86
82
  host: host,
87
83
  scanner: scanner_name,
84
+ source: 'rex',
85
+ scanned_at: Time.current,
88
86
  raw: scan_json,
89
87
  summary: summary,
90
88
  findings: build_findings(scanner),
@@ -101,5 +99,9 @@ module ForemanCveScanner
101
99
  entry.merge('id' => id)
102
100
  end
103
101
  end
102
+
103
+ def cleanup_old_scans(host)
104
+ ::ForemanCveScanner::ScanCleanup.new(scope: host.cve_scans).cleanup!
105
+ end
104
106
  end
105
107
  end
@@ -2,4 +2,4 @@
2
2
 
3
3
  object @cve_scan
4
4
 
5
- attributes :id, :host_id, :scanner, :created_at, :total, :critical, :high, :medium, :low, :summary
5
+ attributes :id, :host_id, :scanner, :source, :scanned_at, :created_at, :total, :critical, :high, :medium, :low, :summary
@@ -4,4 +4,4 @@ object @cve_scan
4
4
 
5
5
  extends 'api/v2/cve_scans/base'
6
6
 
7
- attributes :findings, :created_at, :updated_at
7
+ attributes :findings, :updated_at
@@ -43,13 +43,13 @@ end
43
43
  case @host.architecture.to_s
44
44
  when 'x86_64'
45
45
  trivy_arch = 'Linux-64bit'
46
- grype_arch = 'linux-amd64'
46
+ grype_arch = 'linux_amd64'
47
47
  when 'ppc64le'
48
48
  trivy_arch = 'Linux-PPC64LE'
49
- grype_arch = 'linux-ppc64le'
49
+ grype_arch = 'linux_ppc64le'
50
50
  when 'aarch64'
51
51
  trivy_arch = 'Linux-ARM64'
52
- grype_arch = 'linux-arm64'
52
+ grype_arch = 'linux_arm64'
53
53
  else
54
54
  raise("Architecture '#{@host.architecture}' not supported by template #{template_name}")
55
55
  end
@@ -59,13 +59,26 @@ grype_url = "https://github.com/anchore/grype/releases/download/v#{grype_version
59
59
 
60
60
  case @host.operatingsystem.family
61
61
  when 'Debian'
62
- trivy_install_cmd = "wget -o /tmp/outfile.deb #{trivy_url} && dpkg -i /tmp/outfile.deb; rm -f /tmp/outfile.deb"
63
- grype_install_cmd = "wget -o /tmp/outfile.deb #{grype_url} && dpkg -i /tmp/outfile.deb; rm -f /tmp/outfile.deb"
62
+ trivy_download_cmd = "wget -O /tmp/trivy.deb \"#{trivy_url}\""
63
+ grype_download_cmd = "wget -O /tmp/grype.deb \"#{grype_url}\""
64
+ trivy_install_cmd = "dpkg -i /tmp/trivy.deb"
65
+ grype_install_cmd = "dpkg -i /tmp/grype.deb"
66
+ trivy_cleanup_cmd = "rm -f /tmp/trivy.deb"
67
+ grype_cleanup_cmd = "rm -f /tmp/grype.deb"
64
68
  when 'Redhat', 'Suse'
65
- trivy_install_cmd = "rpm -ivh #{trivy_url}"
66
- grype_install_cmd = "rpm -ivh #{grype_url}"
69
+ trivy_install_cmd = "rpm -ivh \"#{trivy_url}\""
70
+ grype_install_cmd = "rpm -ivh \"#{grype_url}\""
67
71
  end
68
72
  -%>
69
73
 
70
- <%= trivy_install_cmd if input('scanner_to_install') == 'both' || input('scanner_to_install') == 'trivy' %>
71
- <%= grype_install_cmd if input('scanner_to_install') == 'both' || input('scanner_to_install') == 'grype' %>
74
+ <% if input('scanner_to_install') == 'both' || input('scanner_to_install') == 'trivy' -%>
75
+ <%= trivy_download_cmd unless trivy_install_cmd.nil? %>
76
+ <%= trivy_install_cmd %>
77
+ <%= trivy_cleanup_cmd unless trivy_cleanup_cmd.nil? %>
78
+ <% end -%>
79
+
80
+ <% if input('scanner_to_install') == 'both' || input('scanner_to_install') == 'grype' -%>
81
+ <%= grype_download_cmd unless grype_download_cmd.nil? %>
82
+ <%= grype_install_cmd %>
83
+ <%= grype_cleanup_cmd unless grype_cleanup_cmd.nil? %>
84
+ <% end %>
@@ -29,16 +29,16 @@ template_inputs:
29
29
  default: /
30
30
  hidden_value: false
31
31
  - name: scanner
32
- required: true
32
+ required: false
33
33
  input_type: user
34
34
  options: "trivy\r\ngrype"
35
35
  advanced: false
36
36
  value_type: plain
37
- default: trivy
38
37
  hidden_value: false
39
38
  %>
40
39
  <%
41
- scanner = input('scanner')
40
+ scanner = input('scanner').to_s.strip
41
+ scanner = foreman_cve_scanner('preferred_scanner') if scanner.empty?
42
42
  target = input('target').to_sym
43
43
  path = input('path')
44
44
  options = input('options').to_s
@@ -69,5 +69,7 @@ exit 1
69
69
  <% else -%>
70
70
  echo "===START"
71
71
  <%= exec_command %>
72
+ rc=$?
72
73
  echo "===END"
74
+ exit $rc
73
75
  <% end -%>
data/config/routes.rb CHANGED
@@ -8,9 +8,14 @@ Rails.application.routes.draw do
8
8
  constraints: ApiConstraints.new(version: 2, default: true) do
9
9
  constraints(host_id: %r{[^/]+}) do
10
10
  resources :hosts, only: [] do
11
- resources :cve_scans, only: %i[index show] do
11
+ resources :cve_scans, only: %i[index show destroy] do
12
12
  collection do
13
+ post :import
13
14
  get :latest
15
+ get :compare
16
+ end
17
+ member do
18
+ get :export
14
19
  end
15
20
  end
16
21
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddSourceAndScannedAtToForemanCveScannerCveScans < ActiveRecord::Migration[6.1]
4
+ def up
5
+ add_column :foreman_cve_scanner_cve_scans, :source, :string
6
+ add_column :foreman_cve_scanner_cve_scans, :scanned_at, :datetime
7
+
8
+ execute <<~SQL.squish
9
+ UPDATE foreman_cve_scanner_cve_scans
10
+ SET source = 'rex', scanned_at = created_at
11
+ WHERE source IS NULL OR scanned_at IS NULL
12
+ SQL
13
+
14
+ change_column_null :foreman_cve_scanner_cve_scans, :source, false
15
+ change_column_null :foreman_cve_scanner_cve_scans, :scanned_at, false
16
+
17
+ add_index :foreman_cve_scanner_cve_scans, %i[host_id scanned_at],
18
+ order: { scanned_at: :desc }
19
+ end
20
+
21
+ def down
22
+ remove_index :foreman_cve_scanner_cve_scans, %i[host_id scanned_at]
23
+ remove_column :foreman_cve_scanner_cve_scans, :scanned_at
24
+ remove_column :foreman_cve_scanner_cve_scans, :source
25
+ end
26
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'foreman_remote_execution'
4
+ require 'foreman_cve_scanner/template_helpers'
4
5
 
5
6
  module ForemanCveScanner
6
7
  # Rails engine for the Foreman CVE Scanner plugin.
@@ -15,20 +16,7 @@ module ForemanCveScanner
15
16
 
16
17
  initializer 'foreman_cve_scanner.register_plugin', before: :finisher_hook do |app|
17
18
  app.reloader.to_prepare do
18
- Foreman::Plugin.register :foreman_cve_scanner do
19
- requires_foreman '>= 3.13'
20
- register_global_js_file 'fills'
21
-
22
- apipie_documented_controllers ["#{ForemanCveScanner::Engine.root}/app/controllers/api/v2/*.rb"]
23
-
24
- security_block :foreman_cve_scanner do
25
- permission :view_cve_scans,
26
- { 'api/v2/cve_scans': %i[index latest show destroy] },
27
- resource_type: 'Host'
28
- end
29
-
30
- add_all_permissions_to_default_roles
31
- end
19
+ ForemanCveScanner::Engine.register_plugin
32
20
  end
33
21
  end
34
22
 
@@ -38,6 +26,7 @@ module ForemanCveScanner
38
26
  Host::Managed.include ForemanCveScanner::HostExtensions
39
27
  require_dependency 'host_status/cve_status'
40
28
  HostStatus.status_registry.add(HostStatus::CveStatus)
29
+ ForemanCveScanner::Engine.register_katello_integration
41
30
  ForemanCveScanner::Engine.register_rex_features
42
31
  end
43
32
 
@@ -50,10 +39,72 @@ module ForemanCveScanner
50
39
  def self.register_rex_features
51
40
  RemoteExecutionFeature.register(
52
41
  :run_cve_scan,
53
- N_('Run a CVE scan on a host'),
54
- description: N_('Run CVE scan on host'),
55
- host_action_button: true
42
+ N_('Run CVE scan'),
43
+ description: N_('Run CVE scan'),
44
+ host_action_button: true,
45
+ provided_inputs: %w[scanner]
56
46
  )
57
47
  end
48
+
49
+ def self.register_plugin
50
+ Foreman::Plugin.register :foreman_cve_scanner do
51
+ requires_foreman '>= 3.13'
52
+ register_global_js_file 'fills'
53
+ apipie_documented_controllers ForemanCveScanner::Engine.documented_controllers
54
+ extend_template_helpers ForemanCveScanner::TemplateHelpers
55
+ ForemanCveScanner::Engine.register_settings(self)
56
+ ForemanCveScanner::Engine.register_permissions(self)
57
+ add_all_permissions_to_default_roles
58
+ end
59
+ end
60
+
61
+ def self.documented_controllers
62
+ ["#{ForemanCveScanner::Engine.root}/app/controllers/api/v2/*.rb"]
63
+ end
64
+
65
+ def self.register_katello_integration
66
+ return unless Foreman::Plugin.installed?(:katello)
67
+ return unless defined?(::Katello::Host::ProfilesUploader)
68
+ ::Katello::Host::ProfilesUploader.prepend(ForemanCveScanner::ProfilesUploader)
69
+ end
70
+
71
+ def self.register_settings(plugin)
72
+ plugin.settings do
73
+ category :foreman_cve_scanner, N_('CVE Scanner') do
74
+ setting 'preferred_cve_scanner',
75
+ type: :string,
76
+ default: 'trivy',
77
+ full_name: N_('Preferred CVE scanner'),
78
+ description: N_('Default scanner used by the Run CVE scan job template.')
79
+ setting 'run_cve_scan_after_host_profiles_upload',
80
+ type: :boolean,
81
+ default: false,
82
+ full_name: N_('Run CVE scan after host profiles upload'),
83
+ description: N_('When Katello is installed, schedule a CVE scan after a host profiles upload completes.')
84
+ setting 'cve_scan_delete_after_days',
85
+ type: :integer,
86
+ default: 90,
87
+ full_name: N_('Delete CVE scans after X days'),
88
+ description: N_(
89
+ 'Delete CVE scans older than the configured number of days. ' \
90
+ 'Set to 0 to disable automatic cleanup.'
91
+ )
92
+ end
93
+ end
94
+ end
95
+
96
+ def self.register_permissions(plugin)
97
+ plugin.security_block :foreman_cve_scanner do
98
+ permission :view_cve_scans,
99
+ { 'api/v2/cve_scans': %i[index latest show export compare] },
100
+ resource_type: 'Host'
101
+ permission :import_cve_scans,
102
+ { 'api/v2/cve_scans': %i[import] },
103
+ resource_type: 'Host'
104
+ permission :destroy_cve_scans,
105
+ { 'api/v2/cve_scans': %i[destroy] },
106
+ resource_type: 'Host'
107
+ end
108
+ end
58
109
  end
59
110
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanCveScanner
4
+ module TemplateHelpers
5
+ extend ApipieDSL::Class
6
+
7
+ apipie :class, 'Macros related to Foreman CVE Scanner templates' do
8
+ name 'Foreman CVE Scanner'
9
+ sections only: %w[all jobs]
10
+ end
11
+
12
+ apipie :method, 'Returns values from Foreman CVE Scanner plugin settings' do
13
+ desc 'Use this macro in templates to access CVE Scanner settings in safe mode'
14
+ param :setting_name, String, desc: 'Setting name to resolve'
15
+ returns String, desc: 'Value of the requested CVE Scanner setting'
16
+ raises error: 'Foreman::Exception', desc: 'Raised when an unknown setting name is requested'
17
+ example "foreman_cve_scanner('preferred_scanner') #=> 'trivy'"
18
+ end
19
+ def foreman_cve_scanner(setting_name)
20
+ case setting_name.to_s
21
+ when 'preferred_scanner'
22
+ Setting[:preferred_cve_scanner]
23
+ else
24
+ raise ::Foreman::Exception, _('Unknown Foreman CVE Scanner template setting')
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanCveScanner
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
@@ -14,3 +14,14 @@ namespace :test do
14
14
  end
15
15
 
16
16
  Rake::Task[:test].enhance ['test:foreman_cve_scanner']
17
+
18
+ namespace :foreman_cve_scanner do
19
+ desc 'Delete old CVE scans using the configured retention or a DAYS override'
20
+ task cleanup_scans: :environment do
21
+ override = ENV['DAYS']
22
+ deleted = ForemanCveScanner::ScanCleanup.new(days: override.presence).cleanup!
23
+ basis = override.present? ? "DAYS=#{override}" : 'the configured retention'
24
+
25
+ puts "Deleted #{deleted} CVE scans using #{basis}."
26
+ end
27
+ end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_cve_scanner",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Run CVE scan on host and collect report",
5
5
  "main": "webpack/index.js",
6
6
  "directories": {