Commit 56fcaba4 authored by James Johnson's avatar James Johnson Committed by Dmitry Gruzd

Improve Vulnerability Tracking: Use Signatures [RUN ALL RSPEC] [RUN AS-IF-FOSS]

parent d61c5521
# frozen_string_literal: true
module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
end
VulnerabilityFindingHelpers.prepend_if_ee('EE::VulnerabilityFindingHelpers')
# frozen_string_literal: true
module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
end
VulnerabilityFindingSignatureHelpers.prepend_if_ee('EE::VulnerabilityFindingSignatureHelpers')
...@@ -47,10 +47,13 @@ module Security ...@@ -47,10 +47,13 @@ module Security
report_finding = report_finding_for(security_finding) report_finding = report_finding_for(security_finding)
return Vulnerabilities::Finding.new unless report_finding return Vulnerabilities::Finding.new unless report_finding
finding_data = report_finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :links) finding_data = report_finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :links, :signatures)
identifiers = report_finding.identifiers.map do |identifier| identifiers = report_finding.identifiers.map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash) Vulnerabilities::Identifier.new(identifier.to_hash)
end end
signatures = report_finding.signatures.map do |signature|
Vulnerabilities::FindingSignature.new(signature.to_hash)
end
Vulnerabilities::Finding.new(finding_data).tap do |finding| Vulnerabilities::Finding.new(finding_data).tap do |finding|
finding.location_fingerprint = report_finding.location.fingerprint finding.location_fingerprint = report_finding.location.fingerprint
...@@ -59,6 +62,7 @@ module Security ...@@ -59,6 +62,7 @@ module Security
finding.sha = pipeline.sha finding.sha = pipeline.sha
finding.scanner = security_finding.scanner finding.scanner = security_finding.scanner
finding.identifiers = identifiers finding.identifiers = identifiers
finding.signatures = signatures
end end
end end
......
...@@ -75,7 +75,7 @@ module Security ...@@ -75,7 +75,7 @@ module Security
def normalize_report_findings(report_findings, vulnerabilities) def normalize_report_findings(report_findings, vulnerabilities)
report_findings.map do |report_finding| report_findings.map do |report_finding|
finding_hash = report_finding.to_hash finding_hash = report_finding.to_hash
.except(:compare_key, :identifiers, :location, :scanner, :links) .except(:compare_key, :identifiers, :location, :scanner, :links, :signatures)
finding = Vulnerabilities::Finding.new(finding_hash) finding = Vulnerabilities::Finding.new(finding_hash)
# assigning Vulnerabilities to Findings to enable the computed state # assigning Vulnerabilities to Findings to enable the computed state
...@@ -90,6 +90,9 @@ module Security ...@@ -90,6 +90,9 @@ module Security
finding.identifiers = report_finding.identifiers.map do |identifier| finding.identifiers = report_finding.identifiers.map do |identifier|
Vulnerabilities::Identifier.new(identifier.to_hash) Vulnerabilities::Identifier.new(identifier.to_hash)
end end
finding.signatures = report_finding.signatures.map do |signature|
Vulnerabilities::FindingSignature.new(signature.to_hash)
end
finding finding
end end
...@@ -111,18 +114,36 @@ module Security ...@@ -111,18 +114,36 @@ module Security
end end
def dismissal_feedback?(finding) def dismissal_feedback?(finding)
dismissal_feedback_by_fingerprint[finding.project_fingerprint] if ::Feature.enabled?(:vulnerability_finding_signatures, pipeline.project) && !finding.signatures.empty?
dismissal_feedback_by_finding_signatures(finding)
else
dismissal_feedback_by_project_fingerprint(finding)
end
end end
def dismissal_feedback_by_fingerprint def all_dismissal_feedbacks
strong_memoize(:dismissal_feedback_by_fingerprint) do strong_memoize(:all_dismissal_feedbacks) do
pipeline.project pipeline.project
.vulnerability_feedback .vulnerability_feedback
.for_dismissal .for_dismissal
.group_by(&:project_fingerprint)
end end
end end
def dismissal_feedback_by_finding_signatures(finding)
potential_uuids = Set.new([*finding.signature_uuids, finding.uuid].compact)
all_dismissal_feedbacks.any? { |dismissal| potential_uuids.include?(dismissal.finding_uuid) }
end
def dismissal_feedback_by_fingerprint
strong_memoize(:dismissal_feedback_by_fingerprint) do
all_dismissal_feedbacks.group_by(&:project_fingerprint)
end
end
def dismissal_feedback_by_project_fingerprint(finding)
dismissal_feedback_by_fingerprint[finding.project_fingerprint]
end
def confidence_levels def confidence_levels
Array(params.fetch(:confidence, Vulnerabilities::Finding.confidences.keys)) Array(params.fetch(:confidence, Vulnerabilities::Finding.confidences.keys))
end end
......
# frozen_string_literal: true
module EE
module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
def matches_signatures(other_signatures, other_uuid)
other_signature_types = other_signatures.index_by(&:algorithm_type)
# highest first
match_result = nil
signatures.sort_by(&:priority).reverse_each do |signature|
matching_other_signature = other_signature_types[signature.algorithm_type]
next if matching_other_signature.nil?
match_result = matching_other_signature == signature
break
end
if match_result.nil?
[uuid, *signature_uuids].include?(other_uuid)
else
match_result
end
end
def signature_uuids
signatures.map do |signature|
hex_sha = signature.signature_hex
::Security::VulnerabilityUUID.generate(
report_type: report_type,
location_fingerprint: hex_sha,
primary_identifier_fingerprint: primary_identifier&.fingerprint,
project_id: project_id
)
end
end
end
end
# frozen_string_literal: true
module EE
module VulnerabilityFindingSignatureHelpers
extend ActiveSupport::Concern
# If the location object describes a physical location within a file
# (filename + line numbers), the 'location' algorithm_type should be used
#
# If the location object describes arbitrary data, then the 'hash'
# algorithm_type should be used.
PRIORITIES = {
scope_offset: 3,
location: 2,
hash: 1
}.with_indifferent_access.freeze
class_methods do
def priority(algorithm_type)
raise ArgumentError.new("No priority for #{algorithm_type.inspect}") unless PRIORITIES.key?(algorithm_type)
PRIORITIES[algorithm_type]
end
end
def priority
self.class.priority(algorithm_type)
end
end
end
...@@ -180,6 +180,7 @@ class License < ApplicationRecord ...@@ -180,6 +180,7 @@ class License < ApplicationRecord
subepics subepics
threat_monitoring threat_monitoring
vulnerability_auto_fix vulnerability_auto_fix
vulnerability_finding_signatures
] ]
EEU_FEATURES.freeze EEU_FEATURES.freeze
......
...@@ -5,6 +5,7 @@ module Vulnerabilities ...@@ -5,6 +5,7 @@ module Vulnerabilities
include ShaAttribute include ShaAttribute
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
include Presentable include Presentable
include ::VulnerabilityFindingHelpers
# https://gitlab.com/groups/gitlab-org/-/epics/3148 # https://gitlab.com/groups/gitlab-org/-/epics/3148
# https://gitlab.com/gitlab-org/gitlab/-/issues/214563#note_370782508 is why the table names are not renamed # https://gitlab.com/gitlab-org/gitlab/-/issues/214563#note_370782508 is why the table names are not renamed
...@@ -332,12 +333,16 @@ module Vulnerabilities ...@@ -332,12 +333,16 @@ module Vulnerabilities
end end
end end
alias_method :==, :eql? # eql? is necessary in some cases like array intersection alias_method :==, :eql?
def eql?(other) def eql?(other)
other.report_type == report_type && return false unless other.report_type == report_type && other.primary_identifier_fingerprint == primary_identifier_fingerprint
other.location_fingerprint == location_fingerprint &&
other.first_fingerprint == first_fingerprint if ::Feature.enabled?(:vulnerability_finding_signatures, project)
matches_signatures(other.signatures, other.uuid)
else
other.location_fingerprint == location_fingerprint
end
end end
# Array.difference (-) method uses hash and eql? methods to do comparison # Array.difference (-) method uses hash and eql? methods to do comparison
...@@ -348,7 +353,7 @@ module Vulnerabilities ...@@ -348,7 +353,7 @@ module Vulnerabilities
# when Finding is persisted and identifiers are not preloaded. # when Finding is persisted and identifiers are not preloaded.
return super if persisted? && !identifiers.loaded? return super if persisted? && !identifiers.loaded?
report_type.hash ^ location_fingerprint.hash ^ first_fingerprint.hash report_type.hash ^ location_fingerprint.hash ^ primary_identifier_fingerprint.hash
end end
def severity_value def severity_value
...@@ -380,7 +385,7 @@ module Vulnerabilities ...@@ -380,7 +385,7 @@ module Vulnerabilities
protected protected
def first_fingerprint def primary_identifier_fingerprint
identifiers.first&.fingerprint identifiers.first&.fingerprint
end end
......
...@@ -5,11 +5,23 @@ module Vulnerabilities ...@@ -5,11 +5,23 @@ module Vulnerabilities
self.table_name = 'vulnerability_finding_signatures' self.table_name = 'vulnerability_finding_signatures'
include BulkInsertSafe include BulkInsertSafe
include VulnerabilityFindingSignatureHelpers
belongs_to :finding, foreign_key: 'finding_id', inverse_of: :signatures, class_name: 'Vulnerabilities::Finding' belongs_to :finding, foreign_key: 'finding_id', inverse_of: :signatures, class_name: 'Vulnerabilities::Finding'
enum algorithm_type: { hash: 1, location: 2, scope_offset: 3 }, _prefix: :algorithm enum algorithm_type: { hash: 1, location: 2, scope_offset: 3 }, _prefix: :algorithm
validates :finding, presence: true validates :finding, presence: true
def signature_hex
signature_sha.unpack1("H*")
end
def eql?(other)
other.algorithm_type == algorithm_type &&
other.signature_sha == signature_sha
end
alias_method :==, :eql?
end end
end end
...@@ -61,17 +61,21 @@ module Security ...@@ -61,17 +61,21 @@ module Security
return return
end end
vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan, :links) vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan, :links, :signatures)
entity_params = Gitlab::Json.parse(vulnerability_params&.dig(:raw_metadata)).slice('description', 'message', 'solution', 'cve', 'location') entity_params = Gitlab::Json.parse(vulnerability_params&.dig(:raw_metadata)).slice('description', 'message', 'solution', 'cve', 'location')
# Vulnerabilities::Finding (`vulnerability_occurrences`) # Vulnerabilities::Finding (`vulnerability_occurrences`)
vulnerability_finding = vulnerability_findings_by_uuid[finding.uuid] || vulnerability_finding = vulnerability_findings_by_uuid[finding.uuid] ||
create_new_vulnerability_finding(finding, vulnerability_params.merge(entity_params)) find_or_create_vulnerability_finding(finding, vulnerability_params.merge(entity_params))
update_vulnerability_scanner(finding) unless Feature.enabled?(:optimize_sql_query_for_security_report, project) update_vulnerability_scanner(finding) unless Feature.enabled?(:optimize_sql_query_for_security_report, project)
update_vulnerability_finding(vulnerability_finding, vulnerability_params) update_vulnerability_finding(vulnerability_finding, vulnerability_params)
reset_remediations_for(vulnerability_finding, finding) reset_remediations_for(vulnerability_finding, finding)
if ::Feature.enabled?(:vulnerability_finding_signatures, project)
update_feedbacks(vulnerability_finding, vulnerability_params[:uuid])
update_finding_signatures(finding, vulnerability_finding) update_finding_signatures(finding, vulnerability_finding)
end
# The maximum number of identifiers is not used in validation # The maximum number of identifiers is not used in validation
# we just want to ignore the rest if a finding has more than that. # we just want to ignore the rest if a finding has more than that.
...@@ -86,8 +90,16 @@ module Security ...@@ -86,8 +90,16 @@ module Security
create_vulnerability(vulnerability_finding, pipeline) create_vulnerability(vulnerability_finding, pipeline)
end end
def find_or_create_vulnerability_finding(finding, create_params)
if ::Feature.enabled?(:vulnerability_finding_signatures, project)
find_or_create_vulnerability_finding_with_signatures(finding, create_params)
else
find_or_create_vulnerability_finding_with_location(finding, create_params)
end
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def create_new_vulnerability_finding(finding, create_params) def find_or_create_vulnerability_finding_with_location(finding, create_params)
find_params = { find_params = {
scanner: scanners_objects[finding.scanner.key], scanner: scanners_objects[finding.scanner.key],
primary_identifier: identifiers_objects[finding.primary_identifier.key], primary_identifier: identifiers_objects[finding.primary_identifier.key],
...@@ -120,6 +132,56 @@ module Security ...@@ -120,6 +132,56 @@ module Security
end end
end end
def get_matched_findings(finding, normalized_signatures, find_params)
project.vulnerability_findings.where(**find_params).filter do |vf|
vf.matches_signatures(normalized_signatures, finding.uuid)
end
end
def find_or_create_vulnerability_finding_with_signatures(finding, create_params)
find_params = {
# this isn't taking prioritization into account (happens in the filter
# block below), but it *does* limit the number of findings we have to sift through
location_fingerprint: [finding.location.fingerprint, *finding.signatures.map(&:signature_hex)],
scanner: scanners_objects[finding.scanner.key],
primary_identifier: identifiers_objects[finding.primary_identifier.key]
}
normalized_signatures = finding.signatures.map do |signature|
::Vulnerabilities::FindingSignature.new(signature.to_hash)
end
matched_findings = get_matched_findings(finding, normalized_signatures, find_params)
begin
vulnerability_finding = matched_findings.first
if vulnerability_finding.nil?
find_params[:uuid] = finding.uuid
vulnerability_finding = project
.vulnerability_findings
.create_with(create_params)
.find_or_initialize_by(find_params)
end
vulnerability_finding.uuid = finding.uuid
vulnerability_finding.location_fingerprint = if finding.signatures.empty?
finding.location.fingerprint
else
finding.signatures.max_by(&:priority).signature_hex
end
vulnerability_finding.location = create_params.dig(:location)
vulnerability_finding.save!
vulnerability_finding
rescue ActiveRecord::RecordNotUnique
get_matched_findings(finding, normalized_signatures, find_params).first
rescue ActiveRecord::RecordInvalid => e
Gitlab::ErrorTracking.track_and_raise_exception(e, create_params: create_params&.dig(:raw_metadata))
end
end
def update_vulnerability_scanner(finding) def update_vulnerability_scanner(finding)
scanner = scanners_objects[finding.scanner.key] scanner = scanners_objects[finding.scanner.key]
scanner.update!(finding.scanner.to_hash) scanner.update!(finding.scanner.to_hash)
...@@ -223,6 +285,14 @@ module Security ...@@ -223,6 +285,14 @@ module Security
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def update_feedbacks(vulnerability_finding, new_uuid)
vulnerability_finding.load_feedback.each do |feedback|
feedback.finding_uuid = new_uuid
feedback.vulnerability_data = vulnerability_finding.raw_metadata
feedback.save!
end
end
def update_finding_signatures(finding, vulnerability_finding) def update_finding_signatures(finding, vulnerability_finding)
to_update = {} to_update = {}
to_create = [] to_create = []
...@@ -240,12 +310,12 @@ module Security ...@@ -240,12 +310,12 @@ module Security
next if poro_signature.nil? next if poro_signature.nil?
poro_signatures.delete(signature.algorithm_type) poro_signatures.delete(signature.algorithm_type)
to_update[signature.id] = poro_signature.to_h to_update[signature.id] = poro_signature.to_hash
end end
# any remaining poro signatures left are new # any remaining poro signatures left are new
poro_signatures.values.each do |poro_signature| poro_signatures.values.each do |poro_signature|
attributes = poro_signature.to_h.merge(finding_id: vulnerability_finding.id) attributes = poro_signature.to_hash.merge(finding_id: vulnerability_finding.id)
to_create << ::Vulnerabilities::FindingSignature.new(attributes: attributes, created_at: Time.zone.now, updated_at: Time.zone.now) to_create << ::Vulnerabilities::FindingSignature.new(attributes: attributes, created_at: Time.zone.now, updated_at: Time.zone.now)
end end
......
---
name: vulnerability_finding_signatures
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54608
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322044
milestone: '13.11'
type: development
group: group::vulnerability research
default_enabled: false
...@@ -7,13 +7,14 @@ module Gitlab ...@@ -7,13 +7,14 @@ module Gitlab
class Common class Common
SecurityReportParserError = Class.new(Gitlab::Ci::Parsers::ParserError) SecurityReportParserError = Class.new(Gitlab::Ci::Parsers::ParserError)
def self.parse!(json_data, report) def self.parse!(json_data, report, vulnerability_finding_signatures_enabled = false)
new(json_data, report).parse! new(json_data, report, vulnerability_finding_signatures_enabled).parse!
end end
def initialize(json_data, report) def initialize(json_data, report, vulnerability_finding_signatures_enabled = false)
@json_data = json_data @json_data = json_data
@report = report @report = report
@vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled
end end
def parse! def parse!
...@@ -80,11 +81,20 @@ module Gitlab ...@@ -80,11 +81,20 @@ module Gitlab
links = create_links(data['links']) links = create_links(data['links'])
location = create_location(data['location'] || {}) location = create_location(data['location'] || {})
remediations = create_remediations(data['remediations']) remediations = create_remediations(data['remediations'])
signatures = create_signatures(tracking_data(data)) signatures = create_signatures(location, tracking_data(data))
if @vulnerability_finding_signatures_enabled && !signatures.empty?
# NOT the signature_sha - the compare key is hashed
# to create the project_fingerprint
highest_priority_signature = signatures.max_by(&:priority)
uuid = calculate_uuid_v5(identifiers.first, highest_priority_signature.signature_hex)
else
uuid = calculate_uuid_v5(identifiers.first, location&.fingerprint)
end
report.add_finding( report.add_finding(
::Gitlab::Ci::Reports::Security::Finding.new( ::Gitlab::Ci::Reports::Security::Finding.new(
uuid: calculate_uuid_v5(identifiers.first, location), uuid: uuid,
report_type: report.type, report_type: report.type,
name: finding_name(data, identifiers, location), name: finding_name(data, identifiers, location),
compare_key: data['cve'] || '', compare_key: data['cve'] || '',
...@@ -99,14 +109,17 @@ module Gitlab ...@@ -99,14 +109,17 @@ module Gitlab
raw_metadata: data.to_json, raw_metadata: data.to_json,
metadata_version: report_version, metadata_version: report_version,
details: data['details'] || {}, details: data['details'] || {},
signatures: signatures)) signatures: signatures,
project_id: report.project_id,
vulnerability_finding_signatures_enabled: @vulnerability_finding_signatures_enabled))
end end
def create_signatures(data) def create_signatures(location, tracking)
return [] if data.nil? || data['items'].nil? tracking ||= { 'items' => [] }
signature_algorithms = Hash.new { |hash, key| hash[key] = [] } signature_algorithms = Hash.new { |hash, key| hash[key] = [] }
data['items'].each do |item|
tracking['items'].each do |item|
next unless item.key?('signatures') next unless item.key?('signatures')
item['signatures'].each do |signature| item['signatures'].each do |signature|
...@@ -117,14 +130,16 @@ module Gitlab ...@@ -117,14 +130,16 @@ module Gitlab
signature_algorithms.map do |algorithm, values| signature_algorithms.map do |algorithm, values|
value = values.join('|') value = values.join('|')
begin
signature = ::Gitlab::Ci::Reports::Security::FindingSignature.new( signature = ::Gitlab::Ci::Reports::Security::FindingSignature.new(
algorithm_type: algorithm, algorithm_type: algorithm,
signature_value: value signature_value: value
) )
signature.valid? ? signature : nil
rescue ArgumentError => e if signature.valid?
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) signature
else
e = SecurityReportParserError.new("Vulnerability tracking signature is not valid: #{signature}")
Gitlab::ErrorTracking.track_exception(e)
nil nil
end end
end.compact end.compact
...@@ -201,11 +216,11 @@ module Gitlab ...@@ -201,11 +216,11 @@ module Gitlab
"#{identifier.name} in #{location&.fingerprint_path}" "#{identifier.name} in #{location&.fingerprint_path}"
end end
def calculate_uuid_v5(primary_identifier, location) def calculate_uuid_v5(primary_identifier, location_fingerprint)
uuid_v5_name_components = { uuid_v5_name_components = {
report_type: report.type, report_type: report.type,
primary_identifier_fingerprint: primary_identifier&.fingerprint, primary_identifier_fingerprint: primary_identifier&.fingerprint,
location_fingerprint: location&.fingerprint, location_fingerprint: location_fingerprint,
project_id: report.project_id project_id: report.project_id
} }
......
...@@ -5,6 +5,8 @@ module Gitlab ...@@ -5,6 +5,8 @@ module Gitlab
module Reports module Reports
module Security module Security
class Finding class Finding
include ::VulnerabilityFindingHelpers
UNSAFE_SEVERITIES = %w[unknown high critical].freeze UNSAFE_SEVERITIES = %w[unknown high critical].freeze
attr_reader :compare_key attr_reader :compare_key
...@@ -25,10 +27,11 @@ module Gitlab ...@@ -25,10 +27,11 @@ module Gitlab
attr_reader :remediations attr_reader :remediations
attr_reader :details attr_reader :details
attr_reader :signatures attr_reader :signatures
attr_reader :project_id
delegate :file_path, :start_line, :end_line, to: :location delegate :file_path, :start_line, :end_line, to: :location
def initialize(compare_key:, identifiers:, links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: []) # rubocop:disable Metrics/ParameterLists def initialize(compare_key:, identifiers:, links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists
@compare_key = compare_key @compare_key = compare_key
@confidence = confidence @confidence = confidence
@identifiers = identifiers @identifiers = identifiers
...@@ -45,6 +48,8 @@ module Gitlab ...@@ -45,6 +48,8 @@ module Gitlab
@remediations = remediations @remediations = remediations
@details = details @details = details
@signatures = signatures @signatures = signatures
@project_id = project_id
@vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled
@project_fingerprint = generate_project_fingerprint @project_fingerprint = generate_project_fingerprint
end end
...@@ -66,6 +71,7 @@ module Gitlab ...@@ -66,6 +71,7 @@ module Gitlab
severity severity
uuid uuid
details details
signatures
].each_with_object({}) do |key, hash| ].each_with_object({}) do |key, hash|
hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
end end
...@@ -85,13 +91,22 @@ module Gitlab ...@@ -85,13 +91,22 @@ module Gitlab
end end
def eql?(other) def eql?(other)
report_type == other.report_type && return false unless report_type == other.report_type && primary_identifier_fingerprint == other.primary_identifier_fingerprint
location.fingerprint == other.location.fingerprint &&
primary_fingerprint == other.primary_fingerprint if @vulnerability_finding_signatures_enabled
matches_signatures(other.signatures, other.uuid)
else
location.fingerprint == other.location.fingerprint
end
end end
def hash def hash
report_type.hash ^ location.fingerprint.hash ^ primary_fingerprint.hash if @vulnerability_finding_signatures_enabled && !signatures.empty?
highest_signature = signatures.max_by(&:priority)
report_type.hash ^ highest_signature.signature_hex.hash ^ primary_identifier_fingerprint.hash
else
report_type.hash ^ location.fingerprint.hash ^ primary_identifier_fingerprint.hash
end
end end
def valid? def valid?
...@@ -104,7 +119,7 @@ module Gitlab ...@@ -104,7 +119,7 @@ module Gitlab
end end
end end
def primary_fingerprint def primary_identifier_fingerprint
primary_identifier&.fingerprint primary_identifier&.fingerprint
end end
......
...@@ -5,6 +5,8 @@ module Gitlab ...@@ -5,6 +5,8 @@ module Gitlab
module Reports module Reports
module Security module Security
class FindingSignature class FindingSignature
include VulnerabilityFindingSignatureHelpers
attr_accessor :algorithm_type, :signature_value attr_accessor :algorithm_type, :signature_value
def initialize(params = {}) def initialize(params = {})
...@@ -12,11 +14,19 @@ module Gitlab ...@@ -12,11 +14,19 @@ module Gitlab
@signature_value = params.dig(:signature_value) @signature_value = params.dig(:signature_value)
end end
def priority
::Vulnerabilities::FindingSignature.priority(algorithm_type)
end
def signature_sha def signature_sha
Digest::SHA1.digest(signature_value) Digest::SHA1.digest(signature_value)
end end
def to_h def signature_hex
signature_sha.unpack1("H*")
end
def to_hash
{ {
algorithm_type: algorithm_type, algorithm_type: algorithm_type,
signature_sha: signature_sha signature_sha: signature_sha
...@@ -26,6 +36,13 @@ module Gitlab ...@@ -26,6 +36,13 @@ module Gitlab
def valid? def valid?
::Vulnerabilities::FindingSignature.algorithm_types.key?(algorithm_type) ::Vulnerabilities::FindingSignature.algorithm_types.key?(algorithm_type)
end end
def eql?(other)
other.algorithm_type == algorithm_type &&
other.signature_sha == signature_sha
end
alias_method :==, :eql?
end end
end end
end end
......
...@@ -18,12 +18,12 @@ module Gitlab ...@@ -18,12 +18,12 @@ module Gitlab
@package_version = package_version @package_version = package_version
end end
private
def fingerprint_data def fingerprint_data
"#{docker_image_name_without_tag}:#{package_name}" "#{docker_image_name_without_tag}:#{package_name}"
end end
private
def docker_image_name_without_tag def docker_image_name_without_tag
base_name, version = image.split(':') base_name, version = image.split(':')
......
...@@ -16,8 +16,6 @@ module Gitlab ...@@ -16,8 +16,6 @@ module Gitlab
@crash_state = crash_state @crash_state = crash_state
end end
private
def fingerprint_data def fingerprint_data
"#{crash_type}:#{crash_state}" "#{crash_type}:#{crash_state}"
end end
......
...@@ -20,8 +20,6 @@ module Gitlab ...@@ -20,8 +20,6 @@ module Gitlab
alias_method :fingerprint_path, :path alias_method :fingerprint_path, :path
private
def fingerprint_data def fingerprint_data
"#{path}:#{method_name}:#{param}" "#{path}:#{method_name}:#{param}"
end end
......
...@@ -18,8 +18,6 @@ module Gitlab ...@@ -18,8 +18,6 @@ module Gitlab
@package_version = package_version @package_version = package_version
end end
private
def fingerprint_data def fingerprint_data
"#{file_path}:#{package_name}" "#{file_path}:#{package_name}"
end end
......
...@@ -22,8 +22,6 @@ module Gitlab ...@@ -22,8 +22,6 @@ module Gitlab
@start_line = start_line @start_line = start_line
end end
private
def fingerprint_data def fingerprint_data
"#{file_path}:#{start_line}:#{end_line}" "#{file_path}:#{start_line}:#{end_line}"
end end
......
...@@ -22,8 +22,6 @@ module Gitlab ...@@ -22,8 +22,6 @@ module Gitlab
@start_line = start_line @start_line = start_line
end end
private
def fingerprint_data def fingerprint_data
"#{file_path}:#{start_line}:#{end_line}" "#{file_path}:#{start_line}:#{end_line}"
end end
......
...@@ -7,13 +7,17 @@ module Gitlab ...@@ -7,13 +7,17 @@ module Gitlab
class VulnerabilityReportsComparer class VulnerabilityReportsComparer
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :base_report, :head_report attr_reader :base_report, :head_report, :added, :fixed
ACCEPTABLE_REPORT_AGE = 1.week ACCEPTABLE_REPORT_AGE = 1.week
def initialize(base_report, head_report) def initialize(base_report, head_report)
@base_report = base_report @base_report = base_report
@head_report = head_report @head_report = head_report
@added = []
@fixed = []
calculate_changes
end end
def base_report_created_at def base_report_created_at
...@@ -30,16 +34,30 @@ module Gitlab ...@@ -30,16 +34,30 @@ module Gitlab
ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at
end end
def added def calculate_changes
strong_memoize(:added) do base_findings = base_report.findings
head_report.findings - base_report.findings head_findings = head_report.findings
end
head_findings_hash = head_findings.index_by(&:object_id)
# This is slow - O(N^2). If we didn't need to worry about one high
# priority fingerprint that doesn't match overruling a lower
# priority fingerprint that does match, we'd be able to do some
# set operations here
base_findings.each do |base_finding|
still_exists = false
head_findings.each do |head_finding|
next unless base_finding.eql?(head_finding)
still_exists = true
head_findings_hash.delete(head_finding.object_id)
break
end end
def fixed @fixed << base_finding unless still_exists
strong_memoize(:fixed) do
base_report.findings - head_report.findings
end end
@added = head_findings_hash.values
end end
end end
end end
......
...@@ -39,6 +39,7 @@ FactoryBot.define do ...@@ -39,6 +39,7 @@ FactoryBot.define do
project_id: n project_id: n
) )
end end
vulnerability_finding_signatures_enabled { false }
skip_create skip_create
......
...@@ -17,6 +17,7 @@ FactoryBot.define do ...@@ -17,6 +17,7 @@ FactoryBot.define do
category { 'sast' } category { 'sast' }
project_fingerprint { generate(:project_fingerprint) } project_fingerprint { generate(:project_fingerprint) }
vulnerability_data { { category: 'sast' } } vulnerability_data { { category: 'sast' } }
finding_uuid { Gitlab::UUID.v5(SecureRandom.hex) }
trait :dismissal do trait :dismissal do
feedback_type { 'dismissal' } feedback_type { 'dismissal' }
......
...@@ -83,12 +83,12 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -83,12 +83,12 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
# use the same number of queries, regardless of the number of findings # use the same number of queries, regardless of the number of findings
# contained in the pipeline report. # contained in the pipeline report.
sast_findings = pipeline.security_reports.reports['sast'].findings container_scanning_findings = pipeline.security_reports.reports['container_scanning'].findings
dep_findings = pipeline.security_reports.reports['dependency_scanning'].findings dep_findings = pipeline.security_reports.reports['dependency_scanning'].findings
# this test is invalid if we don't have more sast findings than dep findings # this test is invalid if we don't have more container_scanning findings than dep findings
expect(sast_findings.count).to be > dep_findings.count expect(container_scanning_findings.count).to be > dep_findings.count
(sast_findings + dep_findings).each do |report_finding| (container_scanning_findings + dep_findings).each do |report_finding|
# create a finding and a vulnerability for each report finding # create a finding and a vulnerability for each report finding
# (the vulnerability is created with the :confirmed trait) # (the vulnerability is created with the :confirmed trait)
create(:vulnerabilities_finding, create(:vulnerabilities_finding,
...@@ -103,7 +103,7 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -103,7 +103,7 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
# should be the same number of queries between different report types # should be the same number of queries between different report types
expect do expect do
described_class.new(pipeline: pipeline, params: { report_type: %w[sast] }).execute described_class.new(pipeline: pipeline, params: { report_type: %w[container_scanning] }).execute
end.to issue_same_number_of_queries_as { end.to issue_same_number_of_queries_as {
described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning] }).execute described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning] }).execute
} }
...@@ -117,11 +117,11 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -117,11 +117,11 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
orig_security_reports = pipeline.security_reports orig_security_reports = pipeline.security_reports
new_finding = create(:ci_reports_security_finding) new_finding = create(:ci_reports_security_finding)
expect do expect do
described_class.new(pipeline: pipeline, params: { report_type: %w[sast] }).execute described_class.new(pipeline: pipeline, params: { report_type: %w[container_scanning] }).execute
end.to issue_same_number_of_queries_as { end.to issue_same_number_of_queries_as {
orig_security_reports.reports['sast'].add_finding(new_finding) orig_security_reports.reports['container_scanning'].add_finding(new_finding)
allow(pipeline).to receive(:security_reports).and_return(orig_security_reports) allow(pipeline).to receive(:security_reports).and_return(orig_security_reports)
described_class.new(pipeline: pipeline, params: { report_type: %w[sast] }).execute described_class.new(pipeline: pipeline, params: { report_type: %w[container_scanning] }).execute
} }
end end
end end
...@@ -130,9 +130,11 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -130,9 +130,11 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
context 'when sast' do context 'when sast' do
let(:params) { { report_type: %w[sast] } } let(:params) { { report_type: %w[sast] } }
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].findings.map(&:location).map(&:fingerprint) } let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].findings.map(&:location).map(&:fingerprint) }
let(:sast_report_uuids) {pipeline.security_reports.reports['sast'].findings.map(&:uuid) }
it 'includes only sast' do it 'includes only sast' do
expect(subject.findings.map(&:location_fingerprint)).to match_array(sast_report_fingerprints) expect(subject.findings.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.findings.map(&:uuid)).to match_array(sast_report_uuids)
expect(subject.findings.count).to eq(sast_count) expect(subject.findings.count).to eq(sast_count)
end end
end end
...@@ -172,6 +174,7 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -172,6 +174,7 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
let(:ds_finding) { pipeline.security_reports.reports["dependency_scanning"].findings.first } let(:ds_finding) { pipeline.security_reports.reports["dependency_scanning"].findings.first }
let(:sast_finding) { pipeline.security_reports.reports["sast"].findings.first } let(:sast_finding) { pipeline.security_reports.reports["sast"].findings.first }
context 'when vulnerability_finding_signatures feature flag is disabled' do
let!(:feedback) do let!(:feedback) do
[ [
create( create(
...@@ -181,7 +184,8 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -181,7 +184,8 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
project: project, project: project,
pipeline: pipeline, pipeline: pipeline,
project_fingerprint: ds_finding.project_fingerprint, project_fingerprint: ds_finding.project_fingerprint,
vulnerability_data: ds_finding.raw_metadata vulnerability_data: ds_finding.raw_metadata,
finding_uuid: ds_finding.uuid
), ),
create( create(
:vulnerability_feedback, :vulnerability_feedback,
...@@ -190,11 +194,16 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -190,11 +194,16 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
project: project, project: project,
pipeline: pipeline, pipeline: pipeline,
project_fingerprint: sast_finding.project_fingerprint, project_fingerprint: sast_finding.project_fingerprint,
vulnerability_data: sast_finding.raw_metadata vulnerability_data: sast_finding.raw_metadata,
finding_uuid: sast_finding.uuid
) )
] ]
end end
before do
stub_feature_flags(vulnerability_finding_signatures: false)
end
context 'when unscoped' do context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute } subject { described_class.new(pipeline: pipeline).execute }
...@@ -222,6 +231,54 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do ...@@ -222,6 +231,54 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
end end
end end
context 'when vulnerability_finding_signatures feature flag is enabled' do
let!(:feedback) do
[
create(
:vulnerability_feedback,
:dismissal,
:sast,
project: project,
pipeline: pipeline,
project_fingerprint: sast_finding.project_fingerprint,
vulnerability_data: sast_finding.raw_metadata,
finding_uuid: sast_finding.uuid
)
]
end
before do
stub_feature_flags(vulnerability_finding_signatures: true)
end
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns non-dismissed vulnerabilities' do
expect(subject.findings.count).to eq(cs_count + dast_count + ds_count + sast_count - feedback.count)
expect(subject.findings.map(&:project_fingerprint)).not_to include(*feedback.map(&:project_fingerprint))
end
end
context 'when `dismissed`' do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[sast], scope: 'dismissed' } ).execute }
it 'returns non-dismissed vulnerabilities' do
expect(subject.findings.count).to eq(sast_count - 1)
expect(subject.findings.map(&:project_fingerprint)).not_to include(sast_finding.project_fingerprint)
end
end
context 'when `all`' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'returns all vulnerabilities' do
expect(subject.findings.count).to eq(cs_count + dast_count + ds_count + sast_count)
end
end
end
end
context 'by severity' do context 'by severity' do
context 'when unscoped' do context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute } subject { described_class.new(pipeline: pipeline).execute }
......
...@@ -4,30 +4,22 @@ require 'spec_helper' ...@@ -4,30 +4,22 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Security::Common do RSpec.describe Gitlab::Ci::Parsers::Security::Common do
describe '#parse!' do describe '#parse!' do
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
let_it_be(:pipeline) { create(:ci_pipeline) } let_it_be(:pipeline) { create(:ci_pipeline) }
let(:artifact) { build(:ee_ci_job_artifact, :common_security_report) } let(:artifact) { build(:ee_ci_job_artifact, :common_security_report) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) } let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) }
let(:location) { ::Gitlab::Ci::Reports::Security::Locations::DependencyScanning.new(file_path: 'yarn/yarn.lock', package_version: 'v2', package_name: 'saml2') } let(:location) { ::Gitlab::Ci::Reports::Security::Locations::DependencyScanning.new(file_path: 'yarn/yarn.lock', package_version: 'v2', package_name: 'saml2') }
let(:tracking_data) do let(:tracking_data) { nil }
{
'type' => 'source',
'items' => [
'signatures' => [
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
{ 'algorithm' => 'location', 'value' => 'location_value' },
{ 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
]
]
}
end
before do before do
allow_next_instance_of(described_class) do |parser| allow_next_instance_of(described_class) do |parser|
allow(parser).to receive(:create_location).and_return(location) allow(parser).to receive(:create_location).and_return(location)
allow(parser).to receive(:tracking_data).and_return(tracking_data) allow(parser).to receive(:tracking_data).and_return(tracking_data)
end end
artifact.each_blob { |blob| described_class.parse!(blob, report) }
artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) }
end end
describe 'parsing finding.name' do describe 'parsing finding.name' do
...@@ -200,8 +192,21 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ...@@ -200,8 +192,21 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end end
end end
describe 'parsing signature' do describe 'parsing tracking' do
context 'with valid signature information' do let(:tracking_data) do
{
'type' => 'source',
'items' => [
'signatures' => [
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
{ 'algorithm' => 'location', 'value' => 'location_value' },
{ 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
]
]
}
end
context 'with valid tracking information' do
it 'creates signatures for each algorithm' do it 'creates signatures for each algorithm' do
finding = report.findings.first finding = report.findings.first
expect(finding.signatures.size).to eq(3) expect(finding.signatures.size).to eq(3)
...@@ -209,7 +214,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ...@@ -209,7 +214,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
end end
end end
context 'with invalid signature information' do context 'with invalid tracking information' do
let(:tracking_data) do let(:tracking_data) do
{ {
'type' => 'source', 'type' => 'source',
...@@ -229,6 +234,34 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ...@@ -229,6 +234,34 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location']) expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location'])
end end
end end
context 'with valid tracking information' do
it 'creates signatures for each signature algorithm' do
finding = report.findings.first
expect(finding.signatures.size).to eq(3)
expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset])
signatures = finding.signatures.index_by(&:algorithm_type)
expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] }
expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value'])
expect(signatures['location'].signature_value).to eq(expected_values['location']['value'])
expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value'])
end
it 'sets the uuid according to the higest priority signature' do
finding = report.findings.first
highest_signature = finding.signatures.max_by(&:priority)
identifiers = if vulnerability_finding_signatures_enabled
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}"
else
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}"
end
expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers))
end
end
end
end end
end end
end end
...@@ -18,15 +18,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do ...@@ -18,15 +18,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do
expect(subject.algorithm_type).to eq(params[:algorithm_type]) expect(subject.algorithm_type).to eq(params[:algorithm_type])
expect(subject.signature_value).to eq(params[:signature_value]) expect(subject.signature_value).to eq(params[:signature_value])
end end
describe '#valid?' do
it 'returns true' do
expect(subject.valid?).to eq(true)
end end
end end
describe '#to_h' do
it 'returns a hash representation of the signature' do
expect(subject.to_h).to eq(
algorithm_type: params[:algorithm_type],
signature_sha: Digest::SHA1.digest(params[:signature_value])
)
end end
end end
...@@ -50,4 +47,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do ...@@ -50,4 +47,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do
end end
end end
end end
describe '#to_hash' do
it 'returns a hash representation of the signature' do
expect(subject.to_hash).to eq(
algorithm_type: params[:algorithm_type],
signature_sha: Digest::SHA1.digest(params[:signature_value])
)
end
end
end end
...@@ -137,7 +137,8 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -137,7 +137,8 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
scan: occurrence.scan, scan: occurrence.scan,
severity: occurrence.severity, severity: occurrence.severity,
uuid: occurrence.uuid, uuid: occurrence.uuid,
details: occurrence.details details: occurrence.details,
signatures: []
}) })
end end
end end
...@@ -197,9 +198,11 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -197,9 +198,11 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
end end
describe '#eql?' do describe '#eql?' do
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
let(:identifier) { build(:ci_reports_security_identifier) } let(:identifier) { build(:ci_reports_security_identifier) }
let(:location) { build(:ci_reports_security_locations_sast) } let(:location) { build(:ci_reports_security_locations_sast) }
let(:finding) { build(:ci_reports_security_finding, severity: 'low', report_type: :sast, identifiers: [identifier], location: location) } let(:finding) { build(:ci_reports_security_finding, severity: 'low', report_type: :sast, identifiers: [identifier], location: location, vulnerability_finding_signatures_enabled: vulnerability_finding_signatures_enabled) }
let(:report_type) { :secret_detection } let(:report_type) { :secret_detection }
let(:identifier_external_id) { 'foo' } let(:identifier_external_id) { 'foo' }
...@@ -211,9 +214,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -211,9 +214,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
severity: 'low', severity: 'low',
report_type: report_type, report_type: report_type,
identifiers: [other_identifier], identifiers: [other_identifier],
location: other_location) location: other_location,
vulnerability_finding_signatures_enabled: vulnerability_finding_signatures_enabled)
end end
let(:signature) { ::Gitlab::Ci::Reports::Security::FindingSignature.new(algorithm_type: 'location', signature_value: 'value1') }
subject { finding.eql?(other_finding) } subject { finding.eql?(other_finding) }
context 'when the primary_identifier is nil' do context 'when the primary_identifier is nil' do
...@@ -230,25 +236,30 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -230,25 +236,30 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
context 'when the other finding has same primary identifier fingerprint' do context 'when the other finding has same primary identifier fingerprint' do
let(:identifier_external_id) { identifier.external_id } let(:identifier_external_id) { identifier.external_id }
context 'when the other finding has same location fingerprint' do context 'when the other finding has same location signature' do
before do
finding.signatures << signature
other_finding.signatures << signature
end
let(:location_start_line) { location.start_line } let(:location_start_line) { location.start_line }
it { is_expected.to be(true) } it { is_expected.to be(true) }
end end
context 'when the other finding does not have same location fingerprint' do context 'when the other finding does not have same location signature' do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
end end
context 'when the other finding does not have same primary identifier fingerprint' do context 'when the other finding does not have same primary identifier fingerprint' do
context 'when the other finding has same location fingerprint' do context 'when the other finding has same location signature' do
let(:location_start_line) { location.start_line } let(:location_start_line) { location.start_line }
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
context 'when the other finding does not have same location fingerprint' do context 'when the other finding does not have same location signature' do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
end end
...@@ -258,30 +269,31 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -258,30 +269,31 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
context 'when the other finding has same primary identifier fingerprint' do context 'when the other finding has same primary identifier fingerprint' do
let(:identifier_external_id) { identifier.external_id } let(:identifier_external_id) { identifier.external_id }
context 'when the other finding has same location fingerprint' do context 'when the other finding has same location signature' do
let(:location_start_line) { location.start_line } let(:location_start_line) { location.start_line }
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
context 'when the other finding does not have same location fingerprint' do context 'when the other finding does not have same location signature' do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
end end
context 'when the other finding does not have same primary identifier fingerprint' do context 'when the other finding does not have same primary identifier fingerprint' do
context 'when the other finding has same location fingerprint' do context 'when the other finding has same location signature' do
let(:location_start_line) { location.start_line } let(:location_start_line) { location.start_line }
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
context 'when the other finding does not have same location fingerprint' do context 'when the other finding does not have same location signature' do
it { is_expected.to be(false) } it { is_expected.to be(false) }
end end
end end
end end
end end
end
describe '#valid?' do describe '#valid?' do
let(:scanner) { build(:ci_reports_security_scanner) } let(:scanner) { build(:ci_reports_security_scanner) }
...@@ -345,4 +357,55 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do ...@@ -345,4 +357,55 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
it { is_expected.to match_array(expected_keys) } it { is_expected.to match_array(expected_keys) }
end end
describe '#hash' do
let(:scanner) { build(:ci_reports_security_scanner) }
let(:identifiers) { [build(:ci_reports_security_identifier)] }
let(:location) { build(:ci_reports_security_locations_sast) }
let(:uuid) { SecureRandom.uuid }
context 'with vulnerability_finding_signatures enabled' do
let(:finding) do
build(:ci_reports_security_finding,
scanner: scanner,
identifiers: identifiers,
location: location,
uuid: uuid,
compare_key: '',
vulnerability_finding_signatures_enabled: true)
end
let(:low_priority_signature) { ::Gitlab::Ci::Reports::Security::FindingSignature.new(algorithm_type: 'location', signature_value: 'value1') }
let(:high_priority_signature) { ::Gitlab::Ci::Reports::Security::FindingSignature.new(algorithm_type: 'scope_offset', signature_value: 'value2') }
it 'returns the expected hash with no signatures' do
expect(finding.signatures.length).to eq(0)
expect(finding.hash).to eq(finding.report_type.hash ^ finding.location.fingerprint.hash ^ finding.primary_identifier_fingerprint.hash)
end
it 'returns the expected hash with signatures' do
finding.signatures << low_priority_signature
finding.signatures << high_priority_signature
expect(finding.signatures.length).to eq(2)
expect(finding.hash).to eq(finding.report_type.hash ^ high_priority_signature.signature_hex.hash ^ finding.primary_identifier_fingerprint.hash)
end
end
context 'without vulnerability_finding_signatures enabled' do
let(:finding) do
build(:ci_reports_security_finding,
scanner: scanner,
identifiers: identifiers,
location: location,
uuid: uuid,
compare_key: '',
vulnerability_finding_signatures_enabled: false)
end
it 'returns the expected hash' do
expect(finding.hash).to eq(finding.report_type.hash ^ finding.location.fingerprint.hash ^ finding.primary_identifier_fingerprint.hash)
end
end
end
end end
...@@ -8,16 +8,20 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -8,16 +8,20 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
let(:base_vulnerability) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: ::Enums::Vulnerability.severity_levels[:critical]) } let(:base_vulnerability) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: ::Enums::Vulnerability.severity_levels[:critical]) }
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability])} let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability])}
let(:head_vulnerability) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '123', confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: ::Enums::Vulnerability.severity_levels[:critical]) } let(:head_vulnerability) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: base_vulnerability.location_fingerprint, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: ::Enums::Vulnerability.severity_levels[:critical], uuid: base_vulnerability.uuid) }
let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability])} let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability])}
subject { described_class.new(base_report, head_report) }
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
before do before do
allow(base_vulnerability).to receive(:location).and_return({}) allow(base_vulnerability).to receive(:location).and_return({})
allow(head_vulnerability).to receive(:location).and_return({}) allow(head_vulnerability).to receive(:location).and_return({})
stub_feature_flags(vulnerability_finding_signatures: vulnerability_finding_signatures_enabled)
end end
subject { described_class.new(base_report, head_report) }
describe '#base_report_out_of_date' do describe '#base_report_out_of_date' do
context 'no base report' do context 'no base report' do
let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: [])} let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: [])}
...@@ -79,7 +83,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -79,7 +83,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
describe '#fixed' do describe '#fixed' do
let(:vuln) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '888') } let(:vuln) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '888') }
let(:medium_vuln) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium]) } let(:medium_vuln) { build(:vulnerabilities_finding, report_type: :sast, identifiers: [identifier], location_fingerprint: '888', confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid) }
context 'with fixed vulnerability' do context 'with fixed vulnerability' do
let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln])} let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln])}
...@@ -132,4 +136,5 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do ...@@ -132,4 +136,5 @@ RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do
expect(comparer.added).to eq([]) expect(comparer.added).to eq([])
end end
end end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe VulnerabilityFindingSignatureHelpers do
let(:cls) do
Class.new do
include VulnerabilityFindingSignatureHelpers
attr_accessor :algorithm_type
def initialize(algorithm_type)
@algorithm_type = algorithm_type
end
end
end
describe '#priority' do
it 'returns numeric values of the priority string' do
expect(cls.new('scope_offset').priority).to eq(3)
expect(cls.new('location').priority).to eq(2)
expect(cls.new('hash').priority).to eq(1)
end
end
describe '#self.priority' do
it 'returns the numeric value of the provided string' do
expect(cls.priority('scope_offset')).to eq(3)
expect(cls.priority('location')).to eq(2)
expect(cls.priority('hash')).to eq(1)
end
end
end
...@@ -7,6 +7,12 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -7,6 +7,12 @@ RSpec.describe Vulnerabilities::Finding do
it { is_expected.to define_enum_for(:report_type) } it { is_expected.to define_enum_for(:report_type) }
it { is_expected.to define_enum_for(:severity) } it { is_expected.to define_enum_for(:severity) }
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
before do
stub_feature_flags(vulnerability_finding_signatures: vulnerability_finding_signatures_enabled)
end
describe 'associations' do describe 'associations' do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:primary_identifier).class_name('Vulnerabilities::Identifier') } it { is_expected.to belong_to(:primary_identifier).class_name('Vulnerabilities::Identifier') }
...@@ -63,7 +69,7 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -63,7 +69,7 @@ RSpec.describe Vulnerabilities::Finding do
context 'database uniqueness' do context 'database uniqueness' do
let(:finding) { create(:vulnerabilities_finding) } let(:finding) { create(:vulnerabilities_finding) }
let(:new_finding) { finding.dup.tap { |o| o.uuid = SecureRandom.uuid } } let(:new_finding) { finding.dup.tap { |o| o.cve = SecureRandom.uuid } }
it "when all index attributes are identical" do it "when all index attributes are identical" do
expect { new_finding.save! }.to raise_error(ActiveRecord::RecordNotUnique) expect { new_finding.save! }.to raise_error(ActiveRecord::RecordNotUnique)
...@@ -81,7 +87,7 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -81,7 +87,7 @@ RSpec.describe Vulnerabilities::Finding do
with_them do with_them do
it "is valid" do it "is valid" do
expect { new_finding.update!({ key => create(factory_name) }) }.not_to raise_error expect { new_finding.update!({ key => create(factory_name), 'uuid' => SecureRandom.uuid }) }.not_to raise_error
end end
end end
end end
...@@ -673,7 +679,6 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -673,7 +679,6 @@ RSpec.describe Vulnerabilities::Finding do
let(:confirmed_finding) { create(:vulnerabilities_finding, :confirmed) } let(:confirmed_finding) { create(:vulnerabilities_finding, :confirmed) }
let(:resolved_finding) { create(:vulnerabilities_finding, :resolved) } let(:resolved_finding) { create(:vulnerabilities_finding, :resolved) }
let(:dismissed_finding) { create(:vulnerabilities_finding, :dismissed) } let(:dismissed_finding) { create(:vulnerabilities_finding, :dismissed) }
let(:detected_finding) { create(:vulnerabilities_finding, :detected) }
let(:finding_with_issue) { create(:vulnerabilities_finding, :with_issue_feedback) } let(:finding_with_issue) { create(:vulnerabilities_finding, :with_issue_feedback) }
it 'returns the expected state for a unresolved finding' do it 'returns the expected state for a unresolved finding' do
...@@ -692,10 +697,6 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -692,10 +697,6 @@ RSpec.describe Vulnerabilities::Finding do
expect(dismissed_finding.state).to eq 'dismissed' expect(dismissed_finding.state).to eq 'dismissed'
end end
it 'returns the expected state for a detected finding' do
expect(detected_finding.state).to eq 'detected'
end
context 'when a vulnerability present for a dismissed finding' do context 'when a vulnerability present for a dismissed finding' do
before do before do
create(:vulnerability, project: dismissed_finding.project, findings: [dismissed_finding]) create(:vulnerability, project: dismissed_finding.project, findings: [dismissed_finding])
...@@ -1010,4 +1011,117 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -1010,4 +1011,117 @@ RSpec.describe Vulnerabilities::Finding do
end end
end end
end end
describe '#eql?' do
let(:project) { create(:project) }
let(:report_type) { :sast }
let(:identifier_fingerprint) { 'fooo' }
let(:identifier) { build(:vulnerabilities_identifier, fingerprint: identifier_fingerprint) }
let(:location_fingerprint1) { 'fingerprint1' }
let(:location_fingerprint2) { 'fingerprint2' }
let(:finding1) do
build(:vulnerabilities_finding, report_type,
project: project,
primary_identifier: identifier,
location_fingerprint: location_fingerprint1)
end
let(:finding2) do
build(:vulnerabilities_finding, report_type,
project: project,
primary_identifier: identifier,
location_fingerprint: location_fingerprint2)
end
it 'matches the finding based on enabled tracking methods (if feature flag enabled)' do
signature1 = create(
:vulnerabilities_finding_signature,
finding: finding1
)
signature2 = create(
:vulnerabilities_finding_signature,
finding: finding2,
signature_sha: signature1.signature_sha
)
# verify that the signatures do exist and that they match
expect(finding1.signatures.size).to eq(1)
expect(finding2.signatures.size).to eq(1)
expect(signature1.eql?(signature2)).to be(true)
# now verify that the correct matching method was used for eql?
expect(finding1.eql?(finding2)).to be(vulnerability_finding_signatures_enabled)
end
context 'short circuits on the highest priority signature match' do
using RSpec::Parameterized::TableSyntax
let(:same_hash) { false }
let(:same_location) { false }
let(:create_scope_offset) { false }
let(:same_scope_offset) { false}
let(:create_signatures) do
signature1_hash = create(
:vulnerabilities_finding_signature,
algorithm_type: 'hash',
finding: finding1
)
sha = same_hash ? signature1_hash.signature_sha : ::Digest::SHA1.digest(SecureRandom.hex(50))
create(
:vulnerabilities_finding_signature,
algorithm_type: 'hash',
finding: finding2,
signature_sha: sha
)
signature1_location = create(
:vulnerabilities_finding_signature,
algorithm_type: 'location',
finding: finding1
)
sha = same_location ? signature1_location.signature_sha : ::Digest::SHA1.digest(SecureRandom.hex(50))
create(
:vulnerabilities_finding_signature,
algorithm_type: 'location',
finding: finding2,
signature_sha: sha
)
signature1_scope_offset = create(
:vulnerabilities_finding_signature,
algorithm_type: 'scope_offset',
finding: finding1
)
if create_scope_offset
sha = same_scope_offset ? signature1_scope_offset.signature_sha : ::Digest::SHA1.digest(SecureRandom.hex(50))
create(
:vulnerabilities_finding_signature,
algorithm_type: 'scope_offset',
finding: finding2,
signature_sha: sha
)
end
end
where(:same_hash, :same_location, :create_scope_offset, :same_scope_offset, :should_match) do
true | true | true | true | true # everything matches
false | false | true | false | false # nothing matches
true | true | true | false | false # highest priority matches alg/priority but not on value
false | false | true | true | true # highest priority matches alg/priority and value
false | true | false | false | true # highest priority is location, matches alg/priority and value
end
with_them do
it 'matches correctly' do
next unless vulnerability_finding_signatures_enabled
create_signatures
expect(finding1.eql?(finding2)).to be(should_match)
end
end
end
end
end
end end
...@@ -33,7 +33,8 @@ RSpec.describe API::VulnerabilityFindings do ...@@ -33,7 +33,8 @@ RSpec.describe API::VulnerabilityFindings do
project: project, project: project,
pipeline: pipeline, pipeline: pipeline,
project_fingerprint: sast_report.findings.first.project_fingerprint, project_fingerprint: sast_report.findings.first.project_fingerprint,
vulnerability_data: sast_report.findings.first.raw_metadata vulnerability_data: sast_report.findings.first.raw_metadata,
finding_uuid: sast_report.findings.first.uuid
) )
end end
......
...@@ -164,7 +164,7 @@ RSpec.describe Vulnerabilities::FeedbackEntity do ...@@ -164,7 +164,7 @@ RSpec.describe Vulnerabilities::FeedbackEntity do
end end
context 'when finding_uuid is not present' do context 'when finding_uuid is not present' do
let(:feedback) { build_stubbed(:vulnerability_feedback, :issue, project: project) } let(:feedback) { build_stubbed(:vulnerability_feedback, :issue, project: project, finding_uuid: nil) }
it 'has a nil finding_uuid' do it 'has a nil finding_uuid' do
expect(subject[:finding_uuid]).to be_nil expect(subject[:finding_uuid]).to be_nil
......
...@@ -11,6 +11,12 @@ RSpec.describe Ci::CompareSecurityReportsService do ...@@ -11,6 +11,12 @@ RSpec.describe Ci::CompareSecurityReportsService do
collection.map { |t| t['identifiers'].first['external_id'] } collection.map { |t| t['identifiers'].first['external_id'] }
end end
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
before do
stub_feature_flags(vulnerability_finding_signatures: vulnerability_finding_signatures_enabled)
end
describe '#execute DS' do describe '#execute DS' do
before do before do
stub_licensed_features(dependency_scanning: true) stub_licensed_features(dependency_scanning: true)
...@@ -273,4 +279,5 @@ RSpec.describe Ci::CompareSecurityReportsService do ...@@ -273,4 +279,5 @@ RSpec.describe Ci::CompareSecurityReportsService do
end end
end end
end end
end
end end
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Security::StoreReportService, '#execute' do RSpec.describe Security::StoreReportService, '#execute' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:artifact) { create(:ee_ci_job_artifact, trait) } let(:artifact) { create(:ee_ci_job_artifact, trait) }
...@@ -11,13 +13,16 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -11,13 +13,16 @@ RSpec.describe Security::StoreReportService, '#execute' do
let(:pipeline) { artifact.job.pipeline } let(:pipeline) { artifact.job.pipeline }
let(:report) { pipeline.security_reports.get_report(report_type.to_s, artifact) } let(:report) { pipeline.security_reports.get_report(report_type.to_s, artifact) }
subject { described_class.new(pipeline, report).execute }
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
before do before do
stub_feature_flags(vulnerability_finding_signatures: vulnerability_finding_signatures_enabled)
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, security_dashboard: true) stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, security_dashboard: true)
allow(Security::AutoFixWorker).to receive(:perform_async) allow(Security::AutoFixWorker).to receive(:perform_async)
end end
subject { described_class.new(pipeline, report).execute }
context 'without existing data' do context 'without existing data' do
before(:all) do before(:all) do
checksum = 'f00bc6261fa512f0960b7fc3bfcce7fb31997cf32b96fa647bed5668b2c77fee' checksum = 'f00bc6261fa512f0960b7fc3bfcce7fb31997cf32b96fa647bed5668b2c77fee'
...@@ -30,24 +35,18 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -30,24 +35,18 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
context 'for different security reports' do context 'for different security reports' do
using RSpec::Parameterized::TableSyntax
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines, :remediations, :signatures, :optimize_sql_query_for_security_report_ff) do
'with SAST report' | :sast | 1 | 6 | 5 | 7 | 5 | 0 | 2 | false
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 | 0 | 0 | false
'with Dependency Scanning report' | :dependency_scanning_remediation | 1 | 3 | 2 | 3 | 2 | 1 | 0 | false
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 | 0 | 0 | false
'with SAST report' | :sast | 1 | 6 | 5 | 7 | 5 | 0 | 2 | true
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 | 0 | 0 | true
'with Dependency Scanning report' | :dependency_scanning_remediation | 1 | 3 | 2 | 3 | 2 | 1 | 0 | true
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 | 0 | 0 | true
end
with_them do with_them do
before do before do
stub_feature_flags(optimize_sql_query_for_security_report: optimize_sql_query_for_security_report_ff) stub_feature_flags(optimize_sql_query_for_security_report: optimize_sql_query_for_security_report_ff)
end end
where(:case_name, :trait, :scanners, :identifiers, :findings, :finding_identifiers, :finding_pipelines, :remediations, :signatures) do
'with SAST report' | :sast | 1 | 6 | 5 | 7 | 5 | 0 | 2
'with exceeding identifiers' | :with_exceeding_identifiers | 1 | 20 | 1 | 20 | 1 | 0 | 1
'with Dependency Scanning report' | :dependency_scanning_remediation | 1 | 3 | 2 | 3 | 2 | 1 | 2
'with Container Scanning report' | :container_scanning | 1 | 8 | 8 | 8 | 8 | 0 | 8
end
it 'inserts all scanners' do it 'inserts all scanners' do
expect { subject }.to change { Vulnerabilities::Scanner.count }.by(scanners) expect { subject }.to change { Vulnerabilities::Scanner.count }.by(scanners)
end end
...@@ -164,11 +163,6 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -164,11 +163,6 @@ RSpec.describe Security::StoreReportService, '#execute' do
let(:new_pipeline) { create(:ci_pipeline, project: project) } let(:new_pipeline) { create(:ci_pipeline, project: project) }
let(:new_report) { new_pipeline.security_reports.get_report(report_type.to_s, artifact) } let(:new_report) { new_pipeline.security_reports.get_report(report_type.to_s, artifact) }
let(:existing_signature) { create(:vulnerabilities_finding_signature, finding: finding) } let(:existing_signature) { create(:vulnerabilities_finding_signature, finding: finding) }
let(:unsupported_signature) do
create(:vulnerabilities_finding_signature,
finding: finding,
algorithm_type: ::Vulnerabilities::FindingSignature.algorithm_types[:location])
end
let(:trait) { :sast } let(:trait) { :sast }
...@@ -182,7 +176,7 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -182,7 +176,7 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
let!(:finding) do let!(:finding) do
create(:vulnerabilities_finding, created_finding = create(:vulnerabilities_finding,
pipelines: [pipeline], pipelines: [pipeline],
identifiers: [identifier], identifiers: [identifier],
primary_identifier: identifier, primary_identifier: identifier,
...@@ -190,6 +184,15 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -190,6 +184,15 @@ RSpec.describe Security::StoreReportService, '#execute' do
project: project, project: project,
uuid: "e5388f40-18f5-566d-95c6-d64c6f46a00a", uuid: "e5388f40-18f5-566d-95c6-d64c6f46a00a",
location_fingerprint: finding_location_fingerprint) location_fingerprint: finding_location_fingerprint)
existing_finding = report.findings.find { |f| f.location.fingerprint == created_finding.location_fingerprint }
create(:vulnerabilities_finding_signature,
finding: created_finding,
algorithm_type: existing_finding.signatures.first.algorithm_type,
signature_sha: existing_finding.signatures.first.signature_sha)
created_finding
end end
let!(:vulnerability) { create(:vulnerability, findings: [finding], project: project) } let!(:vulnerability) { create(:vulnerability, findings: [finding], project: project) }
...@@ -227,6 +230,14 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -227,6 +230,14 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
it 'updates UUIDv4 to UUIDv5' do it 'updates UUIDv4 to UUIDv5' do
finding.uuid = '00000000-1111-2222-3333-444444444444'
finding.save!
# this report_finding should be used to update the finding's uuid
report_finding = new_report.findings.find { |f| f.location.fingerprint == '0e7d0291d912f56880e39d4fbd80d99dd5d327ba' }
allow(report_finding).to receive(:uuid).and_return(desired_uuid)
report_finding.signatures.pop
subject subject
expect(finding.reload.uuid).to eq(desired_uuid) expect(finding.reload.uuid).to eq(desired_uuid)
...@@ -259,28 +270,24 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -259,28 +270,24 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
it 'updates signatures to match new values' do it 'updates signatures to match new values' do
existing_signature next unless vulnerability_finding_signatures_enabled
unsupported_signature
expect(finding.signatures.count).to eq(2) expect(finding.signatures.count).to eq(1)
signature_algs = finding.signatures.map(&:algorithm_type).sort expect(finding.signatures.first.algorithm_type).to eq('hash')
expect(signature_algs).to eq(%w[hash location])
existing_signature = finding.signatures.first
subject subject
finding.reload finding.reload
existing_signature.reload existing_signature.reload
# check that unsupported algorithm is not deleted expect(finding.signatures.count).to eq(2)
expect(finding.signatures.count).to eq(3) signature_algs = finding.signatures.sort_by(&:priority).map(&:algorithm_type)
signature_algs = finding.signatures.sort.map(&:algorithm_type) expect(signature_algs).to eq(%w[hash scope_offset])
expect(signature_algs).to eq(%w[hash location scope_offset])
# check that the existing hash signature was updated/reused # check that the existing hash signature was updated/reused
expect(existing_signature.id).to eq(finding.signatures.min.id) expect(existing_signature.id).to eq(finding.signatures.find(&:algorithm_hash?).id)
# check that the unsupported signature was not deleted
expect(::Vulnerabilities::FindingSignature.exists?(unsupported_signature.id)).to eq(true)
end end
it 'updates existing vulnerability with new data' do it 'updates existing vulnerability with new data' do
...@@ -299,7 +306,7 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -299,7 +306,7 @@ RSpec.describe Security::StoreReportService, '#execute' do
context 'when the existing resolved vulnerability is discovered again on the latest report' do context 'when the existing resolved vulnerability is discovered again on the latest report' do
before do before do
vulnerability.update!(resolved_on_default_branch: true) vulnerability.update_column(:resolved_on_default_branch, true)
end end
it 'marks the vulnerability as not resolved on default branch' do it 'marks the vulnerability as not resolved on default branch' do
...@@ -331,7 +338,7 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -331,7 +338,7 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
context 'vulnerability issue link' do context 'vulnerability issue link' do
context 'when there is no associated issue feedback with finding' do context 'when there is no assoiciated issue feedback with finding' do
it 'does not insert issue links from the new pipeline' do it 'does not insert issue links from the new pipeline' do
expect { subject }.to change { Vulnerabilities::IssueLink.count }.by(0) expect { subject }.to change { Vulnerabilities::IssueLink.count }.by(0)
end end
...@@ -414,7 +421,7 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -414,7 +421,7 @@ RSpec.describe Security::StoreReportService, '#execute' do
context 'when auto fix feature is disabled' do context 'when auto fix feature is disabled' do
before do before do
project.security_setting.update!(auto_fix_dependency_scanning: false) project.security_setting.update_column(:auto_fix_dependency_scanning, false)
end end
it 'does not start auto fix worker' do it 'does not start auto fix worker' do
...@@ -464,4 +471,5 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -464,4 +471,5 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
end end
end end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment