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.
- checksums.yaml +4 -4
- data/README.md +101 -5
- data/app/controllers/api/v2/cve_scans_controller.rb +170 -1
- data/app/models/foreman_cve_scanner/cve_scan.rb +15 -2
- data/app/models/host_status/cve_status.rb +3 -1
- data/app/services/concerns/foreman_cve_scanner/profiles_uploader.rb +25 -0
- data/app/services/foreman_cve_scanner/cve_report_scanner.rb +5 -8
- data/app/services/foreman_cve_scanner/scan_cleanup.rb +34 -0
- data/app/services/foreman_cve_scanner/scan_comparison.rb +145 -0
- data/app/services/foreman_cve_scanner/scan_importer.rb +17 -15
- data/app/views/api/v2/cve_scans/base.json.rabl +1 -1
- data/app/views/api/v2/cve_scans/main.json.rabl +1 -1
- data/app/views/foreman_cve_scanner/job_templates/install_cve_scanners.erb +22 -9
- data/app/views/foreman_cve_scanner/job_templates/run_cve_scanner.erb +5 -3
- data/config/routes.rb +6 -1
- data/db/migrate/20260514080000_add_source_and_scanned_at_to_foreman_cve_scanner_cve_scans.rb +26 -0
- data/lib/foreman_cve_scanner/engine.rb +68 -17
- data/lib/foreman_cve_scanner/template_helpers.rb +28 -0
- data/lib/foreman_cve_scanner/version.rb +1 -1
- data/lib/tasks/foreman_cve_scanner_tasks.rake +11 -0
- data/package.json +1 -1
- data/test/controllers/api/v2/cve_scans_controller_test.rb +260 -5
- data/test/lib/foreman_cve_scanner/profiles_uploader_test.rb +84 -0
- data/test/lib/foreman_cve_scanner/template_helpers_test.rb +29 -0
- data/test/models/foreman_cve_scanner/cve_scan_test.rb +120 -0
- data/test/models/host_status/cve_status_test.rb +12 -3
- data/test/services/foreman_cve_scanner/scan_cleanup_test.rb +69 -0
- data/test/services/foreman_cve_scanner/scan_comparison_test.rb +84 -0
- data/test/services/foreman_cve_scanner/scan_importer_test.rb +68 -5
- data/webpack/components/CveCompareModal.js +298 -0
- data/webpack/components/CveDetailsCard.js +141 -121
- data/webpack/components/CveFindingsModal.js +131 -111
- data/webpack/components/CveScansReports.js +227 -0
- data/webpack/components/CveScansTab.js +122 -119
- data/webpack/components/CveTrendChart.js +264 -0
- data/webpack/components/__tests__/CveCompareModal.test.js +104 -0
- data/webpack/components/__tests__/CveDetailsCard.test.js +106 -20
- data/webpack/components/__tests__/CveFindingsModal.test.js +54 -2
- data/webpack/components/__tests__/CveScansTab.test.js +185 -5
- data/webpack/components/__tests__/CveTrendChart.test.js +122 -0
- data/webpack/components/__tests__/cve_helpers.test.js +18 -0
- data/webpack/components/cve_helpers.js +139 -0
- data/webpack/components/cve_scans.scss +464 -9
- data/webpack/components/useModalScan.js +26 -0
- metadata +24 -3
- data/webpack/components/CveHistoryTable.js +0 -58
- 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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
@@ -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 = '
|
|
46
|
+
grype_arch = 'linux_amd64'
|
|
47
47
|
when 'ppc64le'
|
|
48
48
|
trivy_arch = 'Linux-PPC64LE'
|
|
49
|
-
grype_arch = '
|
|
49
|
+
grype_arch = 'linux_ppc64le'
|
|
50
50
|
when 'aarch64'
|
|
51
51
|
trivy_arch = 'Linux-ARM64'
|
|
52
|
-
grype_arch = '
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
<%=
|
|
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:
|
|
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
|
-
|
|
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
|
|
54
|
-
description: N_('Run CVE scan
|
|
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
|
|
@@ -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
|