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
@@ -7,8 +7,8 @@ module Api
7
7
  class CveScansControllerTest < ActionController::TestCase
8
8
  def setup
9
9
  @host = FactoryBot.create(:host)
10
- @scan_old = create_scan(created_at: 2.hours.ago, total: 1, low: 1)
11
- @scan_new = create_scan(created_at: 1.hour.ago, total: 2, high: 2)
10
+ @scan_old = create_scan(created_at: 2.hours.ago, scanned_at: 2.hours.ago, total: 1, low: 1)
11
+ @scan_new = create_scan(created_at: 1.hour.ago, scanned_at: 1.hour.ago, total: 2, high: 2)
12
12
  end
13
13
 
14
14
  test 'index returns scans for host' do
@@ -33,11 +33,130 @@ module Api
33
33
  assert_equal @scan_old.id, body['id']
34
34
  end
35
35
 
36
+ test 'import imports external scan for host' do
37
+ assert_difference('ForemanCveScanner::CveScan.count', 1) do
38
+ post :import, params: {
39
+ host_id: @host.id,
40
+ cve_scan: create_payload,
41
+ }
42
+ end
43
+
44
+ assert_response :created
45
+ body = ActiveSupport::JSON.decode(@response.body)
46
+ assert_equal 'external', body['source']
47
+ assert_equal 1, body['critical']
48
+ end
49
+
50
+ test 'import rejects scan without scanned_at' do
51
+ post :import, params: {
52
+ host_id: @host.id,
53
+ cve_scan: create_payload.except(:scanned_at),
54
+ }
55
+
56
+ assert_response :unprocessable_entity
57
+ end
58
+
59
+ test 'compare returns scan comparison for host' do
60
+ set_comparison_findings!
61
+
62
+ get :compare, params: {
63
+ host_id: @host.id,
64
+ first_id: @scan_old.id,
65
+ second_id: @scan_new.id,
66
+ }
67
+
68
+ assert_response :success
69
+ body = ActiveSupport::JSON.decode(@response.body)
70
+ assert_equal @scan_old.id, body['previous']['id']
71
+ assert_equal 1, body['summary']['updated']
72
+ assert_equal 1, body['summary']['resolved']
73
+ assert_equal 1, body['summary']['new']
74
+ end
75
+
76
+ test 'compare includes diff payload for updated finding' do
77
+ set_comparison_findings!
78
+
79
+ get :compare, params: {
80
+ host_id: @host.id,
81
+ first_id: @scan_old.id,
82
+ second_id: @scan_new.id,
83
+ }
84
+
85
+ assert_response :success
86
+ body = ActiveSupport::JSON.decode(@response.body)
87
+ updated_row = body['results'].find { |row| row['id'] == 'CVE-1' }
88
+ assert_equal 'updated', updated_row['status']
89
+ assert_equal 'CRITICAL', updated_row['diff']['severity']['new']
90
+ end
91
+
92
+ test 'compare requires both scan ids' do
93
+ get :compare, params: { host_id: @host.id, first_id: @scan_old.id }
94
+
95
+ assert_response :unprocessable_entity
96
+ end
97
+
98
+ test 'export returns csv headers for scan by id' do
99
+ set_export_findings!
100
+
101
+ get :export, params: { host_id: @host.id, id: @scan_old.id }
102
+
103
+ assert_response :success
104
+ assert_export_headers
105
+ end
106
+
107
+ test 'export returns csv body for scan by id' do
108
+ set_export_findings!
109
+
110
+ get :export, params: { host_id: @host.id, id: @scan_old.id }
111
+
112
+ assert_response :success
113
+ assert_export_body
114
+ end
115
+
116
+ test 'show does not return a scan from another host' do
117
+ other_host = FactoryBot.create(:host)
118
+ other_scan = create_scan(host: other_host, total: 1, low: 1)
119
+
120
+ assert_not_found do
121
+ get :show, params: { host_id: @host.id, id: other_scan.id }
122
+ end
123
+ end
124
+
125
+ test 'export does not return a scan from another host' do
126
+ other_host = FactoryBot.create(:host)
127
+ other_scan = create_scan(host: other_host, total: 1, low: 1)
128
+
129
+ assert_not_found do
130
+ get :export, params: { host_id: @host.id, id: other_scan.id }
131
+ end
132
+ end
133
+
134
+ test 'compare does not return scans from another host' do
135
+ other_host = FactoryBot.create(:host)
136
+ other_scan = create_scan(host: other_host, total: 1, low: 1)
137
+
138
+ assert_not_found do
139
+ get :compare, params: {
140
+ host_id: @host.id,
141
+ first_id: @scan_old.id,
142
+ second_id: other_scan.id,
143
+ }
144
+ end
145
+ end
146
+
36
147
  test 'index returns not found for unknown host' do
37
148
  get :index, params: { host_id: 'does-not-exist' }
38
149
  assert_response :not_found
39
150
  end
40
151
 
152
+ test 'index finds host by name' do
153
+ get :index, params: { host_id: @host.name }
154
+ assert_response :success
155
+ body = ActiveSupport::JSON.decode(@response.body)
156
+ assert_not_nil body['results']
157
+ assert_equal 2, body['results'].size
158
+ end
159
+
41
160
  test 'latest returns no content when no scans exist' do
42
161
  ForemanCveScanner::CveScan.delete_all
43
162
 
@@ -45,22 +164,44 @@ module Api
45
164
  assert_response :no_content
46
165
  end
47
166
 
167
+ test 'destroy deletes scan for host' do
168
+ assert_difference('ForemanCveScanner::CveScan.count', -1) do
169
+ delete :destroy, params: { host_id: @host.id, id: @scan_old.id }
170
+ end
171
+
172
+ assert_response :success
173
+ end
174
+
175
+ test 'destroy does not delete a scan from another host' do
176
+ other_host = FactoryBot.create(:host)
177
+ other_scan = create_scan(host: other_host, total: 1, low: 1)
178
+
179
+ assert_no_difference('ForemanCveScanner::CveScan.count') do
180
+ assert_not_found do
181
+ delete :destroy, params: { host_id: @host.id, id: other_scan.id }
182
+ end
183
+ end
184
+ end
185
+
48
186
  private
49
187
 
50
- # rubocop:disable Metrics/MethodLength
51
188
  def create_scan(options = {})
52
189
  defaults = {
53
190
  created_at: Time.now.utc,
191
+ scanned_at: Time.current,
54
192
  total: 0,
55
193
  critical: 0,
56
194
  high: 0,
57
195
  medium: 0,
58
196
  low: 0,
197
+ host: @host,
59
198
  }
60
199
  options = defaults.merge(options)
61
200
  ForemanCveScanner::CveScan.create!(
62
- host: @host,
201
+ host: options[:host],
63
202
  scanner: 'trivy',
203
+ source: 'rex',
204
+ scanned_at: options[:scanned_at],
64
205
  created_at: options[:created_at],
65
206
  raw: { 'dummy' => true },
66
207
  summary: { 'worst' => 'low' },
@@ -72,7 +213,121 @@ module Api
72
213
  low: options[:low]
73
214
  )
74
215
  end
75
- # rubocop:enable Metrics/MethodLength
216
+
217
+ def set_export_findings!
218
+ @scan_old.update!(
219
+ findings: [
220
+ {
221
+ 'severity' => 'HIGH',
222
+ 'published' => '2026-02-20',
223
+ 'name' => 'openssl',
224
+ 'version' => '1.1',
225
+ 'fixed' => '1.2',
226
+ 'status' => 'fixed',
227
+ 'id' => 'CVE-2026-0001',
228
+ 'title' => 'OpenSSL issue',
229
+ 'url' => 'https://example.test/CVE-2026-0001',
230
+ },
231
+ ]
232
+ )
233
+ end
234
+
235
+ def create_payload
236
+ {
237
+ scanner: 'custom-scanner',
238
+ source: 'external',
239
+ scanned_at: '2026-05-14T08:00:00Z',
240
+ findings: [external_finding],
241
+ }
242
+ end
243
+
244
+ def set_comparison_findings!
245
+ @scan_old.update!(findings: old_comparison_findings)
246
+ @scan_new.update!(findings: new_comparison_findings)
247
+ end
248
+
249
+ def external_finding
250
+ {
251
+ id: 'CVE-2026-1111',
252
+ name: 'openssl',
253
+ severity: 'CRITICAL',
254
+ version: '1.0',
255
+ fixed: '1.1',
256
+ status: 'affected',
257
+ title: 'OpenSSL issue',
258
+ published: '2026-05-14T06:00:00Z',
259
+ url: 'https://example.test/CVE-2026-1111',
260
+ }
261
+ end
262
+
263
+ def old_comparison_findings
264
+ [
265
+ comparison_finding(
266
+ id: 'CVE-1',
267
+ name: 'openssl',
268
+ severity: 'HIGH',
269
+ version: '1.0',
270
+ title: 'OpenSSL issue'
271
+ ),
272
+ comparison_finding(
273
+ id: 'CVE-2',
274
+ name: 'curl',
275
+ severity: 'LOW',
276
+ version: '1.0',
277
+ title: 'Curl issue'
278
+ ),
279
+ ]
280
+ end
281
+
282
+ def new_comparison_findings
283
+ [
284
+ comparison_finding(
285
+ id: 'CVE-1',
286
+ name: 'openssl',
287
+ severity: 'CRITICAL',
288
+ version: '1.0',
289
+ title: 'OpenSSL issue'
290
+ ),
291
+ comparison_finding(
292
+ id: 'CVE-3',
293
+ name: 'bash',
294
+ severity: 'MEDIUM',
295
+ version: '2.0',
296
+ title: 'Bash issue',
297
+ published: '2026-02-21'
298
+ ),
299
+ ]
300
+ end
301
+
302
+ def comparison_finding(attributes)
303
+ {
304
+ 'id' => attributes.fetch(:id),
305
+ 'name' => attributes.fetch(:name),
306
+ 'severity' => attributes.fetch(:severity),
307
+ 'version' => attributes.fetch(:version),
308
+ 'title' => attributes.fetch(:title),
309
+ 'published' => attributes.fetch(:published, '2026-02-20'),
310
+ }
311
+ end
312
+
313
+ def assert_not_found
314
+ yield
315
+ assert_response :not_found
316
+ rescue ActiveRecord::RecordNotFound
317
+ assert true
318
+ end
319
+
320
+ def assert_export_headers
321
+ assert_includes @response.header['Content-Type'], 'text/csv'
322
+ assert_includes @response.header['Content-Disposition'], 'attachment'
323
+ assert_includes @response.header['Content-Disposition'], @host.shortname
324
+ end
325
+
326
+ def assert_export_body
327
+ assert_includes @response.body, 'Severity,Published,Package'
328
+ assert_includes @response.body, 'CVE-2026-0001'
329
+ assert_includes @response.body, 'openssl'
330
+ end
76
331
  end
77
332
  end
78
333
  end
@@ -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
- created_at: Time.now.utc,
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