152 lines
6.3 KiB
Ruby
152 lines
6.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "prawn"
|
|
require "prawn-svg"
|
|
|
|
module Gitlab
|
|
module PDF
|
|
module Security
|
|
class GroupVulnerabilitiesHistory
|
|
include Prawn::View
|
|
|
|
# rubocop:disable Layout/LineLength -- data strings
|
|
SEVERITY_ICONS = {
|
|
critical: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#dd2b0e" d="M6.706.185l4.088 2.31c.437.246.706.702.706 1.195v4.62c0 .493-.269.949-.706 1.195l-4.088 2.31a1.438 1.438 0 0 1-1.412 0l-4.088-2.31A1.376 1.376 0 0 1 .5 8.31V3.69c0-.493.269-.949.706-1.195L5.294.185a1.438 1.438 0 0 1 1.412 0z"/></svg>',
|
|
high: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#dd2b0e" d="M6.713.295l4.992 4.992a1.008 1.008 0 0 1 0 1.426l-4.992 4.992a1.008 1.008 0 0 1-1.426 0L.295 6.713a1.008 1.008 0 0 1 0-1.426L5.287.295a1.008 1.008 0 0 1 1.426 0z"/></svg>',
|
|
medium: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#fc9403" d="M5.04 10.454L.24 3.182c-.398-.603-.29-1.457.24-1.91C.688 1.097.94 1 1.2 1h9.6c.663 0 1.2.61 1.2 1.364 0 .295-.084.582-.24.818l-4.8 7.272c-.398.603-1.15.725-1.68.273a1.295 1.295 0 0 1-.24-.273z"/></svg>',
|
|
low: '<svg width="14" height="14" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><path d="m6.85,12.6a6.05,5.8 0 1 1 0,-11.6a6.05,5.8 0 0 1 0,11.6z" fill="#fc9403" id="severity_low"/></svg>',
|
|
info: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#428fdc" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zm.75-12.25a.75.75 0 1 0-1.5 0 .75.75 0 0 0 1.5 0zM8 5.25a.75.75 0 0 0-.75.75v4.5a.75.75 0 1 0 1.5 0V6A.75.75 0 0 0 8 5.25z"/></svg>',
|
|
unknown: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill="#868686" d="M8 16A8 8 0 1 1 8 0a8 8 0 0 1 0 16zM4.927 4.99c-.285.429-.427.853-.427 1.27 0 .203.09.392.27.566.18.174.4.26.661.26.443 0 .744-.248.903-.746.168-.475.373-.835.616-1.08.243-.244.62-.366 1.134-.366.439 0 .797.12 1.075.363.277.242.416.54.416.892a.97.97 0 0 1-.136.502 1.91 1.91 0 0 1-.336.419 14.35 14.35 0 0 1-.648.558c-.34.282-.611.525-.812.73-.2.205-.362.443-.483.713-.322 1.245 1.35 1.345 1.736.456.047-.086.118-.18.213-.284.096-.103.223-.223.382-.36a41.14 41.14 0 0 0 1.194-1.034c.221-.204.412-.448.573-.73a1.95 1.95 0 0 0 .242-.984c0-.475-.141-.915-.424-1.32-.282-.406-.682-.726-1.2-.962-.518-.235-1.115-.353-1.792-.353-.728 0-1.365.14-1.911.423-.546.282-.961.637-1.246 1.066zm2.14 7.08a1 1 0 1 0 2 0 1 1 0 0 0-2 0z"/></svg>'
|
|
}.freeze
|
|
# rubocop:enable Layout/LineLength
|
|
|
|
def self.render(pdf, data: {})
|
|
new(pdf, data).render
|
|
end
|
|
|
|
def initialize(pdf, data)
|
|
@pdf = pdf
|
|
@charts = process_raw(data)
|
|
@selected_day_range = data&.dig(:selected_day_range)
|
|
@date_info = data&.dig(:date_info)
|
|
@width = 500
|
|
@height = 400
|
|
@icon_size = 14
|
|
@y = pdf.cursor
|
|
end
|
|
|
|
def render
|
|
return :noop if @charts.blank?
|
|
|
|
@pdf.bounding_box([0, @y], width: @pdf.bounds.right, height: @height) do
|
|
# Background
|
|
@pdf.save_graphics_state
|
|
@pdf.fill_color "F9F9F9"
|
|
@pdf.fill_rectangle [0, @pdf.bounds.top], @pdf.bounds.right, @height
|
|
@pdf.restore_graphics_state
|
|
|
|
# Title
|
|
@pdf.text_box(
|
|
s_('Vulnerability History'),
|
|
at: [0, @pdf.bounds.top - 10],
|
|
width: @pdf.bounds.right,
|
|
align: :center,
|
|
style: :bold,
|
|
size: 16
|
|
)
|
|
|
|
# Date information
|
|
@pdf.text_box(
|
|
"#{@date_info} (#{@selected_day_range} #{s_('Days')})",
|
|
at: [0, @pdf.bounds.top - 40],
|
|
width: @pdf.bounds.right,
|
|
align: :center,
|
|
size: 12
|
|
)
|
|
|
|
# the info table
|
|
draw_table_header
|
|
@charts.each_with_index do |chart, index|
|
|
draw_chart_row(chart, index)
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def draw_table_header
|
|
@pdf.save_graphics_state
|
|
y_position = @pdf.bounds.top - 80
|
|
|
|
@pdf.bounding_box([10, y_position - 5], width: 100, height: 20) do
|
|
@pdf.text "Severity", style: :bold, size: 12
|
|
end
|
|
|
|
@pdf.bounding_box([@pdf.bounds.right - 150, y_position - 5], width: 50, height: 20) do
|
|
@pdf.text "%", style: :bold, size: 12, align: :center
|
|
end
|
|
|
|
@pdf.bounding_box([@pdf.bounds.right - 80, y_position - 5], width: 50, height: 20) do
|
|
@pdf.text "#", style: :bold, size: 12, align: :center
|
|
end
|
|
|
|
@pdf.stroke_color "000000"
|
|
@pdf.stroke_horizontal_line 0, @pdf.bounds.right, at: y_position - 25
|
|
|
|
@chart_start_position = y_position - 35
|
|
@pdf.restore_graphics_state
|
|
end
|
|
|
|
def draw_chart_row(chart, index)
|
|
row_height = 70
|
|
y_position = @chart_start_position - (index * row_height)
|
|
|
|
icon_svg = SEVERITY_ICONS[chart[:severity].to_sym]
|
|
@pdf.svg(icon_svg, at: [10, @pdf.cursor - 25], width: @icon_size, height: @icon_size)
|
|
|
|
@pdf.text_box(
|
|
chart[:severity].capitalize,
|
|
at: [30, @pdf.cursor + 12],
|
|
width: 100
|
|
)
|
|
|
|
svg_width = @pdf.bounds.right * 0.5
|
|
@pdf.svg chart[:svg], at: [100, y_position - 15], width: svg_width, height: 70
|
|
|
|
@pdf.text_box(
|
|
chart[:change_in_percent],
|
|
at: [@pdf.bounds.right - 150, y_position - 15],
|
|
width: 50,
|
|
height: 20,
|
|
align: :center,
|
|
style: :bold,
|
|
size: 12
|
|
)
|
|
|
|
@pdf.text_box(
|
|
chart[:current_count].to_s,
|
|
at: [@pdf.bounds.right - 80, y_position - 15],
|
|
width: 50,
|
|
height: 20,
|
|
align: :center,
|
|
style: :bold,
|
|
size: 12
|
|
)
|
|
end
|
|
|
|
def process_raw(data)
|
|
charts = data.present? ? data[:charts] : []
|
|
return if charts.blank?
|
|
|
|
charts.each do |chart|
|
|
svg = CGI.unescape(chart[:svg]).delete!("\n")[%r{(<svg.*</svg>)}, 1]
|
|
chart[:svg] = svg
|
|
end
|
|
|
|
charts
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|