foreman_cve_scanner 0.0.3 → 0.5.1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -18
  3. data/Rakefile +2 -0
  4. data/app/controllers/api/v2/cve_scans_controller.rb +66 -0
  5. data/app/lib/actions/foreman_cve_scanner/cve_scanner_job.rb +3 -18
  6. data/app/models/concerns/foreman_cve_scanner/host_extensions.rb +16 -0
  7. data/app/models/foreman_cve_scanner/cve_scan.rb +15 -0
  8. data/app/models/host_status/cve_status.rb +71 -0
  9. data/app/services/foreman_cve_scanner/cve_report_scanner.rb +39 -37
  10. data/app/services/foreman_cve_scanner/scan_importer.rb +105 -0
  11. data/app/views/api/v2/cve_scans/base.json.rabl +5 -0
  12. data/app/views/api/v2/cve_scans/index.json.rabl +5 -0
  13. data/app/views/api/v2/cve_scans/latest.json.rabl +5 -0
  14. data/app/views/api/v2/cve_scans/main.json.rabl +7 -0
  15. data/app/views/api/v2/cve_scans/show.json.rabl +5 -0
  16. data/app/views/foreman_cve_scanner/job_templates/install_cve_scanners.erb +19 -6
  17. data/app/views/foreman_cve_scanner/job_templates/run_cve_scanner.erb +4 -2
  18. data/config/routes.rb +20 -0
  19. data/db/migrate/20260221000000_create_foreman_cve_scanner_cve_scans.rb +22 -0
  20. data/db/seeds.d/75_job_templates.rb +17 -0
  21. data/lib/foreman_cve_scanner/engine.rb +25 -3
  22. data/lib/foreman_cve_scanner/version.rb +3 -1
  23. data/lib/foreman_cve_scanner.rb +4 -1
  24. data/lib/tasks/foreman_cve_scanner_seeds.rake +12 -0
  25. data/lib/tasks/rubocop.rake +33 -0
  26. data/package.json +48 -0
  27. data/test/actions/foreman_cve_scanner/cve_scanner_job_test.rb +54 -0
  28. data/test/controllers/api/v2/cve_scans_controller_test.rb +78 -0
  29. data/test/models/host_status/cve_status_test.rb +65 -0
  30. data/test/services/foreman_cve_scanner/cve_report_scanner_test.rb +45 -17
  31. data/test/services/foreman_cve_scanner/scan_importer_test.rb +85 -0
  32. data/test/test_plugin_helper.rb +2 -0
  33. data/webpack/components/CveDetailsCard.js +258 -0
  34. data/webpack/components/CveFindingsModal.js +274 -0
  35. data/webpack/components/CveHistoryTable.js +58 -0
  36. data/webpack/components/CveOverviewCard.js +67 -0
  37. data/webpack/components/CveScansTab.js +192 -0
  38. data/webpack/components/CveSummaryCell.js +72 -0
  39. data/webpack/components/SeverityIcon.js +56 -0
  40. data/webpack/components/__tests__/CveDetailsCard.test.js +126 -0
  41. data/webpack/components/__tests__/CveFindingsModal.test.js +78 -0
  42. data/webpack/components/__tests__/CveScansTab.test.js +76 -0
  43. data/webpack/components/__tests__/cve_helpers.test.js +42 -0
  44. data/webpack/components/cve_helpers.js +35 -0
  45. data/webpack/components/cve_scans.scss +200 -0
  46. data/webpack/fills.js +1 -0
  47. data/webpack/fills_index.js +38 -0
  48. data/webpack/index.js +1 -0
  49. metadata +47 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43957c794e37f75dcba9c2d33ea26e3c293baaaec0a166781e1e06b45d529432
4
- data.tar.gz: 4f2d274846b982f944412efdd8c3280cc0ab6e63c3cd7ea9876aa59a2b409c6e
3
+ metadata.gz: 0ef55cf85764a1394ea0e94d9cdf96520e905f49a5e479d2c7254867c6797be9
4
+ data.tar.gz: 874cd48378be0b12d341e06b4f15172b7ad1c45cd607ee7d0c513a38b5a172ae
5
5
  SHA512:
6
- metadata.gz: 83121e7e1f8aa077d5d9cdcb6e7feeb2215202f2a07a4930001eed9c9d7f5d470f15be813ca82a536d5a1af5622f845f3f52af710db82497e125c5d865ae1338
7
- data.tar.gz: 6b63ffd8df67f139b30514d5a6f9d47b253b3451ced1e44f8d48c05d04ad4c03ea6ba39c6b743b2d3792e00a17a0c98c7090caac1f5904ab42022a9dca4262b7
6
+ metadata.gz: 8ba5bf4caff8597578e07c110961376c6c3ae242b8f71092e023993c1514a5a0ae185fe34fc32b052fbb1d2d773ce1dfdfb5a17a0f8ec1bff955d0b69984e6c2
7
+ data.tar.gz: d89adc8ffb92d8075c133084815b9db47b4f17e7dcb73c9e8cb1811cf52441636d1c9b2b6bf2e781438252c3d7a041f1f616476fb0237a17dafeeef2b5fd5c37
data/README.md CHANGED
@@ -1,13 +1,25 @@
1
1
  # ForemanCveScanner
2
2
 
3
- Plugin to
4
- 1. install trivy/grype CVE scanners on a host using a Foreman Remote Execution (REX) job
5
- 2. run a CVE scan using a REX job, collect the output and generate a Config Report
6
-
7
- ![image](https://github.com/ATIX-AG/foreman_cve_scanner/assets/25485845/85e3b676-7d90-41e5-bea5-7e0b5f4a685c)
8
-
9
-
10
- *Introdction here*
3
+ Plugin to:
4
+ 1. install Trivy/Grype on a host using Foreman Remote Execution (REX)
5
+ 2. run CVE scans via REX and parse the results
6
+ 3. store scan history per host in a dedicated table
7
+ 4. expose scan data via API
8
+ 5. show CVE findings in the Host details UI
9
+ 6. show summary status in the Hosts list
10
+
11
+ ![image](https://github.com/ATIX-AG/foreman_cve_scanner/assets/25485845/85e3b676-7d90-41e5-bea5-7e0b5f4a685c)
12
+
13
+ ## Features
14
+
15
+ - REX job templates for installing and running CVE scans
16
+ - JSON output parsing with robust handling of chunked stdout
17
+ - Per-host scan history with totals and severity counts
18
+ - API endpoints for scan history and latest scan
19
+ - Host Details card with findings and modal view
20
+ - Host Details tab “CVE scans” for full history
21
+ - Hosts overview list column “CVE” with quick summary and modal
22
+ - Integrated in the Host Status
11
23
 
12
24
  ## Installation
13
25
 
@@ -16,19 +28,18 @@ for how to install Foreman plugins
16
28
 
17
29
  ## Usage
18
30
 
19
- - Run the REX job to install trivy and/or grype
31
+ - Run the REX job to install Trivy and/or Grype
20
32
  - Run the REX job to scan a host
21
- - Go to the Config Report page for a host to view the scan report
33
+ - You can configure recurring CVE scans via `Monitor -> Jobs`
34
+ - View results in:
35
+ - Hosts overview list column “CVE” (use 'Manage Columns' to enable)
36
+ - Host Details card and modal
37
+ - Host Details tab “CVE scans”
22
38
 
23
39
  ## TODO
24
40
 
25
- - Better possiblities to filter the Config Report (maybe an extension to ConfigReport in Foreman)
26
- - Have a scheduled REX Job to scan the hosts
27
- - Make it visible on the Host Details page or on Foreman directly, if a high priority CVE on a host occurs
28
- - Export a CVE scan
29
- - Deliver trivy / grype via Katello
30
- - More tests
31
- - API
41
+ - Export scan results
42
+ - Deliver Trivy/Grype via Katello
32
43
 
33
44
  ## Contributing
34
45
 
@@ -50,4 +61,3 @@ GNU General Public License for more details.
50
61
 
51
62
  You should have received a copy of the GNU General Public License
52
63
  along with this program. If not, see <http://www.gnu.org/licenses/>.
53
-
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require 'bundler/setup'
3
5
  rescue LoadError
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Api
4
+ module V2
5
+ # API controller for CVE scans per host.
6
+ class CveScansController < V2::BaseController
7
+ before_action :find_host
8
+
9
+ def resource_class
10
+ ::ForemanCveScanner::CveScan
11
+ end
12
+
13
+ api :GET, '/hosts/:host_id/cve_scans', N_('List CVE scans for a host')
14
+ description N_('Returns paginated CVE scan summaries for a host.')
15
+ param :host_id, :identifier, required: true
16
+ param :page, :number, desc: N_('Page number, starting at 1')
17
+ param :per_page, :number, desc: N_('Number of results per page')
18
+ def index
19
+ @cve_scans = cve_scans_index_scope.paginate(paginate_options)
20
+ end
21
+
22
+ api :GET, '/hosts/:host_id/cve_scans/latest', N_('Get latest CVE scan for a host')
23
+ description N_('Returns the most recent CVE scan for a host.')
24
+ param :host_id, :identifier, required: true
25
+ def latest
26
+ @cve_scan = cve_scans_index_scope.first
27
+ head :no_content if @cve_scan.nil?
28
+ end
29
+
30
+ api :GET, '/hosts/:host_id/cve_scans/:id', N_('Show CVE scan for a host')
31
+ description N_('Returns a specific CVE scan by id for a host.')
32
+ param :host_id, :identifier, required: true
33
+ param :id, :identifier, required: true
34
+ def show
35
+ @cve_scan = resource_class.for_host(@host.id).find(params[:id])
36
+ end
37
+
38
+ api :DELETE, '/hosts/:host_id/cve_scans/:id', N_('Delete a CVE scan')
39
+ description N_('Deletes a specific CVE scan by id for a host.')
40
+ param :host_id, :identifier, required: true
41
+ param :id, :identifier, required: true
42
+ def destroy
43
+ @cve_scan = resource_class.for_host(@host.id).find(params[:id])
44
+ process_response @cve_scan.destroy
45
+ end
46
+
47
+ private
48
+
49
+ def cve_scans_index_scope
50
+ scope = resource_class.for_host(@host.id).recent_first
51
+ return scope unless respond_to?(:resource_scope_for_index, true)
52
+ return scope unless scope.respond_to?(:search_for)
53
+
54
+ resource_scope_for_index(scope)
55
+ end
56
+
57
+ def find_host
58
+ scope = ::Host::Base.authorized(:view_hosts)
59
+ @host = scope.find_by(name: params[:host_id]) || scope.find_by(id: params[:host_id])
60
+ return if @host.present?
61
+
62
+ not_found
63
+ end
64
+ end
65
+ end
66
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  module Actions
4
4
  module ForemanCveScanner
5
+ # Dynflow action that parses a CVE scan job output and stores the results.
5
6
  class CveScannerJob < Actions::EntryAction
6
7
  def self.subscribe
7
8
  Actions::RemoteExecution::RunHostJob
@@ -15,16 +16,9 @@ module Actions
15
16
 
16
17
  def finalize(*_args)
17
18
  host = Host.find(input[:host_id])
18
- return if host.blank?
19
19
 
20
- report = {
21
- 'host' => host.name,
22
- 'logs' => [],
23
- 'scan' => format_output(task.main_action.continuous_output.humanize),
24
- 'reported_at' => Time.now.utc.to_s,
25
- 'reporter' => 'cve_scan'
26
- }
27
- ConfigReportImporter.import(report)
20
+ ::ForemanCveScanner::ScanImporter.new(task.main_action.continuous_output)
21
+ .import_for_host!(host)
28
22
  end
29
23
 
30
24
  private
@@ -34,15 +28,6 @@ module Actions
34
28
  .first
35
29
  .template_id, label: feature).any?
36
30
  end
37
-
38
- def format_output(job_output)
39
- output = job_output.each_line(chomp: true)
40
- .drop_while { |l| !l.start_with? '===START' }.drop(1)
41
- .take_while { |l| !l.start_with? '===END' }
42
- .reject(&:empty?)
43
- .join('')
44
- JSON.parse(output)
45
- end
46
31
  end
47
32
  end
48
33
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanCveScanner
4
+ # Adds CVE scan associations to hosts.
5
+ module HostExtensions
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ has_many :cve_scans,
10
+ class_name: 'ForemanCveScanner::CveScan',
11
+ foreign_key: :host_id,
12
+ dependent: :destroy,
13
+ inverse_of: :host
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanCveScanner
4
+ # Stores a single CVE scan result for a host.
5
+ class CveScan < ApplicationRecord
6
+ self.table_name = 'foreman_cve_scanner_cve_scans'
7
+
8
+ belongs_to :host, class_name: '::Host::Managed'
9
+
10
+ validates :host_id, :scanner, :raw, :summary, :findings, presence: true
11
+
12
+ scope :for_host, ->(host_id) { where(host_id: host_id) }
13
+ scope :recent_first, -> { order(created_at: :desc, id: :desc) }
14
+ end
15
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HostStatus
4
+ # Host status entry reflecting latest CVE scan severities.
5
+ class CveStatus < Status
6
+ CVE_SCANNER_STATUS_NONE = 0
7
+ CVE_SCANNER_STATUS_LOW = 1
8
+ CVE_SCANNER_STATUS_MEDIUM = 2
9
+ CVE_SCANNER_STATUS_CRITICAL_HIGH = 3
10
+
11
+ def self.status_name
12
+ N_('CVE')
13
+ end
14
+
15
+ def to_label(_options = {})
16
+ case latest_scan_severity
17
+ when :critical_high
18
+ N_('Critical or high CVEs')
19
+ when :medium
20
+ N_('Medium CVEs')
21
+ when :low
22
+ N_('Low CVEs')
23
+ when :none
24
+ N_('No CVE scans')
25
+ else
26
+ N_('No CVEs')
27
+ end
28
+ end
29
+
30
+ def to_global(_options = {})
31
+ case latest_scan_severity
32
+ when :critical_high
33
+ HostStatus::Global::ERROR
34
+ when :medium, :none
35
+ HostStatus::Global::WARN
36
+ else
37
+ HostStatus::Global::OK
38
+ end
39
+ end
40
+
41
+ def to_status(_options = {})
42
+ case latest_scan_severity
43
+ when :critical_high
44
+ CVE_SCANNER_STATUS_CRITICAL_HIGH
45
+ when :medium
46
+ CVE_SCANNER_STATUS_MEDIUM
47
+ when :low
48
+ CVE_SCANNER_STATUS_LOW
49
+ else
50
+ CVE_SCANNER_STATUS_NONE
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def latest_scan
57
+ @latest_scan ||= ::ForemanCveScanner::CveScan.for_host(host_id).recent_first.first
58
+ end
59
+
60
+ def latest_scan_severity
61
+ scan = latest_scan
62
+ return :none if scan.nil?
63
+
64
+ return :critical_high if scan.critical.to_i.positive? || scan.high.to_i.positive?
65
+ return :medium if scan.medium.to_i.positive?
66
+ return :low if scan.low.to_i.positive?
67
+
68
+ :none
69
+ end
70
+ end
71
+ end
@@ -1,17 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanCveScanner
4
- # Scans ConfigReports after import for indicators of an CveScanner report and
5
- # sets the origin of the report to 'CveScanner'
4
+ # Parses raw CVE scanner reports and produces unified logs/metrics.
5
+ # rubocop:disable Metrics/ClassLength
6
6
  class CveReportScanner
7
- def self.add_reporter_data(_report, raw)
8
- scanner = ForemanCveScanner::CveReportScanner.new(raw)
9
- scanner.generate
10
- raw['logs'] = scanner.logs
11
- raw['status'] = scanner.status
12
- raw['metrics'] = scanner.metrics
13
- raw['report_status_calculator_options'] = { :metrics => %w[critical high medium low total] }
14
- end
7
+ SEVERITY_ORDER = %w[CRITICAL HIGH MEDIUM LOW UNKNOWN].freeze
15
8
 
16
9
  def self.identify_origin(raw)
17
10
  'CveScanner' if cve_scanner_report?(raw)
@@ -37,9 +30,21 @@ module ForemanCveScanner
37
30
 
38
31
  attr_reader :logs, :status
39
32
 
33
+ def unified_vulnerabilities
34
+ @cve_report_data
35
+ end
36
+
37
+ def self.detect_scanner(scan_json)
38
+ return 'grype' if scan_json.is_a?(Hash) && scan_json.key?('matches')
39
+ return 'trivy' if scan_json.is_a?(Hash) && scan_json.key?('Results')
40
+
41
+ 'unknown'
42
+ end
43
+
40
44
  def metrics
41
- res = @status
42
- res['total'] = @status.values.sum
45
+ known = %w[critical high medium low]
46
+ res = @status.slice(*known)
47
+ res['total'] = res.values.sum
43
48
  res
44
49
  end
45
50
 
@@ -47,43 +52,38 @@ module ForemanCveScanner
47
52
 
48
53
  def generate_log_from_unified(id, entry)
49
54
  {
50
- 'log': {
51
- 'level': consume_severity_level(entry['severity']),
52
- 'messages': {
53
- message: "#{id}: #{entry['title']} # url: #{entry['url']}"
55
+ log: {
56
+ level: consume_severity_level(entry['severity']),
57
+ messages: {
58
+ message: "#{id}: #{entry['title']} # url: #{entry['url']}",
54
59
  },
55
60
  sources: {
56
- source: "#{entry['name']} @ #{entry['version']}"
57
- }
58
- }
61
+ source: "#{entry['name']} @ #{entry['version']}",
62
+ },
63
+ },
59
64
  }.deep_stringify_keys
60
65
  end
61
66
 
62
67
  def consume_severity_level(severity)
68
+ severity = severity.to_s.strip.upcase
63
69
  @status[severity.downcase] = 0 unless @status.key?(severity.downcase)
64
70
  @status[severity.downcase] += 1
65
71
 
66
- case severity
67
- when 'CRITICAL'
68
- 'err'
69
- when 'HIGH'
70
- 'warning'
71
- when 'MEDIUM'
72
- 'info'
73
- when 'LOW'
74
- 'debug'
75
- else
76
- 'info'
77
- end
72
+ {
73
+ 'CRITICAL' => 'err',
74
+ 'HIGH' => 'warning',
75
+ 'MEDIUM' => 'info',
76
+ 'LOW' => 'debug',
77
+ }.fetch(severity, 'info')
78
78
  end
79
79
 
80
80
  def generate_grype_entry(entry)
81
81
  {
82
82
  'name' => entry['artifact']['name'],
83
83
  'version' => entry['artifact']['version'],
84
- 'title' => entry['vulnerability']['description'].gsub(/[\[\]"\\]/, ''),
84
+ 'title' => entry['vulnerability']['description'].to_s.gsub(/[\[\]"\\]/, ''),
85
85
  'severity' => entry['vulnerability']['severity'],
86
- 'url' => entry['vulnerability']['dataSource']
86
+ 'url' => entry['vulnerability']['dataSource'],
87
87
  }
88
88
  end
89
89
 
@@ -91,17 +91,17 @@ module ForemanCveScanner
91
91
  unified = {
92
92
  'name' => entry['PkgName'],
93
93
  'version' => entry['InstalledVersion'],
94
- 'title' => entry['Title'].gsub(/[\[\]"\\]/, ''),
94
+ 'title' => entry['Title'].to_s.gsub(/[\[\]"\\]/, ''),
95
95
  'severity' => entry['Severity'],
96
96
  'url' => entry['PrimaryURL'],
97
97
  'status' => entry['Status'],
98
- 'fixed' => entry['FixedVersion'] || 'open'
98
+ 'fixed' => entry['FixedVersion'] || 'open',
99
99
  }
100
100
  unified['published'] = entry['PublishedDate'] if entry.key?('PublishedDate')
101
101
  unified
102
102
  end
103
103
 
104
- # rubocop:disable Metrics/AbcSize
104
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
105
  def generate_unified_vuls
106
106
  raise ::Foreman::Exception, _('Invalid CVE scanner report') unless @raw_data.key?('scan')
107
107
 
@@ -114,6 +114,7 @@ module ForemanCveScanner
114
114
  elsif j.key?('Results') # Trivy
115
115
  j['Results'].each do |r|
116
116
  next unless r.key? 'Vulnerabilities'
117
+
117
118
  r['Vulnerabilities'].each do |vul|
118
119
  vuls[vul['VulnerabilityID']] = generate_trivy_entry(vul)
119
120
  end
@@ -125,6 +126,7 @@ module ForemanCveScanner
125
126
 
126
127
  vuls
127
128
  end
128
- # rubocop:enable Metrics/AbcSize
129
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
129
130
  end
131
+ # rubocop:enable Metrics/ClassLength
130
132
  end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanCveScanner
4
+ # Import CVE scan results from REX job output and persist them.
5
+ class ScanImporter
6
+ def initialize(job_output)
7
+ @job_output = job_output
8
+ end
9
+
10
+ def import_for_host!(host)
11
+ scan_json = format_output(@job_output)
12
+ return nil if scan_json.nil?
13
+
14
+ scanner_name = ::ForemanCveScanner::CveReportScanner.detect_scanner(scan_json)
15
+ persist_scan!(host, scanner_name, scan_json)
16
+ end
17
+
18
+ private
19
+
20
+ def format_output(job_output)
21
+ output_source = normalize_job_output(job_output)
22
+ json_text = extract_json(output_source)
23
+ if json_text.blank? && output_source.to_s.strip.present?
24
+ Rails.logger.warn('CVE scan output did not contain markers or JSON content')
25
+ end
26
+ return nil if json_text.blank?
27
+
28
+ JSON.parse(json_text)
29
+ rescue JSON::ParserError => e
30
+ Rails.logger.error("CVE scan output parse failed: #{e}")
31
+ nil
32
+ end
33
+
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
41
+ output.strip
42
+ end
43
+
44
+ def normalize_job_output(job_output)
45
+ return concat_proxy_output(job_output) if job_output.is_a?(Hash) && job_output.key?('proxy_output')
46
+
47
+ output_source = job_output
48
+ output_source = job_output.humanize if job_output.respond_to?(:humanize) && !job_output.is_a?(String)
49
+ output_source.to_s
50
+ end
51
+
52
+ def concat_proxy_output(job_output)
53
+ result = job_output.dig('proxy_output', 'result') || []
54
+ result.filter_map { |item| item['output'] if item['output_type'] == 'stdout' }.join
55
+ end
56
+
57
+ def persist_scan!(host, scanner_name, scan_json)
58
+ scanner = ::ForemanCveScanner::CveReportScanner.new('scan' => scan_json)
59
+ scanner.generate
60
+
61
+ metrics = scanner.metrics
62
+ scan = ::ForemanCveScanner::CveScan.create!(
63
+ build_scan_attributes(host, scanner_name, scan_json, metrics, scanner)
64
+ )
65
+ refresh_host_status(host)
66
+ scan
67
+ end
68
+
69
+ def refresh_host_status(host)
70
+ status = ::HostStatus::CveStatus.find_or_initialize_by(host: host)
71
+ status.refresh!
72
+ rescue StandardError => e
73
+ Rails.logger.error("CVE status refresh failed for host_id=#{host.id}: #{e}")
74
+ end
75
+
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
+ def build_scan_attributes(host, scanner_name, scan_json, metrics, scanner)
84
+ summary = metrics.merge('worst' => worst_severity(metrics))
85
+ {
86
+ host: host,
87
+ scanner: scanner_name,
88
+ raw: scan_json,
89
+ summary: summary,
90
+ findings: build_findings(scanner),
91
+ total: metrics['total'].to_i,
92
+ critical: metrics['critical'].to_i,
93
+ high: metrics['high'].to_i,
94
+ medium: metrics['medium'].to_i,
95
+ low: metrics['low'].to_i,
96
+ }
97
+ end
98
+
99
+ def build_findings(scanner)
100
+ scanner.unified_vulnerabilities.map do |id, entry|
101
+ entry.merge('id' => id)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @cve_scan
4
+
5
+ attributes :id, :host_id, :scanner, :created_at, :total, :critical, :high, :medium, :low, :summary
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ collection @cve_scans
4
+
5
+ extends 'api/v2/cve_scans/base'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @cve_scan
4
+
5
+ extends 'api/v2/cve_scans/main'
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @cve_scan
4
+
5
+ extends 'api/v2/cve_scans/base'
6
+
7
+ attributes :findings, :created_at, :updated_at
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ object @cve_scan
4
+
5
+ extends 'api/v2/cve_scans/main'
@@ -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 %>
@@ -13,6 +13,7 @@ template_inputs:
13
13
  options: "filesystem\r\ndocker"
14
14
  advanced: false
15
15
  value_type: plain
16
+ default: filesystem
16
17
  hidden_value: false
17
18
  - name: options
18
19
  required: false
@@ -25,6 +26,7 @@ template_inputs:
25
26
  input_type: user
26
27
  advanced: false
27
28
  value_type: plain
29
+ default: /
28
30
  hidden_value: false
29
31
  - name: scanner
30
32
  required: true
@@ -39,7 +41,7 @@ template_inputs:
39
41
  scanner = input('scanner')
40
42
  target = input('target').to_sym
41
43
  path = input('path')
42
- options = input('options')
44
+ options = input('options').to_s
43
45
 
44
46
  scanners = {
45
47
  trivy: {
@@ -57,7 +59,7 @@ if scanner == 'trivy'
57
59
  options += " --exit-code 0 --scanners vuln --quiet --format json"
58
60
  elsif scanner == 'grype'
59
61
  cmd = "#{scanners[:grype][target]}:#{path}"
60
- options += " --quiet --quiet --output json"
62
+ options += " --quiet --output json"
61
63
  end
62
64
  exec_command = "#{scanner} #{cmd} #{options}"
63
65
  -%>
data/config/routes.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ Rails.application.routes.draw do
4
+ namespace :api, defaults: { format: 'json' } do
5
+ scope '(:apiv)', module: :v2,
6
+ defaults: { apiv: 'v2' },
7
+ apiv: /v1|v2/,
8
+ constraints: ApiConstraints.new(version: 2, default: true) do
9
+ constraints(host_id: %r{[^/]+}) do
10
+ resources :hosts, only: [] do
11
+ resources :cve_scans, only: %i[index show] do
12
+ collection do
13
+ get :latest
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end