foreman_cve_scanner 0.5.1 → 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 -3
- 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 +3 -6
- 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 +3 -3
- 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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module ForemanCveScanner
|
|
6
|
+
class ProfilesUploaderTest < ActiveSupport::TestCase
|
|
7
|
+
class DummyUploader
|
|
8
|
+
prepend ForemanCveScanner::ProfilesUploader
|
|
9
|
+
|
|
10
|
+
def initialize(host:, result:)
|
|
11
|
+
@host = host
|
|
12
|
+
@result = result
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def upload
|
|
16
|
+
@result
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def setup
|
|
21
|
+
skip 'Katello is not installed' unless Foreman::Plugin.installed?(:katello)
|
|
22
|
+
|
|
23
|
+
@host = FactoryBot.create(:host)
|
|
24
|
+
@previous_preferred_scanner = Setting[:preferred_cve_scanner]
|
|
25
|
+
@previous_run_after_upload = Setting[:run_cve_scan_after_host_profiles_upload]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def teardown
|
|
29
|
+
return unless Foreman::Plugin.installed?(:katello)
|
|
30
|
+
|
|
31
|
+
Setting[:preferred_cve_scanner] = @previous_preferred_scanner
|
|
32
|
+
Setting[:run_cve_scan_after_host_profiles_upload] = @previous_run_after_upload
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test 'upload schedules cve scan when setting is enabled' do
|
|
36
|
+
Setting[:preferred_cve_scanner] = 'grype'
|
|
37
|
+
Setting[:run_cve_scan_after_host_profiles_upload] = true
|
|
38
|
+
|
|
39
|
+
captured = {}
|
|
40
|
+
triggering = Struct.new(:mode).new
|
|
41
|
+
composer = Struct.new(:triggering) do
|
|
42
|
+
attr_reader :triggered
|
|
43
|
+
|
|
44
|
+
def trigger!
|
|
45
|
+
@triggered = true
|
|
46
|
+
end
|
|
47
|
+
end.new(triggering)
|
|
48
|
+
|
|
49
|
+
JobInvocationComposer.stub(:for_feature, lambda { |feature, host, scanner:|
|
|
50
|
+
captured[:feature] = feature
|
|
51
|
+
captured[:host] = host
|
|
52
|
+
captured[:scanner] = scanner
|
|
53
|
+
composer
|
|
54
|
+
}) do
|
|
55
|
+
assert DummyUploader.new(host: @host, result: true).upload
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
assert_equal [:run_cve_scan, @host, 'grype'],
|
|
59
|
+
[captured[:feature], captured[:host], captured[:scanner]]
|
|
60
|
+
assert_equal :future, triggering.mode
|
|
61
|
+
assert composer.triggered
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test 'upload keeps upstream result when scan scheduling fails' do
|
|
65
|
+
Setting[:run_cve_scan_after_host_profiles_upload] = true
|
|
66
|
+
|
|
67
|
+
JobInvocationComposer.stub(:for_feature, lambda { |_feature, _host, scanner:|
|
|
68
|
+
raise StandardError, scanner
|
|
69
|
+
}) do
|
|
70
|
+
assert DummyUploader.new(host: @host, result: true).upload
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
test 'upload does not schedule cve scan when setting is disabled' do
|
|
75
|
+
Setting[:run_cve_scan_after_host_profiles_upload] = false
|
|
76
|
+
|
|
77
|
+
JobInvocationComposer.stub(:for_feature, lambda { |_feature, _host, scanner:|
|
|
78
|
+
flunk("unexpected scanner #{scanner}")
|
|
79
|
+
}) do
|
|
80
|
+
assert DummyUploader.new(host: @host, result: true).upload
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module ForemanCveScanner
|
|
6
|
+
class TemplateHelpersTest < ActiveSupport::TestCase
|
|
7
|
+
include ForemanCveScanner::TemplateHelpers
|
|
8
|
+
|
|
9
|
+
def setup
|
|
10
|
+
@previous_preferred_scanner = Setting[:preferred_cve_scanner]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def teardown
|
|
14
|
+
Setting[:preferred_cve_scanner] = @previous_preferred_scanner
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
test 'foreman_cve_scanner returns preferred scanner setting' do
|
|
18
|
+
Setting[:preferred_cve_scanner] = 'grype'
|
|
19
|
+
|
|
20
|
+
assert_equal 'grype', foreman_cve_scanner('preferred_scanner')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test 'foreman_cve_scanner raises for unknown setting name' do
|
|
24
|
+
assert_raises(::Foreman::Exception) do
|
|
25
|
+
foreman_cve_scanner('unknown_setting')
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module ForemanCveScanner
|
|
6
|
+
class CveScanTest < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@host = FactoryBot.create(:host)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
test 'is invalid without required host, scanner, source and scanned_at attributes' do
|
|
12
|
+
scan = CveScan.new
|
|
13
|
+
|
|
14
|
+
assert_not scan.valid?
|
|
15
|
+
assert_includes scan.errors.attribute_names, :host_id
|
|
16
|
+
assert_includes scan.errors.attribute_names, :scanner
|
|
17
|
+
assert_includes scan.errors.attribute_names, :source
|
|
18
|
+
assert_includes scan.errors.attribute_names, :scanned_at
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
test 'is invalid without required raw and summary payload attributes' do
|
|
22
|
+
scan = CveScan.new
|
|
23
|
+
|
|
24
|
+
assert_not scan.valid?
|
|
25
|
+
assert_includes scan.errors.attribute_names, :raw
|
|
26
|
+
assert_includes scan.errors.attribute_names, :summary
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
test 'is invalid with nil findings' do
|
|
30
|
+
scan = CveScan.new(
|
|
31
|
+
host: @host,
|
|
32
|
+
scanner: 'trivy',
|
|
33
|
+
source: 'rex',
|
|
34
|
+
scanned_at: Time.current,
|
|
35
|
+
raw: { 'dummy' => true },
|
|
36
|
+
summary: { 'worst' => 'none' },
|
|
37
|
+
findings: nil,
|
|
38
|
+
total: 0,
|
|
39
|
+
critical: 0,
|
|
40
|
+
high: 0,
|
|
41
|
+
medium: 0,
|
|
42
|
+
low: 0
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
assert_not scan.valid?
|
|
46
|
+
assert_includes scan.errors.attribute_names, :findings
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
test 'is valid with empty findings' do
|
|
50
|
+
scan = CveScan.new(
|
|
51
|
+
host: @host,
|
|
52
|
+
scanner: 'trivy',
|
|
53
|
+
source: 'rex',
|
|
54
|
+
scanned_at: Time.current,
|
|
55
|
+
raw: { 'dummy' => true },
|
|
56
|
+
summary: { 'worst' => 'none' },
|
|
57
|
+
findings: [],
|
|
58
|
+
total: 0,
|
|
59
|
+
critical: 0,
|
|
60
|
+
high: 0,
|
|
61
|
+
medium: 0,
|
|
62
|
+
low: 0
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
assert scan.valid?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
test 'for_host returns only scans for the given host' do
|
|
69
|
+
other_host = FactoryBot.create(:host)
|
|
70
|
+
host_scan = create_scan(host: @host, total: 1)
|
|
71
|
+
other_scan = create_scan(host: other_host, total: 2)
|
|
72
|
+
|
|
73
|
+
result = CveScan.for_host(@host.id)
|
|
74
|
+
|
|
75
|
+
assert_includes result, host_scan
|
|
76
|
+
assert_not_includes result, other_scan
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
test 'recent_first orders by scanned_at desc and id desc' do
|
|
80
|
+
older = create_scan(host: @host, scanned_at: 2.hours.ago)
|
|
81
|
+
newer = create_scan(host: @host, scanned_at: 1.hour.ago)
|
|
82
|
+
timestamp = Time.zone.parse('2026-05-13 10:00:00')
|
|
83
|
+
same_time_a = create_scan(host: @host, scanned_at: timestamp)
|
|
84
|
+
same_time_b = create_scan(host: @host, scanned_at: timestamp)
|
|
85
|
+
|
|
86
|
+
result = CveScan.recent_first.to_a
|
|
87
|
+
|
|
88
|
+
assert_operator result.index(newer), :<, result.index(older)
|
|
89
|
+
assert_operator result.index(same_time_b), :<, result.index(same_time_a)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
test 'destroying a host destroys its cve scans' do
|
|
93
|
+
create_scan(host: @host, total: 1)
|
|
94
|
+
create_scan(host: @host, total: 2)
|
|
95
|
+
|
|
96
|
+
assert_difference('ForemanCveScanner::CveScan.count', -2) do
|
|
97
|
+
@host.destroy
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def create_scan(host:, scanned_at: Time.current, total: 0)
|
|
104
|
+
CveScan.create!(
|
|
105
|
+
host: host,
|
|
106
|
+
scanner: 'trivy',
|
|
107
|
+
source: 'rex',
|
|
108
|
+
scanned_at: scanned_at,
|
|
109
|
+
raw: { 'dummy' => true },
|
|
110
|
+
summary: { 'worst' => 'low' },
|
|
111
|
+
findings: [{ 'id' => 'CVE-0000-0000' }],
|
|
112
|
+
total: total,
|
|
113
|
+
critical: 0,
|
|
114
|
+
high: 0,
|
|
115
|
+
medium: 0,
|
|
116
|
+
low: total
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -39,6 +39,14 @@ module HostStatus
|
|
|
39
39
|
assert_equal 1, status.to_status
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
test 'zero findings scan returns ok status' do
|
|
43
|
+
create_scan(critical: 0, high: 0, medium: 0, low: 0)
|
|
44
|
+
status = @host.get_status(HostStatus::CveStatus)
|
|
45
|
+
assert_equal 'No CVEs found', status.to_label
|
|
46
|
+
assert_equal HostStatus::Global::OK, status.to_global
|
|
47
|
+
assert_equal 0, status.to_status
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
test 'status is registered in registry' do
|
|
43
51
|
assert_includes HostStatus.status_registry, HostStatus::CveStatus
|
|
44
52
|
end
|
|
@@ -50,10 +58,11 @@ module HostStatus
|
|
|
50
58
|
ForemanCveScanner::CveScan.create!(
|
|
51
59
|
host: @host,
|
|
52
60
|
scanner: 'trivy',
|
|
53
|
-
|
|
61
|
+
source: 'rex',
|
|
62
|
+
scanned_at: Time.current,
|
|
54
63
|
raw: { 'dummy' => true },
|
|
55
|
-
summary: { 'worst' => 'low' },
|
|
56
|
-
findings: [{ 'id' => 'CVE-0000-0000' }],
|
|
64
|
+
summary: { 'worst' => total.zero? ? 'none' : 'low' },
|
|
65
|
+
findings: total.zero? ? [] : [{ 'id' => 'CVE-0000-0000' }],
|
|
57
66
|
total: total,
|
|
58
67
|
critical: critical,
|
|
59
68
|
high: high,
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module ForemanCveScanner
|
|
6
|
+
class ScanCleanupTest < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@host = FactoryBot.create(:host)
|
|
9
|
+
@previous_setting = Setting[:cve_scan_delete_after_days]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def teardown
|
|
13
|
+
Setting[:cve_scan_delete_after_days] = @previous_setting
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
test 'cleanup! does nothing when retention is disabled' do
|
|
17
|
+
Setting[:cve_scan_delete_after_days] = 0
|
|
18
|
+
old_scan = create_scan(10.days.ago)
|
|
19
|
+
|
|
20
|
+
deleted = ScanCleanup.new(scope: @host.cve_scans).cleanup!
|
|
21
|
+
|
|
22
|
+
assert_equal 0, deleted
|
|
23
|
+
assert ForemanCveScanner::CveScan.exists?(old_scan.id)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
test 'cleanup! removes scans older than configured retention' do
|
|
27
|
+
Setting[:cve_scan_delete_after_days] = 5
|
|
28
|
+
old_scan = create_scan(10.days.ago)
|
|
29
|
+
recent_scan = create_scan(2.days.ago)
|
|
30
|
+
|
|
31
|
+
deleted = ScanCleanup.new(scope: @host.cve_scans).cleanup!
|
|
32
|
+
|
|
33
|
+
assert_equal 1, deleted
|
|
34
|
+
assert_not ForemanCveScanner::CveScan.exists?(old_scan.id)
|
|
35
|
+
assert ForemanCveScanner::CveScan.exists?(recent_scan.id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test 'cleanup! uses explicit day override' do
|
|
39
|
+
Setting[:cve_scan_delete_after_days] = 30
|
|
40
|
+
old_scan = create_scan(10.days.ago)
|
|
41
|
+
recent_scan = create_scan(2.days.ago)
|
|
42
|
+
|
|
43
|
+
deleted = ScanCleanup.new(scope: @host.cve_scans, days: 5).cleanup!
|
|
44
|
+
|
|
45
|
+
assert_equal 1, deleted
|
|
46
|
+
assert_not ForemanCveScanner::CveScan.exists?(old_scan.id)
|
|
47
|
+
assert ForemanCveScanner::CveScan.exists?(recent_scan.id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def create_scan(scanned_at)
|
|
53
|
+
ForemanCveScanner::CveScan.create!(
|
|
54
|
+
host: @host,
|
|
55
|
+
scanner: 'trivy',
|
|
56
|
+
source: 'rex',
|
|
57
|
+
scanned_at: scanned_at,
|
|
58
|
+
raw: { 'dummy' => true },
|
|
59
|
+
summary: { 'worst' => 'low' },
|
|
60
|
+
findings: [{ 'id' => 'CVE-0000-0000' }],
|
|
61
|
+
total: 1,
|
|
62
|
+
critical: 0,
|
|
63
|
+
high: 0,
|
|
64
|
+
medium: 0,
|
|
65
|
+
low: 1
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module ForemanCveScanner
|
|
6
|
+
class ScanComparisonTest < ActiveSupport::TestCase
|
|
7
|
+
def setup
|
|
8
|
+
@host = FactoryBot.create(:host)
|
|
9
|
+
@first_scan = create_scan(
|
|
10
|
+
scanned_at: 2.hours.ago,
|
|
11
|
+
findings: first_findings
|
|
12
|
+
)
|
|
13
|
+
@second_scan = create_scan(
|
|
14
|
+
scanned_at: 1.hour.ago,
|
|
15
|
+
findings: second_findings
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test 'compare builds summary and result rows' do
|
|
20
|
+
comparison = ScanComparison.compare(@first_scan, @second_scan)
|
|
21
|
+
statuses = comparison[:results].to_h { |row| [row[:id], row] }
|
|
22
|
+
|
|
23
|
+
assert_equal 1, comparison[:summary]['updated']
|
|
24
|
+
assert_equal 1, comparison[:summary]['resolved']
|
|
25
|
+
assert_equal 1, comparison[:summary]['new']
|
|
26
|
+
assert_equal 'updated', statuses['CVE-1'][:status]
|
|
27
|
+
assert_equal(
|
|
28
|
+
{ old: 'HIGH', new: 'CRITICAL' },
|
|
29
|
+
statuses['CVE-1'][:diff]['severity']
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
test 'compare includes previous and current scan metadata' do
|
|
34
|
+
comparison = ScanComparison.compare(@first_scan, @second_scan)
|
|
35
|
+
|
|
36
|
+
assert_equal @first_scan.id, comparison[:previous][:id]
|
|
37
|
+
assert_equal @second_scan.id, comparison[:current][:id]
|
|
38
|
+
assert_equal @first_scan.scanner, comparison[:previous][:scanner]
|
|
39
|
+
assert_equal @second_scan.scanner, comparison[:current][:scanner]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def finding(id, name, severity, version)
|
|
45
|
+
{
|
|
46
|
+
'id' => id,
|
|
47
|
+
'name' => name,
|
|
48
|
+
'severity' => severity,
|
|
49
|
+
'version' => version,
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def first_findings
|
|
54
|
+
[
|
|
55
|
+
finding('CVE-1', 'openssl', 'HIGH', '1.0'),
|
|
56
|
+
finding('CVE-2', 'curl', 'LOW', '1.0'),
|
|
57
|
+
]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def second_findings
|
|
61
|
+
[
|
|
62
|
+
finding('CVE-1', 'openssl', 'CRITICAL', '1.0'),
|
|
63
|
+
finding('CVE-3', 'glibc', 'HIGH', '1.0'),
|
|
64
|
+
]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def create_scan(scanned_at:, findings:)
|
|
68
|
+
ForemanCveScanner::CveScan.create!(
|
|
69
|
+
host: @host,
|
|
70
|
+
scanner: 'trivy',
|
|
71
|
+
source: 'rex',
|
|
72
|
+
scanned_at: scanned_at,
|
|
73
|
+
raw: { 'dummy' => true },
|
|
74
|
+
summary: { 'worst' => 'high' },
|
|
75
|
+
findings: findings,
|
|
76
|
+
total: findings.size,
|
|
77
|
+
critical: findings.count { |finding| finding['severity'] == 'CRITICAL' },
|
|
78
|
+
high: findings.count { |finding| finding['severity'] == 'HIGH' },
|
|
79
|
+
medium: findings.count { |finding| finding['severity'] == 'MEDIUM' },
|
|
80
|
+
low: findings.count { |finding| finding['severity'] == 'LOW' }
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -6,6 +6,11 @@ module ForemanCveScanner
|
|
|
6
6
|
class ScanImporterTest < ActiveSupport::TestCase
|
|
7
7
|
def setup
|
|
8
8
|
@host = FactoryBot.create(:host)
|
|
9
|
+
@previous_retention = Setting[:cve_scan_delete_after_days]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def teardown
|
|
13
|
+
Setting[:cve_scan_delete_after_days] = @previous_retention
|
|
9
14
|
end
|
|
10
15
|
|
|
11
16
|
test 'import_for_host! persists scan from trivy output' do
|
|
@@ -19,6 +24,7 @@ module ForemanCveScanner
|
|
|
19
24
|
assert_not_nil scan
|
|
20
25
|
assert_equal @host.id, scan.host_id
|
|
21
26
|
assert_equal 'trivy', scan.scanner
|
|
27
|
+
assert_equal 'rex', scan.source
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
test 'import_for_host! sets totals and findings for trivy output' do
|
|
@@ -29,6 +35,7 @@ module ForemanCveScanner
|
|
|
29
35
|
|
|
30
36
|
assert_operator scan.total, :>, 0
|
|
31
37
|
assert_equal scan.total, scan.findings.count
|
|
38
|
+
assert_not_nil scan.scanned_at
|
|
32
39
|
end
|
|
33
40
|
|
|
34
41
|
test 'import_for_host! persists scan from proxy output' do
|
|
@@ -51,20 +58,76 @@ module ForemanCveScanner
|
|
|
51
58
|
assert_operator scan.total, :>, 0
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
test 'import_for_host!
|
|
61
|
+
test 'import_for_host! raises when no json markers' do
|
|
55
62
|
importer = ForemanCveScanner::ScanImporter.new('no markers here')
|
|
56
63
|
|
|
64
|
+
assert_raises(::Foreman::Exception) do
|
|
65
|
+
importer.import_for_host!(@host)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
test 'import_for_host! raises when json is invalid' do
|
|
70
|
+
importer = ForemanCveScanner::ScanImporter.new("===START\n{bad\n===END")
|
|
71
|
+
|
|
72
|
+
assert_raises(::Foreman::Exception) do
|
|
73
|
+
importer.import_for_host!(@host)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
test 'import_for_host! raises when marker block is empty' do
|
|
78
|
+
importer = ForemanCveScanner::ScanImporter.new("===START\n===END")
|
|
79
|
+
|
|
80
|
+
assert_raises(::Foreman::Exception) do
|
|
81
|
+
importer.import_for_host!(@host)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test 'import_for_host! cleans up old scans for the host using retention setting' do
|
|
86
|
+
Setting[:cve_scan_delete_after_days] = 1
|
|
87
|
+
old_scan = ForemanCveScanner::CveScan.create!(
|
|
88
|
+
host: @host,
|
|
89
|
+
scanner: 'trivy',
|
|
90
|
+
source: 'rex',
|
|
91
|
+
scanned_at: 3.days.ago,
|
|
92
|
+
raw: { 'dummy' => true },
|
|
93
|
+
summary: { 'worst' => 'low' },
|
|
94
|
+
findings: [{ 'id' => 'CVE-0000-0000' }],
|
|
95
|
+
total: 1,
|
|
96
|
+
critical: 0,
|
|
97
|
+
high: 0,
|
|
98
|
+
medium: 0,
|
|
99
|
+
low: 1
|
|
100
|
+
)
|
|
101
|
+
output = wrap_output(load_fixture('trivy.json'))
|
|
102
|
+
importer = ForemanCveScanner::ScanImporter.new(output)
|
|
103
|
+
|
|
57
104
|
scan = importer.import_for_host!(@host)
|
|
58
105
|
|
|
59
|
-
|
|
106
|
+
assert_not_nil scan
|
|
107
|
+
assert_not ForemanCveScanner::CveScan.exists?(old_scan.id)
|
|
108
|
+
assert ForemanCveScanner::CveScan.exists?(scan.id)
|
|
60
109
|
end
|
|
61
110
|
|
|
62
|
-
test 'import_for_host!
|
|
63
|
-
|
|
111
|
+
test 'import_for_host! persists scan with zero findings' do
|
|
112
|
+
output = wrap_output(
|
|
113
|
+
{
|
|
114
|
+
'Results' => [
|
|
115
|
+
{
|
|
116
|
+
'Target' => '/',
|
|
117
|
+
'Class' => 'os-pkgs',
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}.to_json
|
|
121
|
+
)
|
|
122
|
+
importer = ForemanCveScanner::ScanImporter.new(output)
|
|
64
123
|
|
|
65
124
|
scan = importer.import_for_host!(@host)
|
|
66
125
|
|
|
67
|
-
|
|
126
|
+
assert_not_nil scan
|
|
127
|
+
assert_equal 'trivy', scan.scanner
|
|
128
|
+
assert_empty scan.findings
|
|
129
|
+
assert_equal 0, scan.total
|
|
130
|
+
assert_equal 'none', scan.summary['worst']
|
|
68
131
|
end
|
|
69
132
|
|
|
70
133
|
private
|