blob: adc666d99872881e42ada4249556837cbbd7dbb5 (
plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
|
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
class VulnerabilityReportsComparer
include Gitlab::Utils::StrongMemoize
attr_reader :base_report, :head_report
ACCEPTABLE_REPORT_AGE = 1.week
MAX_FINDINGS_COUNT = 25
def initialize(project, base_report, head_report)
@base_report = base_report
@head_report = head_report
@signatures_enabled = project.licensed_feature_available?(:vulnerability_finding_signatures)
if @signatures_enabled
@added_findings = []
@fixed_findings = []
calculate_changes
end
end
def base_report_created_at
@base_report.created_at
end
def head_report_created_at
@head_report.created_at
end
def base_report_out_of_date
return false unless @base_report.created_at
ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at
end
def added
strong_memoize(:added) do
all_added_findings.take(MAX_FINDINGS_COUNT) # rubocop:disable CodeReuse/ActiveRecord (This is Array#take)
end
end
def fixed
strong_memoize(:fixed) do
all_fixed_findings.take(MAX_FINDINGS_COUNT) # rubocop:disable CodeReuse/ActiveRecord (This is Array#take)
end
end
private
def calculate_changes
# This is a deconstructed version of the eql? method on
# Ci::Reports::Security::Finding. It:
#
# * precomputes for the head_findings (using FindingMatcher):
# * sets of signature shas grouped by priority
# * mappings of signature shas to the head finding object
#
# These are then used when iterating the base findings to perform
# fast(er) prioritized, signature-based comparisons between each base finding
# and the head findings.
#
# Both the head_findings and base_findings arrays are iterated once
base_findings = base_report.findings
head_findings = head_report.findings
matcher = FindingMatcher.new(head_findings)
base_findings.each do |base_finding|
next if base_finding.requires_manual_resolution?
matched_head_finding = matcher.find_and_remove_match!(base_finding)
@fixed_findings << base_finding if matched_head_finding.nil?
end
@added_findings = matcher.unmatched_head_findings.values
end
def all_added_findings
if @signatures_enabled
@added_findings
else
head_report.findings - base_report.findings
end
end
def all_fixed_findings
if @signatures_enabled
@fixed_findings
else
base_report.findings - head_report.findings
end
end
end
class FindingMatcher
attr_reader :unmatched_head_findings, :head_findings
include Gitlab::Utils::StrongMemoize
def initialize(head_findings)
@head_findings = head_findings
@unmatched_head_findings = @head_findings.index_by(&:object_id)
end
def find_and_remove_match!(base_finding)
matched_head_finding = find_matched_head_finding_for(base_finding)
# no signatures matched, so check the normal uuids of the base and head findings
# for a match
matched_head_finding = head_signatures_shas[base_finding.uuid] if matched_head_finding.nil?
@unmatched_head_findings.delete(matched_head_finding.object_id) unless matched_head_finding.nil?
matched_head_finding
end
private
def find_matched_head_finding_for(base_finding)
base_signature = sorted_signatures_for(base_finding).find do |signature|
# at this point a head_finding exists that has a signature with a
# matching priority, and a matching sha --> lookup the actual finding
# object from head_signatures_shas
head_signatures_shas[signature.signature_sha].eql?(base_finding)
end
base_signature.present? ? head_signatures_shas[base_signature.signature_sha] : nil
end
def sorted_signatures_for(base_finding)
base_finding.signatures.select { |signature| head_finding_signature?(signature) }
.sort_by { |sig| -sig.priority }
end
def head_finding_signature?(signature)
head_signatures_priorities[signature.priority].include?(signature.signature_sha)
end
def head_signatures_priorities
strong_memoize(:head_signatures_priorities) do
signatures_priorities = Hash.new { |hash, key| hash[key] = Set.new }
head_findings.each_with_object(signatures_priorities) do |head_finding, memo|
head_finding.signatures.each do |signature|
memo[signature.priority].add(signature.signature_sha)
end
end
end
end
def head_signatures_shas
strong_memoize(:head_signatures_shas) do
head_findings.each_with_object({}) do |head_finding, memo|
head_finding.signatures.each do |signature|
memo[signature.signature_sha] = head_finding
end
# for the final uuid check when no signatures have matched
memo[head_finding.uuid] = head_finding
end
end
end
end
end
end
end
end
|