Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
56fcaba4
Commit
56fcaba4
authored
Apr 15, 2021
by
James Johnson
Committed by
Dmitry Gruzd
Apr 15, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve Vulnerability Tracking: Use Signatures [RUN ALL RSPEC] [RUN AS-IF-FOSS]
parent
d61c5521
Changes
34
Hide whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
2275 additions
and
1688 deletions
+2275
-1688
app/models/concerns/vulnerability_finding_helpers.rb
app/models/concerns/vulnerability_finding_helpers.rb
+7
-0
app/models/concerns/vulnerability_finding_signature_helpers.rb
...odels/concerns/vulnerability_finding_signature_helpers.rb
+7
-0
ee/app/finders/security/findings_finder.rb
ee/app/finders/security/findings_finder.rb
+5
-1
ee/app/finders/security/pipeline_vulnerabilities_finder.rb
ee/app/finders/security/pipeline_vulnerabilities_finder.rb
+27
-6
ee/app/models/concerns/ee/vulnerability_finding_helpers.rb
ee/app/models/concerns/ee/vulnerability_finding_helpers.rb
+39
-0
ee/app/models/concerns/ee/vulnerability_finding_signature_helpers.rb
...ls/concerns/ee/vulnerability_finding_signature_helpers.rb
+30
-0
ee/app/models/license.rb
ee/app/models/license.rb
+1
-0
ee/app/models/vulnerabilities/finding.rb
ee/app/models/vulnerabilities/finding.rb
+11
-6
ee/app/models/vulnerabilities/finding_signature.rb
ee/app/models/vulnerabilities/finding_signature.rb
+12
-0
ee/app/services/security/store_report_service.rb
ee/app/services/security/store_report_service.rb
+76
-6
ee/config/feature_flags/development/vulnerability_finding_signatures.yml
...re_flags/development/vulnerability_finding_signatures.yml
+8
-0
ee/lib/gitlab/ci/parsers/security/common.rb
ee/lib/gitlab/ci/parsers/security/common.rb
+34
-19
ee/lib/gitlab/ci/reports/security/finding.rb
ee/lib/gitlab/ci/reports/security/finding.rb
+21
-6
ee/lib/gitlab/ci/reports/security/finding_signature.rb
ee/lib/gitlab/ci/reports/security/finding_signature.rb
+18
-1
ee/lib/gitlab/ci/reports/security/locations/container_scanning.rb
...itlab/ci/reports/security/locations/container_scanning.rb
+2
-2
ee/lib/gitlab/ci/reports/security/locations/coverage_fuzzing.rb
.../gitlab/ci/reports/security/locations/coverage_fuzzing.rb
+0
-2
ee/lib/gitlab/ci/reports/security/locations/dast.rb
ee/lib/gitlab/ci/reports/security/locations/dast.rb
+0
-2
ee/lib/gitlab/ci/reports/security/locations/dependency_scanning.rb
...tlab/ci/reports/security/locations/dependency_scanning.rb
+0
-2
ee/lib/gitlab/ci/reports/security/locations/sast.rb
ee/lib/gitlab/ci/reports/security/locations/sast.rb
+0
-2
ee/lib/gitlab/ci/reports/security/locations/secret_detection.rb
.../gitlab/ci/reports/security/locations/secret_detection.rb
+0
-2
ee/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
...lab/ci/reports/security/vulnerability_reports_comparer.rb
+27
-9
ee/spec/factories/ci/reports/security/findings.rb
ee/spec/factories/ci/reports/security/findings.rb
+1
-0
ee/spec/factories/vulnerabilities/feedback.rb
ee/spec/factories/vulnerabilities/feedback.rb
+1
-0
ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb
.../finders/security/pipeline_vulnerabilities_finder_spec.rb
+102
-45
ee/spec/lib/gitlab/ci/parsers/security/common_spec.rb
ee/spec/lib/gitlab/ci/parsers/security/common_spec.rb
+192
-159
ee/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb
.../lib/gitlab/ci/reports/security/finding_signature_spec.rb
+14
-8
ee/spec/lib/gitlab/ci/reports/security/finding_spec.rb
ee/spec/lib/gitlab/ci/reports/security/finding_spec.rb
+122
-59
ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
...i/reports/security/vulnerability_reports_comparer_spec.rb
+85
-80
ee/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb
.../concerns/vulnerability_finding_signature_helpers_spec.rb
+32
-0
ee/spec/models/vulnerabilities/finding_spec.rb
ee/spec/models/vulnerabilities/finding_spec.rb
+853
-739
ee/spec/requests/api/vulnerability_findings_spec.rb
ee/spec/requests/api/vulnerability_findings_spec.rb
+2
-1
ee/spec/serializers/vulnerabilities/feedback_entity_spec.rb
ee/spec/serializers/vulnerabilities/feedback_entity_spec.rb
+1
-1
ee/spec/services/ci/compare_security_reports_service_spec.rb
ee/spec/services/ci/compare_security_reports_service_spec.rb
+212
-205
ee/spec/services/security/store_report_service_spec.rb
ee/spec/services/security/store_report_service_spec.rb
+333
-325
No files found.
app/models/concerns/vulnerability_finding_helpers.rb
0 → 100644
View file @
56fcaba4
# frozen_string_literal: true
module
VulnerabilityFindingHelpers
extend
ActiveSupport
::
Concern
end
VulnerabilityFindingHelpers
.
prepend_if_ee
(
'EE::VulnerabilityFindingHelpers'
)
app/models/concerns/vulnerability_finding_signature_helpers.rb
0 → 100644
View file @
56fcaba4
# frozen_string_literal: true
module
VulnerabilityFindingSignatureHelpers
extend
ActiveSupport
::
Concern
end
VulnerabilityFindingSignatureHelpers
.
prepend_if_ee
(
'EE::VulnerabilityFindingSignatureHelpers'
)
ee/app/finders/security/findings_finder.rb
View file @
56fcaba4
...
...
@@ -47,10 +47,13 @@ module Security
report_finding
=
report_finding_for
(
security_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
|
Vulnerabilities
::
Identifier
.
new
(
identifier
.
to_hash
)
end
signatures
=
report_finding
.
signatures
.
map
do
|
signature
|
Vulnerabilities
::
FindingSignature
.
new
(
signature
.
to_hash
)
end
Vulnerabilities
::
Finding
.
new
(
finding_data
).
tap
do
|
finding
|
finding
.
location_fingerprint
=
report_finding
.
location
.
fingerprint
...
...
@@ -59,6 +62,7 @@ module Security
finding
.
sha
=
pipeline
.
sha
finding
.
scanner
=
security_finding
.
scanner
finding
.
identifiers
=
identifiers
finding
.
signatures
=
signatures
end
end
...
...
ee/app/finders/security/pipeline_vulnerabilities_finder.rb
View file @
56fcaba4
...
...
@@ -75,7 +75,7 @@ module Security
def
normalize_report_findings
(
report_findings
,
vulnerabilities
)
report_findings
.
map
do
|
report_finding
|
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
)
# assigning Vulnerabilities to Findings to enable the computed state
...
...
@@ -90,6 +90,9 @@ module Security
finding
.
identifiers
=
report_finding
.
identifiers
.
map
do
|
identifier
|
Vulnerabilities
::
Identifier
.
new
(
identifier
.
to_hash
)
end
finding
.
signatures
=
report_finding
.
signatures
.
map
do
|
signature
|
Vulnerabilities
::
FindingSignature
.
new
(
signature
.
to_hash
)
end
finding
end
...
...
@@ -111,18 +114,36 @@ module Security
end
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
def
all_dismissal_feedbacks
strong_memoize
(
:all_dismissal_feedbacks
)
do
pipeline
.
project
.
vulnerability_feedback
.
for_dismissal
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
pipeline
.
project
.
vulnerability_feedback
.
for_dismissal
.
group_by
(
&
:project_fingerprint
)
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
Array
(
params
.
fetch
(
:confidence
,
Vulnerabilities
::
Finding
.
confidences
.
keys
))
end
...
...
ee/app/models/concerns/ee/vulnerability_finding_helpers.rb
0 → 100644
View file @
56fcaba4
# 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
ee/app/models/concerns/ee/vulnerability_finding_signature_helpers.rb
0 → 100644
View file @
56fcaba4
# 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
ee/app/models/license.rb
View file @
56fcaba4
...
...
@@ -180,6 +180,7 @@ class License < ApplicationRecord
subepics
threat_monitoring
vulnerability_auto_fix
vulnerability_finding_signatures
]
EEU_FEATURES
.
freeze
...
...
ee/app/models/vulnerabilities/finding.rb
View file @
56fcaba4
...
...
@@ -5,6 +5,7 @@ module Vulnerabilities
include
ShaAttribute
include
::
Gitlab
::
Utils
::
StrongMemoize
include
Presentable
include
::
VulnerabilityFindingHelpers
# 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
...
...
@@ -332,12 +333,16 @@ module Vulnerabilities
end
end
alias_method
:==
,
:eql?
# eql? is necessary in some cases like array intersection
alias_method
:==
,
:eql?
def
eql?
(
other
)
other
.
report_type
==
report_type
&&
other
.
location_fingerprint
==
location_fingerprint
&&
other
.
first_fingerprint
==
first_fingerprint
return
false
unless
other
.
report_type
==
report_type
&&
other
.
primary_identifier_fingerprint
==
primary_identifier_fingerprint
if
::
Feature
.
enabled?
(
:vulnerability_finding_signatures
,
project
)
matches_signatures
(
other
.
signatures
,
other
.
uuid
)
else
other
.
location_fingerprint
==
location_fingerprint
end
end
# Array.difference (-) method uses hash and eql? methods to do comparison
...
...
@@ -348,7 +353,7 @@ module Vulnerabilities
# when Finding is persisted and identifiers are not preloaded.
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
def
severity_value
...
...
@@ -380,7 +385,7 @@ module Vulnerabilities
protected
def
first
_fingerprint
def
primary_identifier
_fingerprint
identifiers
.
first
&
.
fingerprint
end
...
...
ee/app/models/vulnerabilities/finding_signature.rb
View file @
56fcaba4
...
...
@@ -5,11 +5,23 @@ module Vulnerabilities
self
.
table_name
=
'vulnerability_finding_signatures'
include
BulkInsertSafe
include
VulnerabilityFindingSignatureHelpers
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
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
ee/app/services/security/store_report_service.rb
View file @
56fcaba4
...
...
@@ -61,17 +61,21 @@ module Security
return
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'
)
# Vulnerabilities::Finding (`vulnerability_occurrences`)
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_finding
(
vulnerability_finding
,
vulnerability_params
)
reset_remediations_for
(
vulnerability_finding
,
finding
)
update_finding_signatures
(
finding
,
vulnerability_finding
)
if
::
Feature
.
enabled?
(
:vulnerability_finding_signatures
,
project
)
update_feedbacks
(
vulnerability_finding
,
vulnerability_params
[
:uuid
])
update_finding_signatures
(
finding
,
vulnerability_finding
)
end
# The maximum number of identifiers is not used in validation
# we just want to ignore the rest if a finding has more than that.
...
...
@@ -86,8 +90,16 @@ module Security
create_vulnerability
(
vulnerability_finding
,
pipeline
)
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
def
create_new_vulnerability_finding
(
finding
,
create_params
)
def
find_or_create_vulnerability_finding_with_location
(
finding
,
create_params
)
find_params
=
{
scanner:
scanners_objects
[
finding
.
scanner
.
key
],
primary_identifier:
identifiers_objects
[
finding
.
primary_identifier
.
key
],
...
...
@@ -120,6 +132,56 @@ module Security
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
)
scanner
=
scanners_objects
[
finding
.
scanner
.
key
]
scanner
.
update!
(
finding
.
scanner
.
to_hash
)
...
...
@@ -223,6 +285,14 @@ module Security
end
# 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
)
to_update
=
{}
to_create
=
[]
...
...
@@ -240,12 +310,12 @@ module Security
next
if
poro_signature
.
nil?
poro_signatures
.
delete
(
signature
.
algorithm_type
)
to_update
[
signature
.
id
]
=
poro_signature
.
to_h
to_update
[
signature
.
id
]
=
poro_signature
.
to_h
ash
end
# any remaining poro signatures left are new
poro_signatures
.
values
.
each
do
|
poro_signature
|
attributes
=
poro_signature
.
to_h
.
merge
(
finding_id:
vulnerability_finding
.
id
)
attributes
=
poro_signature
.
to_h
ash
.
merge
(
finding_id:
vulnerability_finding
.
id
)
to_create
<<
::
Vulnerabilities
::
FindingSignature
.
new
(
attributes:
attributes
,
created_at:
Time
.
zone
.
now
,
updated_at:
Time
.
zone
.
now
)
end
...
...
ee/config/feature_flags/development/vulnerability_finding_signatures.yml
0 → 100644
View file @
56fcaba4
---
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
ee/lib/gitlab/ci/parsers/security/common.rb
View file @
56fcaba4
...
...
@@ -7,13 +7,14 @@ module Gitlab
class
Common
SecurityReportParserError
=
Class
.
new
(
Gitlab
::
Ci
::
Parsers
::
ParserError
)
def
self
.
parse!
(
json_data
,
report
)
new
(
json_data
,
report
).
parse!
def
self
.
parse!
(
json_data
,
report
,
vulnerability_finding_signatures_enabled
=
false
)
new
(
json_data
,
report
,
vulnerability_finding_signatures_enabled
).
parse!
end
def
initialize
(
json_data
,
report
)
def
initialize
(
json_data
,
report
,
vulnerability_finding_signatures_enabled
=
false
)
@json_data
=
json_data
@report
=
report
@vulnerability_finding_signatures_enabled
=
vulnerability_finding_signatures_enabled
end
def
parse!
...
...
@@ -80,11 +81,20 @@ module Gitlab
links
=
create_links
(
data
[
'links'
])
location
=
create_location
(
data
[
'location'
]
||
{})
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
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Finding
.
new
(
uuid:
calculate_uuid_v5
(
identifiers
.
first
,
location
)
,
uuid:
uuid
,
report_type:
report
.
type
,
name:
finding_name
(
data
,
identifiers
,
location
),
compare_key:
data
[
'cve'
]
||
''
,
...
...
@@ -99,14 +109,17 @@ module Gitlab
raw_metadata:
data
.
to_json
,
metadata_version:
report_version
,
details:
data
[
'details'
]
||
{},
signatures:
signatures
))
signatures:
signatures
,
project_id:
report
.
project_id
,
vulnerability_finding_signatures_enabled:
@vulnerability_finding_signatures_enabled
))
end
def
create_signatures
(
data
)
return
[]
if
data
.
nil?
||
data
[
'items'
].
nil?
def
create_signatures
(
location
,
tracking
)
tracking
||=
{
'items'
=>
[]
}
signature_algorithms
=
Hash
.
new
{
|
hash
,
key
|
hash
[
key
]
=
[]
}
data
[
'items'
].
each
do
|
item
|
tracking
[
'items'
].
each
do
|
item
|
next
unless
item
.
key?
(
'signatures'
)
item
[
'signatures'
].
each
do
|
signature
|
...
...
@@ -117,14 +130,16 @@ module Gitlab
signature_algorithms
.
map
do
|
algorithm
,
values
|
value
=
values
.
join
(
'|'
)
begin
signature
=
::
Gitlab
::
Ci
::
Reports
::
Security
::
FindingSignature
.
new
(
algorithm_type:
algorithm
,
signature_value:
value
)
signature
.
valid?
?
signature
:
nil
rescue
ArgumentError
=>
e
Gitlab
::
ErrorTracking
.
track_and_raise_for_dev_exception
(
e
)
signature
=
::
Gitlab
::
Ci
::
Reports
::
Security
::
FindingSignature
.
new
(
algorithm_type:
algorithm
,
signature_value:
value
)
if
signature
.
valid?
signature
else
e
=
SecurityReportParserError
.
new
(
"Vulnerability tracking signature is not valid:
#{
signature
}
"
)
Gitlab
::
ErrorTracking
.
track_exception
(
e
)
nil
end
end
.
compact
...
...
@@ -201,11 +216,11 @@ module Gitlab
"
#{
identifier
.
name
}
in
#{
location
&
.
fingerprint_path
}
"
end
def
calculate_uuid_v5
(
primary_identifier
,
location
)
def
calculate_uuid_v5
(
primary_identifier
,
location
_fingerprint
)
uuid_v5_name_components
=
{
report_type:
report
.
type
,
primary_identifier_fingerprint:
primary_identifier
&
.
fingerprint
,
location_fingerprint:
location
&
.
fingerprint
,
location_fingerprint:
location
_
fingerprint
,
project_id:
report
.
project_id
}
...
...
ee/lib/gitlab/ci/reports/security/finding.rb
View file @
56fcaba4
...
...
@@ -5,6 +5,8 @@ module Gitlab
module
Reports
module
Security
class
Finding
include
::
VulnerabilityFindingHelpers
UNSAFE_SEVERITIES
=
%w[unknown high critical]
.
freeze
attr_reader
:compare_key
...
...
@@ -25,10 +27,11 @@ module Gitlab
attr_reader
:remediations
attr_reader
:details
attr_reader
:signatures
attr_reader
:project_id
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
@confidence
=
confidence
@identifiers
=
identifiers
...
...
@@ -45,6 +48,8 @@ module Gitlab
@remediations
=
remediations
@details
=
details
@signatures
=
signatures
@project_id
=
project_id
@vulnerability_finding_signatures_enabled
=
vulnerability_finding_signatures_enabled
@project_fingerprint
=
generate_project_fingerprint
end
...
...
@@ -66,6 +71,7 @@ module Gitlab
severity
uuid
details
signatures
]
.
each_with_object
({})
do
|
key
,
hash
|
hash
[
key
]
=
public_send
(
key
)
# rubocop:disable GitlabSecurity/PublicSend
end
...
...
@@ -85,13 +91,22 @@ module Gitlab
end
def
eql?
(
other
)
report_type
==
other
.
report_type
&&
location
.
fingerprint
==
other
.
location
.
fingerprint
&&
primary_fingerprint
==
other
.
primary_fingerprint
return
false
unless
report_type
==
other
.
report_type
&&
primary_identifier_fingerprint
==
other
.
primary_identifier_fingerprint
if
@vulnerability_finding_signatures_enabled
matches_signatures
(
other
.
signatures
,
other
.
uuid
)
else
location
.
fingerprint
==
other
.
location
.
fingerprint
end
end
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
def
valid?
...
...
@@ -104,7 +119,7 @@ module Gitlab
end
end
def
primary_fingerprint
def
primary_
identifier_
fingerprint
primary_identifier
&
.
fingerprint
end
...
...
ee/lib/gitlab/ci/reports/security/finding_signature.rb
View file @
56fcaba4
...
...
@@ -5,6 +5,8 @@ module Gitlab
module
Reports
module
Security
class
FindingSignature
include
VulnerabilityFindingSignatureHelpers
attr_accessor
:algorithm_type
,
:signature_value
def
initialize
(
params
=
{})
...
...
@@ -12,11 +14,19 @@ module Gitlab
@signature_value
=
params
.
dig
(
:signature_value
)
end
def
priority
::
Vulnerabilities
::
FindingSignature
.
priority
(
algorithm_type
)
end
def
signature_sha
Digest
::
SHA1
.
digest
(
signature_value
)
end
def
to_h
def
signature_hex
signature_sha
.
unpack1
(
"H*"
)
end
def
to_hash
{
algorithm_type:
algorithm_type
,
signature_sha:
signature_sha
...
...
@@ -26,6 +36,13 @@ module Gitlab
def
valid?
::
Vulnerabilities
::
FindingSignature
.
algorithm_types
.
key?
(
algorithm_type
)
end
def
eql?
(
other
)
other
.
algorithm_type
==
algorithm_type
&&
other
.
signature_sha
==
signature_sha
end
alias_method
:==
,
:eql?
end
end
end
...
...
ee/lib/gitlab/ci/reports/security/locations/container_scanning.rb
View file @
56fcaba4
...
...
@@ -18,12 +18,12 @@ module Gitlab
@package_version
=
package_version
end
private
def
fingerprint_data
"
#{
docker_image_name_without_tag
}
:
#{
package_name
}
"
end
private
def
docker_image_name_without_tag
base_name
,
version
=
image
.
split
(
':'
)
...
...
ee/lib/gitlab/ci/reports/security/locations/coverage_fuzzing.rb
View file @
56fcaba4
...
...
@@ -16,8 +16,6 @@ module Gitlab
@crash_state
=
crash_state
end
private
def
fingerprint_data
"
#{
crash_type
}
:
#{
crash_state
}
"
end
...
...
ee/lib/gitlab/ci/reports/security/locations/dast.rb
View file @
56fcaba4
...
...
@@ -20,8 +20,6 @@ module Gitlab
alias_method
:fingerprint_path
,
:path
private
def
fingerprint_data
"
#{
path
}
:
#{
method_name
}
:
#{
param
}
"
end
...
...
ee/lib/gitlab/ci/reports/security/locations/dependency_scanning.rb
View file @
56fcaba4
...
...
@@ -18,8 +18,6 @@ module Gitlab
@package_version
=
package_version
end
private
def
fingerprint_data
"
#{
file_path
}
:
#{
package_name
}
"
end
...
...
ee/lib/gitlab/ci/reports/security/locations/sast.rb
View file @
56fcaba4
...
...
@@ -22,8 +22,6 @@ module Gitlab
@start_line
=
start_line
end
private
def
fingerprint_data
"
#{
file_path
}
:
#{
start_line
}
:
#{
end_line
}
"
end
...
...
ee/lib/gitlab/ci/reports/security/locations/secret_detection.rb
View file @
56fcaba4
...
...
@@ -22,8 +22,6 @@ module Gitlab
@start_line
=
start_line
end
private
def
fingerprint_data
"
#{
file_path
}
:
#{
start_line
}
:
#{
end_line
}
"
end
...
...
ee/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
View file @
56fcaba4
...
...
@@ -7,13 +7,17 @@ module Gitlab
class
VulnerabilityReportsComparer
include
Gitlab
::
Utils
::
StrongMemoize
attr_reader
:base_report
,
:head_report
attr_reader
:base_report
,
:head_report
,
:added
,
:fixed
ACCEPTABLE_REPORT_AGE
=
1
.
week
def
initialize
(
base_report
,
head_report
)
@base_report
=
base_report
@head_report
=
head_report
@added
=
[]
@fixed
=
[]
calculate_changes
end
def
base_report_created_at
...
...
@@ -30,16 +34,30 @@ module Gitlab
ACCEPTABLE_REPORT_AGE
.
ago
>
@base_report
.
created_at
end
def
added
strong_memoize
(
:added
)
do
head_report
.
findings
-
base
_report
.
findings
end
end
def
calculate_changes
base_findings
=
base_report
.
findings
head_findings
=
head
_report
.
findings
head_findings_hash
=
head_findings
.
index_by
(
&
:object_id
)
def
fixed
strong_memoize
(
:fixed
)
do
base_report
.
findings
-
head_report
.
findings
# 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
@fixed
<<
base_finding
unless
still_exists
end
@added
=
head_findings_hash
.
values
end
end
end
...
...
ee/spec/factories/ci/reports/security/findings.rb
View file @
56fcaba4
...
...
@@ -39,6 +39,7 @@ FactoryBot.define do
project_id:
n
)
end
vulnerability_finding_signatures_enabled
{
false
}
skip_create
...
...
ee/spec/factories/vulnerabilities/feedback.rb
View file @
56fcaba4
...
...
@@ -17,6 +17,7 @@ FactoryBot.define do
category
{
'sast'
}
project_fingerprint
{
generate
(
:project_fingerprint
)
}
vulnerability_data
{
{
category:
'sast'
}
}
finding_uuid
{
Gitlab
::
UUID
.
v5
(
SecureRandom
.
hex
)
}
trait
:dismissal
do
feedback_type
{
'dismissal'
}
...
...
ee/spec/finders/security/pipeline_vulnerabilities_finder_spec.rb
View file @
56fcaba4
...
...
@@ -83,12 +83,12 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
# use the same number of queries, regardless of the number of findings
# 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
# this test is invalid if we don't have more
sast
findings than dep findings
expect
(
sast
_findings
.
count
).
to
be
>
dep_findings
.
count
# this test is invalid if we don't have more
container_scanning
findings than dep findings
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
# (the vulnerability is created with the :confirmed trait)
create
(
:vulnerabilities_finding
,
...
...
@@ -103,7 +103,7 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
# should be the same number of queries between different report types
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
{
described_class
.
new
(
pipeline:
pipeline
,
params:
{
report_type:
%w[dependency_scanning]
}).
execute
}
...
...
@@ -117,11 +117,11 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
orig_security_reports
=
pipeline
.
security_reports
new_finding
=
create
(
:ci_reports_security_finding
)
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
{
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
)
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
...
...
@@ -130,9 +130,11 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
context
'when sast'
do
let
(
:params
)
{
{
report_type:
%w[sast]
}
}
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
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
)
end
end
...
...
@@ -172,52 +174,107 @@ RSpec.describe Security::PipelineVulnerabilitiesFinder do
let
(
:ds_finding
)
{
pipeline
.
security_reports
.
reports
[
"dependency_scanning"
].
findings
.
first
}
let
(
:sast_finding
)
{
pipeline
.
security_reports
.
reports
[
"sast"
].
findings
.
first
}
let!
(
:feedback
)
do
[
create
(
:vulnerability_feedback
,
:dismissal
,
:dependency_scanning
,
project:
project
,
pipeline:
pipeline
,
project_fingerprint:
ds_finding
.
project_fingerprint
,
vulnerability_data:
ds_finding
.
raw_metadata
),
create
(
:vulnerability_feedback
,
:dismissal
,
:sast
,
project:
project
,
pipeline:
pipeline
,
project_fingerprint:
sast_finding
.
project_fingerprint
,
vulnerability_data:
sast_finding
.
raw_metadata
)
]
end
context
'when vulnerability_finding_signatures feature flag is disabled'
do
let!
(
:feedback
)
do
[
create
(
:vulnerability_feedback
,
:dismissal
,
:dependency_scanning
,
project:
project
,
pipeline:
pipeline
,
project_fingerprint:
ds_finding
.
project_fingerprint
,
vulnerability_data:
ds_finding
.
raw_metadata
,
finding_uuid:
ds_finding
.
uuid
),
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
context
'when unscoped'
do
subject
{
described_class
.
new
(
pipeline:
pipeline
).
execute
}
before
do
stub_feature_flags
(
vulnerability_finding_signatures:
false
)
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
))
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
end
context
'when `dismissed`'
do
subject
{
described_class
.
new
(
pipeline:
pipeline
,
params:
{
report_type:
%w[dependency_scanning]
,
scope:
'dismissed'
}
).
execute
}
context
'when `dismissed`'
do
subject
{
described_class
.
new
(
pipeline:
pipeline
,
params:
{
report_type:
%w[dependency_scanning]
,
scope:
'dismissed'
}
).
execute
}
it
'returns non-dismissed vulnerabilities'
do
expect
(
subject
.
findings
.
count
).
to
eq
(
ds_count
-
1
)
expect
(
subject
.
findings
.
map
(
&
:project_fingerprint
)).
not_to
include
(
ds_finding
.
project_fingerprint
)
it
'returns non-dismissed vulnerabilities'
do
expect
(
subject
.
findings
.
count
).
to
eq
(
ds_count
-
1
)
expect
(
subject
.
findings
.
map
(
&
:project_fingerprint
)).
not_to
include
(
ds_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
context
'when `all`'
do
let
(
:params
)
{
{
report_type:
%w[sast dast container_scanning dependency_scanning]
,
scope:
'all'
}
}
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
it
'returns all vulnerabilities'
do
expect
(
subject
.
findings
.
count
).
to
eq
(
cs_count
+
dast_count
+
ds_count
+
sast_count
)
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
...
...
ee/spec/lib/gitlab/ci/parsers/security/common_spec.rb
View file @
56fcaba4
...
...
@@ -4,212 +4,195 @@ require 'spec_helper'
RSpec
.
describe
Gitlab
::
Ci
::
Parsers
::
Security
::
Common
do
describe
'#parse!'
do
let_it_be
(
:pipeline
)
{
create
(
:ci_pipeline
)
}
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
(
:location
)
{
::
Gitlab
::
Ci
::
Reports
::
Security
::
Locations
::
DependencyScanning
.
new
(
file_path:
'yarn/yarn.lock'
,
package_version:
'v2'
,
package_name:
'saml2'
)
}
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
where
(
vulnerability_finding_signatures_enabled:
[
true
,
false
])
with_them
do
let_it_be
(
:pipeline
)
{
create
(
:ci_pipeline
)
}
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
(
:location
)
{
::
Gitlab
::
Ci
::
Reports
::
Security
::
Locations
::
DependencyScanning
.
new
(
file_path:
'yarn/yarn.lock'
,
package_version:
'v2'
,
package_name:
'saml2'
)
}
let
(
:tracking_data
)
{
nil
}
before
do
allow_next_instance_of
(
described_class
)
do
|
parser
|
allow
(
parser
).
to
receive
(
:create_location
).
and_return
(
location
)
allow
(
parser
).
to
receive
(
:tracking_data
).
and_return
(
tracking_data
)
end
before
do
allow_next_instance_of
(
described_class
)
do
|
parser
|
allow
(
parser
).
to
receive
(
:create_location
).
and_return
(
location
)
allow
(
parser
).
to
receive
(
:tracking_data
).
and_return
(
tracking_data
)
artifact
.
each_blob
{
|
blob
|
described_class
.
parse!
(
blob
,
report
,
vulnerability_finding_signatures_enabled
)
}
end
artifact
.
each_blob
{
|
blob
|
described_class
.
parse!
(
blob
,
report
)
}
end
describe
'parsing finding.name'
do
let
(
:artifact
)
{
build
(
:ee_ci_job_artifact
,
:common_security_report_with_blank_names
)
}
context
'when message is provided'
do
it
'sets message from the report as a finding name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1020'
}
expected_name
=
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
)[
'message'
]
describe
'parsing finding.name'
do
let
(
:artifact
)
{
build
(
:ee_ci_job_artifact
,
:common_security_report_with_blank_names
)
}
expect
(
vulnerability
.
name
).
to
eq
(
expected_name
)
end
end
context
'when message is not provided'
do
context
'and name is provided'
do
it
'sets name from the report as a name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1030'
}
expected_name
=
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
)[
'name'
]
context
'when message is provided'
do
it
'sets message from the report as a finding name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1020'
}
expected_name
=
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
)[
'message'
]
expect
(
vulnerability
.
name
).
to
eq
(
expected_name
)
end
end
context
'and name is not provided'
do
context
'when CVE identifier exists'
do
it
'combines identifier with location to create name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-2017-11429'
}
expect
(
vulnerability
.
name
).
to
eq
(
"CVE-2017-11429 in yarn.lock"
)
context
'when message is not provided'
do
context
'and name is provided'
do
it
'sets name from the report as a name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1030'
}
expected_name
=
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
)[
'name'
]
expect
(
vulnerability
.
name
).
to
eq
(
expected_name
)
end
end
context
'when CWE identifier exists'
do
it
'combines identifier with location to create name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CWE-2017-11429'
}
expect
(
vulnerability
.
name
).
to
eq
(
"CWE-2017-11429 in yarn.lock"
)
context
'and name is not provided'
do
context
'when CVE identifier exists'
do
it
'combines identifier with location to create name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-2017-11429'
}
expect
(
vulnerability
.
name
).
to
eq
(
"CVE-2017-11429 in yarn.lock"
)
end
end
context
'when CWE identifier exists'
do
it
'combines identifier with location to create name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CWE-2017-11429'
}
expect
(
vulnerability
.
name
).
to
eq
(
"CWE-2017-11429 in yarn.lock"
)
end
end
end
context
'when neither CVE nor CWE identifier exist'
do
it
'combines identifier with location to create name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'OTHER-2017-11429'
}
expect
(
vulnerability
.
name
).
to
eq
(
"other-2017-11429 in yarn.lock"
)
context
'when neither CVE nor CWE identifier exist'
do
it
'combines identifier with location to create name'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'OTHER-2017-11429'
}
expect
(
vulnerability
.
name
).
to
eq
(
"other-2017-11429 in yarn.lock"
)
end
end
end
end
end
end
describe
'parsing finding.details'
do
context
'when details are provided'
do
it
'sets details from the report'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1020'
}
expected_details
=
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
)[
'details'
]
describe
'parsing finding.details'
do
context
'when details are provided'
do
it
'sets details from the report'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1020'
}
expected_details
=
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
)[
'details'
]
expect
(
vulnerability
.
details
).
to
eq
(
expected_details
)
expect
(
vulnerability
.
details
).
to
eq
(
expected_details
)
end
end
end
context
'when details are not provided'
do
it
'sets empty hash'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1030'
}
expect
(
vulnerability
.
details
).
to
eq
({})
context
'when details are not provided'
do
it
'sets empty hash'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
'CVE-1030'
}
expect
(
vulnerability
.
details
).
to
eq
({})
end
end
end
end
describe
'parsing remediations'
do
let
(
:expected_remediation
)
{
create
(
:ci_reports_security_remediation
,
diff:
''
)
}
describe
'parsing remediations'
do
let
(
:expected_remediation
)
{
create
(
:ci_reports_security_remediation
,
diff:
''
)
}
it
'finds remediation with same cve'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
"CVE-1020"
}
remediation
=
{
'fixes'
=>
[{
'cve'
=>
'CVE-1020'
}],
'summary'
=>
''
,
'diff'
=>
''
}
it
'finds remediation with same cve'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
"CVE-1020"
}
remediation
=
{
'fixes'
=>
[{
'cve'
=>
'CVE-1020'
}],
'summary'
=>
''
,
'diff'
=>
''
}
expect
(
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
).
dig
(
'remediations'
).
first
).
to
include
remediation
expect
(
vulnerability
.
remediations
.
first
.
checksum
).
to
eq
(
expected_remediation
.
checksum
)
end
expect
(
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
).
dig
(
'remediations'
).
first
).
to
include
remediation
expect
(
vulnerability
.
remediations
.
first
.
checksum
).
to
eq
(
expected_remediation
.
checksum
)
end
it
'finds remediation with same id'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
"CVE-1030"
}
remediation
=
{
'fixes'
=>
[{
'cve'
=>
'CVE'
,
'id'
=>
'bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3'
}],
'summary'
=>
''
,
'diff'
=>
''
}
it
'finds remediation with same id'
do
vulnerability
=
report
.
findings
.
find
{
|
x
|
x
.
compare_key
==
"CVE-1030"
}
remediation
=
{
'fixes'
=>
[{
'cve'
=>
'CVE'
,
'id'
=>
'bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3'
}],
'summary'
=>
''
,
'diff'
=>
''
}
expect
(
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
).
dig
(
'remediations'
).
first
).
to
include
remediation
expect
(
vulnerability
.
remediations
.
first
.
checksum
).
to
eq
(
expected_remediation
.
checksum
)
end
expect
(
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
).
dig
(
'remediations'
).
first
).
to
include
remediation
expect
(
vulnerability
.
remediations
.
first
.
checksum
).
to
eq
(
expected_remediation
.
checksum
)
end
it
'does not find remediation with different id'
do
fix_with_id
=
{
"fixes"
:
[
{
"id"
:
"2134"
,
"cve"
:
"CVE-1"
}
],
"summary"
:
""
,
"diff"
:
""
}
it
'does not find remediation with different id'
do
fix_with_id
=
{
"fixes"
:
[
{
"id"
:
"2134"
,
"cve"
:
"CVE-1"
}
],
"summary"
:
""
,
"diff"
:
""
}
report
.
findings
.
map
do
|
vulnerability
|
expect
(
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
).
dig
(
'remediations'
)).
not_to
include
(
fix_with_id
)
report
.
findings
.
map
do
|
vulnerability
|
expect
(
Gitlab
::
Json
.
parse
(
vulnerability
.
raw_metadata
).
dig
(
'remediations'
)).
not_to
include
(
fix_with_id
)
end
end
end
end
describe
'parsing scanners'
do
subject
(
:scanner
)
{
report
.
findings
.
first
.
scanner
}
describe
'parsing scanners'
do
subject
(
:scanner
)
{
report
.
findings
.
first
.
scanner
}
context
'when vendor is not missing in scanner'
do
it
'returns scanner with parsed vendor value'
do
expect
(
scanner
.
vendor
).
to
eq
(
'GitLab'
)
context
'when vendor is not missing in scanner'
do
it
'returns scanner with parsed vendor value'
do
expect
(
scanner
.
vendor
).
to
eq
(
'GitLab'
)
end
end
end
end
describe
'parsing scan'
do
it
'returns scan object for each finding'
do
scans
=
report
.
findings
.
map
(
&
:scan
)
describe
'parsing scan'
do
it
'returns scan object for each finding'
do
scans
=
report
.
findings
.
map
(
&
:scan
)
expect
(
scans
.
map
(
&
:status
).
all?
(
'success'
)).
to
be
(
true
)
expect
(
scans
.
map
(
&
:type
).
all?
(
'dependency_scanning'
)).
to
be
(
true
)
expect
(
scans
.
map
(
&
:start_time
).
all?
(
'placeholder-value'
)).
to
be
(
true
)
expect
(
scans
.
map
(
&
:end_time
).
all?
(
'placeholder-value'
)).
to
be
(
true
)
expect
(
scans
.
size
).
to
eq
(
3
)
expect
(
scans
.
first
).
to
be_a
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Scan
)
end
expect
(
scans
.
map
(
&
:status
).
all?
(
'success'
)).
to
be
(
true
)
expect
(
scans
.
map
(
&
:type
).
all?
(
'dependency_scanning'
)).
to
be
(
true
)
expect
(
scans
.
map
(
&
:start_time
).
all?
(
'placeholder-value'
)).
to
be
(
true
)
expect
(
scans
.
map
(
&
:end_time
).
all?
(
'placeholder-value'
)).
to
be
(
true
)
expect
(
scans
.
size
).
to
eq
(
3
)
expect
(
scans
.
first
).
to
be_a
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Scan
)
end
it
'returns nil when scan is not a hash'
do
empty_report
=
Gitlab
::
Ci
::
Reports
::
Security
::
Report
.
new
(
artifact
.
file_type
,
pipeline
,
2
.
weeks
.
ago
)
described_class
.
parse!
({}.
to_json
,
empty_report
)
it
'returns nil when scan is not a hash'
do
empty_report
=
Gitlab
::
Ci
::
Reports
::
Security
::
Report
.
new
(
artifact
.
file_type
,
pipeline
,
2
.
weeks
.
ago
)
described_class
.
parse!
({}.
to_json
,
empty_report
)
expect
(
empty_report
.
scan
).
to
be
(
nil
)
expect
(
empty_report
.
scan
).
to
be
(
nil
)
end
end
end
describe
'parsing links'
do
it
'returns links object for each finding'
,
:aggregate_failures
do
links
=
report
.
findings
.
flat_map
(
&
:links
)
describe
'parsing links'
do
it
'returns links object for each finding'
,
:aggregate_failures
do
links
=
report
.
findings
.
flat_map
(
&
:links
)
expect
(
links
.
map
(
&
:url
)).
to
match_array
([
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020'
,
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'
])
expect
(
links
.
map
(
&
:name
)).
to
match_array
([
nil
,
'CVE-1030'
])
expect
(
links
.
size
).
to
eq
(
2
)
expect
(
links
.
first
).
to
be_a
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Link
)
end
end
describe
'setting the uuid'
do
let
(
:finding_uuids
)
{
report
.
findings
.
map
(
&
:uuid
)
}
let
(
:uuid_1
)
do
Security
::
VulnerabilityUUID
.
generate
(
report_type:
"dependency_scanning"
,
primary_identifier_fingerprint:
"4ff8184cd18485b6e85d5b101e341b12eacd1b3b"
,
location_fingerprint:
"33dc9f32c77dde16d39c69d3f78f27ca3114a7c5"
,
project_id:
pipeline
.
project_id
)
expect
(
links
.
map
(
&
:url
)).
to
match_array
([
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020'
,
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030'
])
expect
(
links
.
map
(
&
:name
)).
to
match_array
([
nil
,
'CVE-1030'
])
expect
(
links
.
size
).
to
eq
(
2
)
expect
(
links
.
first
).
to
be_a
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Link
)
end
end
let
(
:uuid_2
)
do
Security
::
VulnerabilityUUID
.
generate
(
report_type:
"dependency_scanning"
,
primary_identifier_fingerprint:
"d55f9e66e79882ae63af9fd55cc822ab75307e31"
,
location_fingerprint:
"33dc9f32c77dde16d39c69d3f78f27ca3114a7c5"
,
project_id:
pipeline
.
project_id
)
end
describe
'setting the uuid'
do
let
(
:finding_uuids
)
{
report
.
findings
.
map
(
&
:uuid
)
}
let
(
:uuid_1
)
do
Security
::
VulnerabilityUUID
.
generate
(
report_type:
"dependency_scanning"
,
primary_identifier_fingerprint:
"4ff8184cd18485b6e85d5b101e341b12eacd1b3b"
,
location_fingerprint:
"33dc9f32c77dde16d39c69d3f78f27ca3114a7c5"
,
project_id:
pipeline
.
project_id
)
end
let
(
:expected_uuids
)
{
[
uuid_1
,
uuid_2
,
nil
]
}
let
(
:uuid_2
)
do
Security
::
VulnerabilityUUID
.
generate
(
report_type:
"dependency_scanning"
,
primary_identifier_fingerprint:
"d55f9e66e79882ae63af9fd55cc822ab75307e31"
,
location_fingerprint:
"33dc9f32c77dde16d39c69d3f78f27ca3114a7c5"
,
project_id:
pipeline
.
project_id
)
end
it
'sets the UUIDv5 for findings'
,
:aggregate_failures
do
expect
(
finding_uuids
).
to
match_array
(
expected_uuids
)
end
end
let
(
:expected_uuids
)
{
[
uuid_1
,
uuid_2
,
nil
]
}
describe
'parsing signature'
do
context
'with valid signature information'
do
it
'creates signatures for each algorithm'
do
finding
=
report
.
findings
.
first
expect
(
finding
.
signatures
.
size
).
to
eq
(
3
)
expect
(
finding
.
signatures
.
map
(
&
:algorithm_type
).
to_set
).
to
eq
(
Set
[
'hash'
,
'location'
,
'scope_offset'
])
it
'sets the UUIDv5 for findings'
,
:aggregate_failures
do
expect
(
finding_uuids
).
to
match_array
(
expected_uuids
)
end
end
context
'with invalid signature information
'
do
describe
'parsing tracking
'
do
let
(
:tracking_data
)
do
{
'type'
=>
'source'
,
...
...
@@ -217,16 +200,66 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do
'signatures'
=>
[
{
'algorithm'
=>
'hash'
,
'value'
=>
'hash_value'
},
{
'algorithm'
=>
'location'
,
'value'
=>
'location_value'
},
{
'algorithm'
=>
'
INVALID
'
,
'value'
=>
'scope_offset_value'
}
{
'algorithm'
=>
'
scope_offset
'
,
'value'
=>
'scope_offset_value'
}
]
]
}
end
it
'ignores invalid algorithm types'
do
finding
=
report
.
findings
.
first
expect
(
finding
.
signatures
.
size
).
to
eq
(
2
)
expect
(
finding
.
signatures
.
map
(
&
:algorithm_type
).
to_set
).
to
eq
(
Set
[
'hash'
,
'location'
])
context
'with valid tracking information'
do
it
'creates signatures for each algorithm'
do
finding
=
report
.
findings
.
first
expect
(
finding
.
signatures
.
size
).
to
eq
(
3
)
expect
(
finding
.
signatures
.
map
(
&
:algorithm_type
).
to_set
).
to
eq
(
Set
[
'hash'
,
'location'
,
'scope_offset'
])
end
end
context
'with invalid tracking information'
do
let
(
:tracking_data
)
do
{
'type'
=>
'source'
,
'items'
=>
[
'signatures'
=>
[
{
'algorithm'
=>
'hash'
,
'value'
=>
'hash_value'
},
{
'algorithm'
=>
'location'
,
'value'
=>
'location_value'
},
{
'algorithm'
=>
'INVALID'
,
'value'
=>
'scope_offset_value'
}
]
]
}
end
it
'ignores invalid algorithm types'
do
finding
=
report
.
findings
.
first
expect
(
finding
.
signatures
.
size
).
to
eq
(
2
)
expect
(
finding
.
signatures
.
map
(
&
:algorithm_type
).
to_set
).
to
eq
(
Set
[
'hash'
,
'location'
])
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
...
...
ee/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb
View file @
56fcaba4
...
...
@@ -18,15 +18,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do
expect
(
subject
.
algorithm_type
).
to
eq
(
params
[
:algorithm_type
])
expect
(
subject
.
signature_value
).
to
eq
(
params
[
:signature_value
])
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
])
)
describe
'#valid?'
do
it
'returns true'
do
expect
(
subject
.
valid?
).
to
eq
(
true
)
end
end
end
end
...
...
@@ -50,4 +47,13 @@ RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do
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
ee/spec/lib/gitlab/ci/reports/security/finding_spec.rb
View file @
56fcaba4
...
...
@@ -137,7 +137,8 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
scan:
occurrence
.
scan
,
severity:
occurrence
.
severity
,
uuid:
occurrence
.
uuid
,
details:
occurrence
.
details
details:
occurrence
.
details
,
signatures:
[]
})
end
end
...
...
@@ -197,87 +198,98 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
end
describe
'#eql?'
do
let
(
:identifier
)
{
build
(
:ci_reports_security_identifier
)
}
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
(
:report_type
)
{
:secret_detection
}
let
(
:identifier_external_id
)
{
'foo'
}
let
(
:location_start_line
)
{
0
}
let
(
:other_identifier
)
{
build
(
:ci_reports_security_identifier
,
external_id:
identifier_external_id
)
}
let
(
:other_location
)
{
build
(
:ci_reports_security_locations_sast
,
start_line:
location_start_line
)
}
let
(
:other_finding
)
do
build
(
:ci_reports_security_finding
,
severity:
'low'
,
report_type:
report_type
,
identifiers:
[
other_identifier
],
location:
other_location
)
end
where
(
vulnerability_finding_signatures_enabled:
[
true
,
false
])
with_them
do
let
(
:identifier
)
{
build
(
:ci_reports_security_identifier
)
}
let
(
:location
)
{
build
(
:ci_reports_security_locations_sast
)
}
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
(
:identifier_external_id
)
{
'foo'
}
let
(
:location_start_line
)
{
0
}
let
(
:other_identifier
)
{
build
(
:ci_reports_security_identifier
,
external_id:
identifier_external_id
)
}
let
(
:other_location
)
{
build
(
:ci_reports_security_locations_sast
,
start_line:
location_start_line
)
}
let
(
:other_finding
)
do
build
(
:ci_reports_security_finding
,
severity:
'low'
,
report_type:
report_type
,
identifiers:
[
other_identifier
],
location:
other_location
,
vulnerability_finding_signatures_enabled:
vulnerability_finding_signatures_enabled
)
end
subject
{
finding
.
eql?
(
other_finding
)
}
let
(
:signature
)
{
::
Gitlab
::
Ci
::
Reports
::
Security
::
FindingSignature
.
new
(
algorithm_type:
'location'
,
signature_value:
'value1'
)
}
context
'when the primary_identifier is nil'
do
let
(
:identifier
)
{
nil
}
subject
{
finding
.
eql?
(
other_finding
)
}
it
'does not raise an exception'
do
expect
{
subject
}.
not_to
raise_error
context
'when the primary_identifier is nil'
do
let
(
:identifier
)
{
nil
}
it
'does not raise an exception'
do
expect
{
subject
}.
not_to
raise_error
end
end
end
context
'when the other finding has same `report_type`'
do
let
(
:report_type
)
{
:sast
}
context
'when the other finding has same `report_type`'
do
let
(
:report_type
)
{
:sast
}
context
'when the other finding has same primary identifier fingerprint'
do
let
(
:identifier_external_id
)
{
identifier
.
external_id
}
context
'when the other finding has same primary identifier fingerprint'
do
let
(
:identifier_external_id
)
{
identifier
.
external_id
}
context
'when the other finding has same location fingerprint'
do
let
(
:location_start_line
)
{
location
.
start_line
}
context
'when the other finding has same location signature'
do
before
do
finding
.
signatures
<<
signature
other_finding
.
signatures
<<
signature
end
it
{
is_expected
.
to
be
(
true
)
}
end
let
(
:location_start_line
)
{
location
.
start_line
}
context
'when the other finding does not have same location fingerprint'
do
it
{
is_expected
.
to
be
(
false
)
}
it
{
is_expected
.
to
be
(
true
)
}
end
context
'when the other finding does not have same location signature'
do
it
{
is_expected
.
to
be
(
false
)
}
end
end
end
context
'when the other finding does not have same primary identifier fingerprint'
do
context
'when the other finding has same location fingerprint
'
do
let
(
:location_start_line
)
{
location
.
start_line
}
context
'when the other finding does not have same primary identifier fingerprint'
do
context
'when the other finding has same location signature
'
do
let
(
:location_start_line
)
{
location
.
start_line
}
it
{
is_expected
.
to
be
(
false
)
}
end
it
{
is_expected
.
to
be
(
false
)
}
end
context
'when the other finding does not have same location fingerprint'
do
it
{
is_expected
.
to
be
(
false
)
}
context
'when the other finding does not have same location signature'
do
it
{
is_expected
.
to
be
(
false
)
}
end
end
end
end
context
'when the other finding does not have same `report_type`'
do
context
'when the other finding has same primary identifier fingerprint'
do
let
(
:identifier_external_id
)
{
identifier
.
external_id
}
context
'when the other finding does not have same `report_type`'
do
context
'when the other finding has same primary identifier fingerprint'
do
let
(
:identifier_external_id
)
{
identifier
.
external_id
}
context
'when the other finding has same location fingerprint
'
do
let
(
:location_start_line
)
{
location
.
start_line
}
context
'when the other finding has same location signature
'
do
let
(
:location_start_line
)
{
location
.
start_line
}
it
{
is_expected
.
to
be
(
false
)
}
end
it
{
is_expected
.
to
be
(
false
)
}
end
context
'when the other finding does not have same location fingerprint'
do
it
{
is_expected
.
to
be
(
false
)
}
context
'when the other finding does not have same location signature'
do
it
{
is_expected
.
to
be
(
false
)
}
end
end
end
context
'when the other finding does not have same primary identifier fingerprint'
do
context
'when the other finding has same location fingerprint
'
do
let
(
:location_start_line
)
{
location
.
start_line
}
context
'when the other finding does not have same primary identifier fingerprint'
do
context
'when the other finding has same location signature
'
do
let
(
:location_start_line
)
{
location
.
start_line
}
it
{
is_expected
.
to
be
(
false
)
}
end
it
{
is_expected
.
to
be
(
false
)
}
end
context
'when the other finding does not have same location fingerprint'
do
it
{
is_expected
.
to
be
(
false
)
}
context
'when the other finding does not have same location signature'
do
it
{
is_expected
.
to
be
(
false
)
}
end
end
end
end
...
...
@@ -345,4 +357,55 @@ RSpec.describe Gitlab::Ci::Reports::Security::Finding do
it
{
is_expected
.
to
match_array
(
expected_keys
)
}
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
ee/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb
View file @
56fcaba4
...
...
@@ -8,128 +8,133 @@ 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_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
])}
before
do
allow
(
base_vulnerability
).
to
receive
(
:location
).
and_return
({})
allow
(
head_vulnerability
).
to
receive
(
:location
).
and_return
({})
end
subject
{
described_class
.
new
(
base_report
,
head_report
)
}
describe
'#base_report_out_of_date'
do
context
'no base report'
do
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[],
findings:
[])}
where
(
vulnerability_finding_signatures_enabled:
[
true
,
false
])
it
'is not out of date'
do
expect
(
subject
.
base_report_out_of_date
).
to
be
false
end
with_them
do
before
do
allow
(
base_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
context
'base report older than one week
'
do
let
(
:report
)
{
build
(
:ci_reports_security_report
,
created_at:
1
.
week
.
ago
-
60
.
seconds
)
}
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[
report
])}
describe
'#base_report_out_of_date
'
do
context
'no base report'
do
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[],
findings:
[
])}
it
'is not out of date'
do
expect
(
subject
.
base_report_out_of_date
).
to
be
true
it
'is not out of date'
do
expect
(
subject
.
base_report_out_of_date
).
to
be
false
end
end
end
context
'base report less than one week old'
do
let
(
:report
)
{
build
(
:ci_reports_security_report
,
created_at:
1
.
week
.
ago
+
60
.
seconds
)
}
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[
report
])}
context
'base report older than one week'
do
let
(
:report
)
{
build
(
:ci_reports_security_report
,
created_at:
1
.
week
.
ago
-
60
.
seconds
)
}
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[
report
])}
it
'is not out of date'
do
expect
(
subject
.
base_report_out_of_date
).
to
be
true
end
end
context
'base report less than one week old'
do
let
(
:report
)
{
build
(
:ci_reports_security_report
,
created_at:
1
.
week
.
ago
+
60
.
seconds
)
}
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[
report
])}
it
'is not out of date'
do
expect
(
subject
.
base_report_out_of_date
).
to
be
false
it
'is not out of date'
do
expect
(
subject
.
base_report_out_of_date
).
to
be
false
end
end
end
end
describe
'#added'
do
let
(
:vuln
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'888'
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
Enums
::
Vulnerability
.
severity_levels
[
:critical
])
}
let
(
:low_vuln
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'888'
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
Enums
::
Vulnerability
.
severity_levels
[
:low
])
}
describe
'#added'
do
let
(
:vuln
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'888'
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
Enums
::
Vulnerability
.
severity_levels
[
:critical
])
}
let
(
:low_vuln
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'888'
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
Enums
::
Vulnerability
.
severity_levels
[
:low
])
}
context
'with new vulnerability'
do
let
(
:head_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
head_vulnerability
,
vuln
])}
context
'with new vulnerability'
do
let
(
:head_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
head_vulnerability
,
vuln
])}
it
'points to source tree'
do
expect
(
subject
.
added
).
to
eq
([
vuln
])
it
'points to source tree'
do
expect
(
subject
.
added
).
to
eq
([
vuln
])
end
end
end
context
'when comparing reports with different fingerprints'
do
let
(
:base_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'A'
)
}
let
(
:head_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'B'
)
}
let
(
:head_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
head_vulnerability
,
vuln
])}
context
'when comparing reports with different fingerprints'
do
let
(
:base_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'A'
)
}
let
(
:head_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'B'
)
}
let
(
:head_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
head_vulnerability
,
vuln
])}
it
'does not find any overlap'
do
expect
(
subject
.
added
).
to
eq
([
head_vulnerability
,
vuln
])
it
'does not find any overlap'
do
expect
(
subject
.
added
).
to
eq
([
head_vulnerability
,
vuln
])
end
end
end
context
'order'
do
let
(
:head_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
head_vulnerability
,
vuln
,
low_vuln
])}
context
'order'
do
let
(
:head_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
head_vulnerability
,
vuln
,
low_vuln
])}
it
'does not change'
do
expect
(
subject
.
added
).
to
eq
([
vuln
,
low_vuln
])
it
'does not change'
do
expect
(
subject
.
added
).
to
eq
([
vuln
,
low_vuln
])
end
end
end
end
describe
'#fixed'
do
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
]
)
}
describe
'#fixed'
do
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
],
uuid:
vuln
.
uuid
)
}
context
'with fixed vulnerability'
do
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
base_vulnerability
,
vuln
])}
context
'with fixed vulnerability'
do
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
base_vulnerability
,
vuln
])}
it
'points to base tree'
do
expect
(
subject
.
fixed
).
to
eq
([
vuln
])
it
'points to base tree'
do
expect
(
subject
.
fixed
).
to
eq
([
vuln
])
end
end
end
context
'when comparing reports with different fingerprints'
do
let
(
:base_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'A'
)
}
let
(
:head_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'B'
)
}
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
base_vulnerability
,
vuln
])}
context
'when comparing reports with different fingerprints'
do
let
(
:base_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'A'
)
}
let
(
:head_vulnerability
)
{
build
(
:vulnerabilities_finding
,
report_type: :sast
,
identifiers:
[
identifier
],
location_fingerprint:
'B'
)
}
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
base_vulnerability
,
vuln
])}
it
'does not find any overlap'
do
expect
(
subject
.
fixed
).
to
eq
([
base_vulnerability
,
vuln
])
it
'does not find any overlap'
do
expect
(
subject
.
fixed
).
to
eq
([
base_vulnerability
,
vuln
])
end
end
end
context
'order'
do
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
vuln
,
medium_vuln
,
base_vulnerability
])}
context
'order'
do
let
(
:base_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
findings:
[
vuln
,
medium_vuln
,
base_vulnerability
])}
it
'does not change'
do
expect
(
subject
.
fixed
).
to
eq
([
vuln
,
medium_vuln
])
it
'does not change'
do
expect
(
subject
.
fixed
).
to
eq
([
vuln
,
medium_vuln
])
end
end
end
end
describe
'with empty vulnerabilities'
do
let
(
:empty_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[],
findings:
[])}
describe
'with empty vulnerabilities'
do
let
(
:empty_report
)
{
build
(
:ci_reports_security_aggregated_reports
,
reports:
[],
findings:
[])}
it
'returns empty array when reports are not present'
do
comparer
=
described_class
.
new
(
empty_report
,
empty_report
)
it
'returns empty array when reports are not present'
do
comparer
=
described_class
.
new
(
empty_report
,
empty_report
)
expect
(
comparer
.
fixed
).
to
eq
([])
expect
(
comparer
.
added
).
to
eq
([])
end
expect
(
comparer
.
fixed
).
to
eq
([])
expect
(
comparer
.
added
).
to
eq
([])
end
it
'returns added vulnerability when base is empty and head is not empty'
do
comparer
=
described_class
.
new
(
empty_report
,
head_report
)
it
'returns added vulnerability when base is empty and head is not empty'
do
comparer
=
described_class
.
new
(
empty_report
,
head_report
)
expect
(
comparer
.
fixed
).
to
eq
([])
expect
(
comparer
.
added
).
to
eq
([
head_vulnerability
])
end
expect
(
comparer
.
fixed
).
to
eq
([])
expect
(
comparer
.
added
).
to
eq
([
head_vulnerability
])
end
it
'returns fixed vulnerability when head is empty and base is not empty'
do
comparer
=
described_class
.
new
(
base_report
,
empty_report
)
it
'returns fixed vulnerability when head is empty and base is not empty'
do
comparer
=
described_class
.
new
(
base_report
,
empty_report
)
expect
(
comparer
.
fixed
).
to
eq
([
base_vulnerability
])
expect
(
comparer
.
added
).
to
eq
([])
expect
(
comparer
.
fixed
).
to
eq
([
base_vulnerability
])
expect
(
comparer
.
added
).
to
eq
([])
end
end
end
end
ee/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb
0 → 100644
View file @
56fcaba4
# 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
ee/spec/models/vulnerabilities/finding_spec.rb
View file @
56fcaba4
...
...
@@ -7,638 +7,619 @@ RSpec.describe Vulnerabilities::Finding do
it
{
is_expected
.
to
define_enum_for
(
:report_type
)
}
it
{
is_expected
.
to
define_enum_for
(
:severity
)
}
describe
'associations'
do
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
(
:scanner
).
class_name
(
'Vulnerabilities::Scanner'
)
}
it
{
is_expected
.
to
belong_to
(
:vulnerability
).
inverse_of
(
:findings
)
}
it
{
is_expected
.
to
have_many
(
:pipelines
).
class_name
(
'Ci::Pipeline'
)
}
it
{
is_expected
.
to
have_many
(
:finding_pipelines
).
class_name
(
'Vulnerabilities::FindingPipeline'
).
with_foreign_key
(
'occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:identifiers
).
class_name
(
'Vulnerabilities::Identifier'
)
}
it
{
is_expected
.
to
have_many
(
:finding_identifiers
).
class_name
(
'Vulnerabilities::FindingIdentifier'
).
with_foreign_key
(
'occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:finding_links
).
class_name
(
'Vulnerabilities::FindingLink'
).
with_foreign_key
(
'vulnerability_occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:finding_remediations
).
class_name
(
'Vulnerabilities::FindingRemediation'
).
with_foreign_key
(
'vulnerability_occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:remediations
).
through
(
:finding_remediations
)
}
it
{
is_expected
.
to
have_many
(
:finding_evidences
).
class_name
(
'Vulnerabilities::FindingEvidence'
).
with_foreign_key
(
'vulnerability_occurrence_id'
)
}
end
describe
'validations'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
it
{
is_expected
.
to
validate_presence_of
(
:scanner
)
}
it
{
is_expected
.
to
validate_presence_of
(
:project
)
}
it
{
is_expected
.
to
validate_presence_of
(
:uuid
)
}
it
{
is_expected
.
to
validate_presence_of
(
:project_fingerprint
)
}
it
{
is_expected
.
to
validate_presence_of
(
:primary_identifier
)
}
it
{
is_expected
.
to
validate_presence_of
(
:location_fingerprint
)
}
it
{
is_expected
.
to
validate_presence_of
(
:name
)
}
it
{
is_expected
.
to
validate_presence_of
(
:report_type
)
}
it
{
is_expected
.
to
validate_presence_of
(
:metadata_version
)
}
it
{
is_expected
.
to
validate_presence_of
(
:raw_metadata
)
}
it
{
is_expected
.
to
validate_presence_of
(
:severity
)
}
it
{
is_expected
.
to
validate_presence_of
(
:confidence
)
}
it
{
is_expected
.
to
validate_length_of
(
:description
).
is_at_most
(
15000
)
}
it
{
is_expected
.
to
validate_length_of
(
:message
).
is_at_most
(
3000
)
}
it
{
is_expected
.
to
validate_length_of
(
:solution
).
is_at_most
(
7000
)
}
it
{
is_expected
.
to
validate_length_of
(
:cve
).
is_at_most
(
48400
)
}
context
'when value for details field is valid'
do
it
'is valid'
do
finding
.
details
=
{}
expect
(
finding
).
to
be_valid
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
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
(
:scanner
).
class_name
(
'Vulnerabilities::Scanner'
)
}
it
{
is_expected
.
to
belong_to
(
:vulnerability
).
inverse_of
(
:findings
)
}
it
{
is_expected
.
to
have_many
(
:pipelines
).
class_name
(
'Ci::Pipeline'
)
}
it
{
is_expected
.
to
have_many
(
:finding_pipelines
).
class_name
(
'Vulnerabilities::FindingPipeline'
).
with_foreign_key
(
'occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:identifiers
).
class_name
(
'Vulnerabilities::Identifier'
)
}
it
{
is_expected
.
to
have_many
(
:finding_identifiers
).
class_name
(
'Vulnerabilities::FindingIdentifier'
).
with_foreign_key
(
'occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:finding_links
).
class_name
(
'Vulnerabilities::FindingLink'
).
with_foreign_key
(
'vulnerability_occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:finding_remediations
).
class_name
(
'Vulnerabilities::FindingRemediation'
).
with_foreign_key
(
'vulnerability_occurrence_id'
)
}
it
{
is_expected
.
to
have_many
(
:remediations
).
through
(
:finding_remediations
)
}
it
{
is_expected
.
to
have_many
(
:finding_evidences
).
class_name
(
'Vulnerabilities::FindingEvidence'
).
with_foreign_key
(
'vulnerability_occurrence_id'
)
}
end
describe
'validations'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
it
{
is_expected
.
to
validate_presence_of
(
:scanner
)
}
it
{
is_expected
.
to
validate_presence_of
(
:project
)
}
it
{
is_expected
.
to
validate_presence_of
(
:uuid
)
}
it
{
is_expected
.
to
validate_presence_of
(
:project_fingerprint
)
}
it
{
is_expected
.
to
validate_presence_of
(
:primary_identifier
)
}
it
{
is_expected
.
to
validate_presence_of
(
:location_fingerprint
)
}
it
{
is_expected
.
to
validate_presence_of
(
:name
)
}
it
{
is_expected
.
to
validate_presence_of
(
:report_type
)
}
it
{
is_expected
.
to
validate_presence_of
(
:metadata_version
)
}
it
{
is_expected
.
to
validate_presence_of
(
:raw_metadata
)
}
it
{
is_expected
.
to
validate_presence_of
(
:severity
)
}
it
{
is_expected
.
to
validate_presence_of
(
:confidence
)
}
it
{
is_expected
.
to
validate_length_of
(
:description
).
is_at_most
(
15000
)
}
it
{
is_expected
.
to
validate_length_of
(
:message
).
is_at_most
(
3000
)
}
it
{
is_expected
.
to
validate_length_of
(
:solution
).
is_at_most
(
7000
)
}
it
{
is_expected
.
to
validate_length_of
(
:cve
).
is_at_most
(
48400
)
}
context
'when value for details field is valid'
do
it
'is valid'
do
finding
.
details
=
{}
expect
(
finding
).
to
be_valid
end
end
end
context
'when value for details field is invalid'
do
it
'returns errors'
do
finding
.
details
=
{
invalid:
'data'
}
context
'when value for details field is invalid'
do
it
'returns errors'
do
finding
.
details
=
{
invalid:
'data'
}
expect
(
finding
).
to
be_invalid
expect
(
finding
.
errors
.
full_messages
).
to
eq
([
"Details must be a valid json schema"
])
expect
(
finding
).
to
be_invalid
expect
(
finding
.
errors
.
full_messages
).
to
eq
([
"Details must be a valid json schema"
])
end
end
end
end
context
'database uniqueness'
do
let
(
:finding
)
{
create
(
:vulnerabilities_finding
)
}
let
(
:new_finding
)
{
finding
.
dup
.
tap
{
|
o
|
o
.
uuid
=
SecureRandom
.
uuid
}
}
context
'database uniqueness'
do
let
(
:finding
)
{
create
(
:vulnerabilities_finding
)
}
let
(
:new_finding
)
{
finding
.
dup
.
tap
{
|
o
|
o
.
cve
=
SecureRandom
.
uuid
}
}
it
"when all index attributes are identical"
do
expect
{
new_finding
.
save!
}.
to
raise_error
(
ActiveRecord
::
RecordNotUnique
)
end
it
"when all index attributes are identical"
do
expect
{
new_finding
.
save!
}.
to
raise_error
(
ActiveRecord
::
RecordNotUnique
)
end
describe
'when some parameters are changed'
do
using
RSpec
::
Parameterized
::
TableSyntax
describe
'when some parameters are changed'
do
using
RSpec
::
Parameterized
::
TableSyntax
# we use block to delay object creations
where
(
:key
,
:factory_name
)
do
:primary_identifier
|
:vulnerabilities_identifier
:scanner
|
:vulnerabilities_scanner
:project
|
:project
end
# we use block to delay object creations
where
(
:key
,
:factory_name
)
do
:primary_identifier
|
:vulnerabilities_identifier
:scanner
|
:vulnerabilities_scanner
:project
|
:project
end
with_them
do
it
"is valid"
do
expect
{
new_finding
.
update!
({
key
=>
create
(
factory_name
)
})
}.
not_to
raise_error
with_them
do
it
"is valid"
do
expect
{
new_finding
.
update!
({
key
=>
create
(
factory_name
),
'uuid'
=>
SecureRandom
.
uuid
})
}.
not_to
raise_error
end
end
end
end
end
context
'order'
do
let!
(
:finding1
)
{
create
(
:vulnerabilities_finding
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
::
Enums
::
Vulnerability
.
severity_levels
[
:high
])
}
let!
(
:finding2
)
{
create
(
:vulnerabilities_finding
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:medium
],
severity:
::
Enums
::
Vulnerability
.
severity_levels
[
:critical
])
}
let!
(
:finding3
)
{
create
(
:vulnerabilities_finding
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
::
Enums
::
Vulnerability
.
severity_levels
[
:critical
])
}
context
'order'
do
let!
(
:finding1
)
{
create
(
:vulnerabilities_finding
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
::
Enums
::
Vulnerability
.
severity_levels
[
:high
])
}
let!
(
:finding2
)
{
create
(
:vulnerabilities_finding
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:medium
],
severity:
::
Enums
::
Vulnerability
.
severity_levels
[
:critical
])
}
let!
(
:finding3
)
{
create
(
:vulnerabilities_finding
,
confidence:
::
Enums
::
Vulnerability
.
confidence_levels
[
:high
],
severity:
::
Enums
::
Vulnerability
.
severity_levels
[
:critical
])
}
it
'orders by severity and confidence'
do
expect
(
described_class
.
all
.
ordered
).
to
eq
([
finding3
,
finding2
,
finding1
])
it
'orders by severity and confidence'
do
expect
(
described_class
.
all
.
ordered
).
to
eq
([
finding3
,
finding2
,
finding1
])
end
end
end
describe
'.report_type'
do
let
(
:report_type
)
{
:sast
}
describe
'.report_type'
do
let
(
:report_type
)
{
:sast
}
subject
{
described_class
.
report_type
(
report_type
)
}
subject
{
described_class
.
report_type
(
report_type
)
}
context
'when finding has the corresponding report type'
do
let!
(
:finding
)
{
create
(
:vulnerabilities_finding
,
report_type:
report_type
)
}
context
'when finding has the corresponding report type'
do
let!
(
:finding
)
{
create
(
:vulnerabilities_finding
,
report_type:
report_type
)
}
it
'selects the finding'
do
is_expected
.
to
eq
([
finding
])
it
'selects the finding'
do
is_expected
.
to
eq
([
finding
])
end
end
end
context
'when finding does not have security reports'
do
let!
(
:finding
)
{
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
)
}
context
'when finding does not have security reports'
do
let!
(
:finding
)
{
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
)
}
it
'does not select the finding'
do
is_expected
.
to
be_empty
it
'does not select the finding'
do
is_expected
.
to
be_empty
end
end
end
end
describe
'.for_pipelines_with_sha'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
:success
,
project:
project
)
}
describe
'.for_pipelines_with_sha'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
:success
,
project:
project
)
}
before
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
)
end
before
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
)
end
subject
(
:findings
)
{
described_class
.
for_pipelines_with_sha
([
pipeline
])
}
subject
(
:findings
)
{
described_class
.
for_pipelines_with_sha
([
pipeline
])
}
it
'sets the sha'
do
expect
(
findings
.
first
.
sha
).
to
eq
(
pipeline
.
sha
)
it
'sets the sha'
do
expect
(
findings
.
first
.
sha
).
to
eq
(
pipeline
.
sha
)
end
end
end
describe
'.by_report_types'
do
let!
(
:vulnerability_sast
)
{
create
(
:vulnerabilities_finding
,
report_type: :sast
)
}
let!
(
:vulnerability_secret_detection
)
{
create
(
:vulnerabilities_finding
,
report_type: :secret_detection
)
}
let!
(
:vulnerability_dast
)
{
create
(
:vulnerabilities_finding
,
report_type: :dast
)
}
let!
(
:vulnerability_depscan
)
{
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
)
}
let!
(
:vulnerability_covfuzz
)
{
create
(
:vulnerabilities_finding
,
report_type: :coverage_fuzzing
)
}
let!
(
:vulnerability_apifuzz
)
{
create
(
:vulnerabilities_finding
,
report_type: :api_fuzzing
)
}
describe
'.by_report_types'
do
let!
(
:vulnerability_sast
)
{
create
(
:vulnerabilities_finding
,
report_type: :sast
)
}
let!
(
:vulnerability_secret_detection
)
{
create
(
:vulnerabilities_finding
,
report_type: :secret_detection
)
}
let!
(
:vulnerability_dast
)
{
create
(
:vulnerabilities_finding
,
report_type: :dast
)
}
let!
(
:vulnerability_depscan
)
{
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
)
}
let!
(
:vulnerability_covfuzz
)
{
create
(
:vulnerabilities_finding
,
report_type: :coverage_fuzzing
)
}
let!
(
:vulnerability_apifuzz
)
{
create
(
:vulnerabilities_finding
,
report_type: :api_fuzzing
)
}
subject
{
described_class
.
by_report_types
(
param
)
}
subject
{
described_class
.
by_report_types
(
param
)
}
context
'with one param'
do
let
(
:param
)
{
Vulnerabilities
::
Finding
.
report_types
[
'sast'
]
}
context
'with one param'
do
let
(
:param
)
{
Vulnerabilities
::
Finding
.
report_types
[
'sast'
]
}
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability_sast
)
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability_sast
)
end
end
end
context
'with array of params'
do
let
(
:param
)
do
[
Vulnerabilities
::
Finding
.
report_types
[
'dependency_scanning'
],
Vulnerabilities
::
Finding
.
report_types
[
'dast'
],
Vulnerabilities
::
Finding
.
report_types
[
'secret_detection'
],
Vulnerabilities
::
Finding
.
report_types
[
'coverage_fuzzing'
],
Vulnerabilities
::
Finding
.
report_types
[
'api_fuzzing'
]
]
end
context
'with array of params'
do
let
(
:param
)
do
[
Vulnerabilities
::
Finding
.
report_types
[
'dependency_scanning'
],
Vulnerabilities
::
Finding
.
report_types
[
'dast'
],
Vulnerabilities
::
Finding
.
report_types
[
'secret_detection'
],
Vulnerabilities
::
Finding
.
report_types
[
'coverage_fuzzing'
],
Vulnerabilities
::
Finding
.
report_types
[
'api_fuzzing'
]
]
end
it
'returns found records'
do
is_expected
.
to
contain_exactly
(
vulnerability_dast
,
vulnerability_depscan
,
vulnerability_secret_detection
,
vulnerability_covfuzz
,
vulnerability_apifuzz
)
it
'returns found records'
do
is_expected
.
to
contain_exactly
(
vulnerability_dast
,
vulnerability_depscan
,
vulnerability_secret_detection
,
vulnerability_covfuzz
,
vulnerability_apifuzz
)
end
end
end
context
'without found record'
do
let
(
:param
)
{
::
Enums
::
Vulnerability
.
report_types
[
'container_scanning'
]}
context
'without found record'
do
let
(
:param
)
{
::
Enums
::
Vulnerability
.
report_types
[
'container_scanning'
]}
it
'returns empty collection'
do
is_expected
.
to
be_empty
it
'returns empty collection'
do
is_expected
.
to
be_empty
end
end
end
end
describe
'.by_projects'
do
let!
(
:vulnerability1
)
{
create
(
:vulnerabilities_finding
)
}
let!
(
:vulnerability2
)
{
create
(
:vulnerabilities_finding
)
}
describe
'.by_projects'
do
let!
(
:vulnerability1
)
{
create
(
:vulnerabilities_finding
)
}
let!
(
:vulnerability2
)
{
create
(
:vulnerabilities_finding
)
}
subject
{
described_class
.
by_projects
(
param
)
}
subject
{
described_class
.
by_projects
(
param
)
}
context
'with found record'
do
let
(
:param
)
{
vulnerability1
.
project_id
}
context
'with found record'
do
let
(
:param
)
{
vulnerability1
.
project_id
}
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability1
)
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability1
)
end
end
end
end
describe
'.by_scanners'
do
context
'with found record'
do
it
'returns found record'
do
vulnerability1
=
create
(
:vulnerabilities_finding
)
create
(
:vulnerabilities_finding
)
param
=
vulnerability1
.
scanner_id
describe
'.by_scanners'
do
context
'with found record'
do
it
'returns found record'
do
vulnerability1
=
create
(
:vulnerabilities_finding
)
create
(
:vulnerabilities_finding
)
param
=
vulnerability1
.
scanner_id
result
=
described_class
.
by_scanners
(
param
)
result
=
described_class
.
by_scanners
(
param
)
expect
(
result
).
to
contain_exactly
(
vulnerability1
)
expect
(
result
).
to
contain_exactly
(
vulnerability1
)
end
end
end
end
describe
'.by_severities'
do
let!
(
:vulnerability_high
)
{
create
(
:vulnerabilities_finding
,
severity: :high
)
}
let!
(
:vulnerability_low
)
{
create
(
:vulnerabilities_finding
,
severity: :low
)
}
describe
'.by_severities'
do
let!
(
:vulnerability_high
)
{
create
(
:vulnerabilities_finding
,
severity: :high
)
}
let!
(
:vulnerability_low
)
{
create
(
:vulnerabilities_finding
,
severity: :low
)
}
subject
{
described_class
.
by_severities
(
param
)
}
subject
{
described_class
.
by_severities
(
param
)
}
context
'with one param'
do
let
(
:param
)
{
described_class
.
severities
[
:low
]
}
context
'with one param'
do
let
(
:param
)
{
described_class
.
severities
[
:low
]
}
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability_low
)
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability_low
)
end
end
end
context
'without found record'
do
let
(
:param
)
{
described_class
.
severities
[
:unknown
]
}
context
'without found record'
do
let
(
:param
)
{
described_class
.
severities
[
:unknown
]
}
it
'returns empty collection'
do
is_expected
.
to
be_empty
it
'returns empty collection'
do
is_expected
.
to
be_empty
end
end
end
end
describe
'.by_confidences'
do
let!
(
:vulnerability_high
)
{
create
(
:vulnerabilities_finding
,
confidence: :high
)
}
let!
(
:vulnerability_low
)
{
create
(
:vulnerabilities_finding
,
confidence: :low
)
}
describe
'.by_confidences'
do
let!
(
:vulnerability_high
)
{
create
(
:vulnerabilities_finding
,
confidence: :high
)
}
let!
(
:vulnerability_low
)
{
create
(
:vulnerabilities_finding
,
confidence: :low
)
}
subject
{
described_class
.
by_confidences
(
param
)
}
subject
{
described_class
.
by_confidences
(
param
)
}
context
'with matching param'
do
let
(
:param
)
{
described_class
.
confidences
[
:low
]
}
context
'with matching param'
do
let
(
:param
)
{
described_class
.
confidences
[
:low
]
}
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability_low
)
it
'returns found record'
do
is_expected
.
to
contain_exactly
(
vulnerability_low
)
end
end
end
context
'with non-matching param'
do
let
(
:param
)
{
described_class
.
confidences
[
:unknown
]
}
context
'with non-matching param'
do
let
(
:param
)
{
described_class
.
confidences
[
:unknown
]
}
it
'returns empty collection'
do
is_expected
.
to
be_empty
it
'returns empty collection'
do
is_expected
.
to
be_empty
end
end
end
end
describe
'.counted_by_severity'
do
let!
(
:high_vulnerabilities
)
{
create_list
(
:vulnerabilities_finding
,
3
,
severity: :high
)
}
let!
(
:medium_vulnerabilities
)
{
create_list
(
:vulnerabilities_finding
,
2
,
severity: :medium
)
}
let!
(
:low_vulnerabilities
)
{
create_list
(
:vulnerabilities_finding
,
1
,
severity: :low
)
}
describe
'.counted_by_severity'
do
let!
(
:high_vulnerabilities
)
{
create_list
(
:vulnerabilities_finding
,
3
,
severity: :high
)
}
let!
(
:medium_vulnerabilities
)
{
create_list
(
:vulnerabilities_finding
,
2
,
severity: :medium
)
}
let!
(
:low_vulnerabilities
)
{
create_list
(
:vulnerabilities_finding
,
1
,
severity: :low
)
}
subject
{
described_class
.
counted_by_severity
}
subject
{
described_class
.
counted_by_severity
}
it
'returns counts'
do
is_expected
.
to
eq
({
4
=>
1
,
5
=>
2
,
6
=>
3
})
it
'returns counts'
do
is_expected
.
to
eq
({
4
=>
1
,
5
=>
2
,
6
=>
3
})
end
end
end
describe
'.undismissed'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:project2
)
{
create
(
:project
)
}
describe
'.undismissed'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:project2
)
{
create
(
:project
)
}
let!
(
:finding1
)
{
create
(
:vulnerabilities_finding
,
project:
project
)
}
let!
(
:finding2
)
{
create
(
:vulnerabilities_finding
,
project:
project
,
report_type: :dast
)
}
let!
(
:finding3
)
{
create
(
:vulnerabilities_finding
,
project:
project2
)
}
let!
(
:finding1
)
{
create
(
:vulnerabilities_finding
,
project:
project
)
}
let!
(
:finding2
)
{
create
(
:vulnerabilities_finding
,
project:
project
,
report_type: :dast
)
}
let!
(
:finding3
)
{
create
(
:vulnerabilities_finding
,
project:
project2
)
}
before
do
create
(
:vulnerability_feedback
,
:dismissal
,
project:
finding1
.
project
,
project_fingerprint:
finding1
.
project_fingerprint
)
create
(
:vulnerability_feedback
,
:dismissal
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
project2
)
create
(
:vulnerability_feedback
,
:dismissal
,
category: :sast
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
finding2
.
project
)
end
it
'returns all non-dismissed findings'
do
expect
(
described_class
.
undismissed
).
to
contain_exactly
(
finding2
,
finding3
)
end
it
'returns non-dismissed findings for project'
do
expect
(
project2
.
vulnerability_findings
.
undismissed
).
to
contain_exactly
(
finding3
)
end
end
describe
'.dismissed'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:project2
)
{
create
(
:project
)
}
before
do
create
(
:vulnerability_feedback
,
:dismissal
,
project:
finding1
.
project
,
project_fingerprint:
finding1
.
project_fingerprint
)
create
(
:vulnerability_feedback
,
:dismissal
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
project2
)
create
(
:vulnerability_feedback
,
:dismissal
,
category: :sast
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
finding2
.
project
)
end
let!
(
:finding1
)
{
create
(
:vulnerabilities_finding
,
project:
project
)
}
let!
(
:finding2
)
{
create
(
:vulnerabilities_finding
,
project:
project
,
report_type: :dast
)
}
let!
(
:finding3
)
{
create
(
:vulnerabilities_finding
,
project:
project2
)
}
it
'returns all non-dismissed findings'
do
expect
(
described_class
.
undismissed
).
to
contain_exactly
(
finding2
,
finding3
)
end
before
do
create
(
:vulnerability_feedback
,
:dismissal
,
project:
finding1
.
project
,
project_fingerprint:
finding1
.
project_fingerprint
)
create
(
:vulnerability_feedback
,
:dismissal
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
project2
)
create
(
:vulnerability_feedback
,
:dismissal
,
category: :sast
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
finding2
.
project
)
end
it
'returns all dismissed findings'
do
expect
(
described_class
.
dismissed
).
to
contain_exactly
(
finding1
)
end
it
'returns dismissed findings for project'
do
expect
(
project
.
vulnerability_findings
.
dismissed
).
to
contain_exactly
(
finding1
)
it
'returns non-dismissed findings for project'
do
expect
(
project2
.
vulnerability_findings
.
undismissed
).
to
contain_exactly
(
finding3
)
end
end
end
describe
'.batch_count_by_project_and_severity
'
do
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
:success
,
project:
project
)
}
let
(
:project
)
{
create
(
:project
)
}
describe
'.dismissed
'
do
let_it_be
(
:project
)
{
create
(
:
project
)
}
let_it_be
(
:project2
)
{
create
(
:project
)
}
it
'fetches a vulnerability count for the given project and severity'
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
let!
(
:finding1
)
{
create
(
:vulnerabilities_finding
,
project:
project
)
}
let!
(
:finding2
)
{
create
(
:vulnerabilities_finding
,
project:
project
,
report_type: :dast
)
}
let!
(
:finding3
)
{
create
(
:vulnerabilities_finding
,
project:
project2
)
}
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'high'
)
before
do
create
(
:vulnerability_feedback
,
:dismissal
,
project:
finding1
.
project
,
project_fingerprint:
finding1
.
project_fingerprint
)
create
(
:vulnerability_feedback
,
:dismissal
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
project2
)
create
(
:vulnerability_feedback
,
:dismissal
,
category: :sast
,
project_fingerprint:
finding2
.
project_fingerprint
,
project:
finding2
.
project
)
end
expect
(
count
).
to
be
(
1
)
end
it
'returns all dismissed findings'
do
expect
(
described_class
.
dismissed
).
to
contain_exactly
(
finding1
)
end
it
'only returns vulnerabilities from the latest successful pipeline'
do
old_pipeline
=
create
(
:ci_pipeline
,
:success
,
project:
project
)
latest_pipeline
=
create
(
:ci_pipeline
,
:success
,
project:
project
)
latest_failed_pipeline
=
create
(
:ci_pipeline
,
:failed
,
project:
project
)
create
(
:vulnerabilities_finding
,
pipelines:
[
old_pipeline
],
project:
project
,
severity: :critical
)
create
(
:vulnerabilities_finding
,
pipelines:
[
latest_failed_pipeline
],
project:
project
,
severity: :critical
)
create_list
(
:vulnerabilities_finding
,
2
,
pipelines:
[
latest_pipeline
],
project:
project
,
severity: :critical
)
it
'returns dismissed findings for project'
do
expect
(
project
.
vulnerability_findings
.
dismissed
).
to
contain_exactly
(
finding1
)
end
end
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'critical'
)
describe
'.batch_count_by_project_and_severity'
do
let
(
:pipeline
)
{
create
(
:ci_pipeline
,
:success
,
project:
project
)
}
let
(
:project
)
{
create
(
:project
)
}
expect
(
count
).
to
be
(
2
)
end
it
'fetches a vulnerability count for the given project and severity'
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
it
'returns 0 when there are no vulnerabilities for that severity level'
do
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'high'
)
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'high'
)
expect
(
count
).
to
be
(
0
)
end
expect
(
count
).
to
be
(
1
)
end
it
'batch loads the counts'
do
projects
=
create_list
(
:project
,
2
)
it
'only returns vulnerabilities from the latest successful pipeline'
do
old_pipeline
=
create
(
:ci_pipeline
,
:success
,
project:
project
)
latest_pipeline
=
create
(
:ci_pipeline
,
:success
,
project:
project
)
latest_failed_pipeline
=
create
(
:ci_pipeline
,
:failed
,
project:
project
)
create
(
:vulnerabilities_finding
,
pipelines:
[
old_pipeline
],
project:
project
,
severity: :critical
)
create
(
:vulnerabilities_finding
,
pipelines:
[
latest_failed_pipeline
],
project:
project
,
severity: :critical
)
create_list
(
:vulnerabilities_finding
,
2
,
pipelines:
[
latest_pipeline
],
project:
project
,
severity: :critical
)
projects
.
each
do
|
project
|
pipeline
=
create
(
:ci_pipeline
,
:success
,
project:
project
)
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'critical'
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :low
)
expect
(
count
).
to
be
(
2
)
end
projects_and_severities
=
[
[
projects
.
first
,
'high'
],
[
projects
.
first
,
'low'
],
[
projects
.
second
,
'high'
],
[
projects
.
second
,
'low'
]
]
it
'returns 0 when there are no vulnerabilities for that severity level'
do
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'high'
)
counts
=
projects_and_severities
.
map
do
|
(
project
,
severity
)
|
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
severity
)
expect
(
count
).
to
be
(
0
)
end
expect
{
expect
(
counts
).
to
all
(
be
1
)
}.
not_to
exceed_query_limit
(
1
)
end
it
'batch loads the counts'
do
projects
=
create_list
(
:project
,
2
)
it
'does not include dismissed vulnerabilities in the counts'
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
dismissed_vulnerability
=
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
create
(
:vulnerability_feedback
,
project:
project
,
project_fingerprint:
dismissed_vulnerability
.
project_fingerprint
,
feedback_type: :dismissal
)
projects
.
each
do
|
project
|
pipeline
=
create
(
:ci_pipeline
,
:success
,
project:
project
)
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'high'
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :low
)
end
expect
(
count
).
to
be
(
1
)
end
projects_and_severities
=
[
[
projects
.
first
,
'high'
],
[
projects
.
first
,
'low'
],
[
projects
.
second
,
'high'
],
[
projects
.
second
,
'low'
]
]
it
"does not overwrite one project's counts with another's"
do
project1
=
create
(
:project
)
project2
=
create
(
:project
)
pipeline1
=
create
(
:ci_pipeline
,
:success
,
project:
project1
)
pipeline2
=
create
(
:ci_pipeline
,
:success
,
project:
project2
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline1
],
project:
project1
,
severity: :critical
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline2
],
project:
project2
,
severity: :high
)
counts
=
projects_and_severities
.
map
do
|
(
project
,
severity
)
|
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
severity
)
end
project1_critical_count
=
described_class
.
batch_count_by_project_and_severity
(
project1
.
id
,
'critical'
)
project1_high_count
=
described_class
.
batch_count_by_project_and_severity
(
project1
.
id
,
'high'
)
project2_critical_count
=
described_class
.
batch_count_by_project_and_severity
(
project2
.
id
,
'critical'
)
project2_high_count
=
described_class
.
batch_count_by_project_and_severity
(
project2
.
id
,
'high'
)
expect
{
expect
(
counts
).
to
all
(
be
1
)
}.
not_to
exceed_query_limit
(
1
)
end
expect
(
project1_critical_count
).
to
be
(
1
)
expect
(
project1_high_count
).
to
be
(
0
)
expect
(
project2_critical_count
).
to
be
(
0
)
expect
(
project2_high_count
).
to
be
(
1
)
end
end
it
'does not include dismissed vulnerabilities in the counts'
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
dismissed_vulnerability
=
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
project:
project
,
severity: :high
)
create
(
:vulnerability_feedback
,
project:
project
,
project_fingerprint:
dismissed_vulnerability
.
project_fingerprint
,
feedback_type: :dismissal
)
describe
'#links'
do
let_it_be
(
:finding
,
reload:
true
)
do
create
(
:vulnerabilities_finding
,
raw_metadata:
{
links:
[{
url:
'https://raw.gitlab.com'
,
name:
'raw_metadata_link'
}]
}.
to_json
)
end
count
=
described_class
.
batch_count_by_project_and_severity
(
project
.
id
,
'high'
)
subject
(
:links
)
{
finding
.
links
}
expect
(
count
).
to
be
(
1
)
end
context
'when there are no finding links'
do
it
'returns links from raw_metadata'
do
expect
(
links
).
to
eq
([{
'url'
=>
'https://raw.gitlab.com'
,
'name'
=>
'raw_metadata_link'
}])
it
"does not overwrite one project's counts with another's"
do
project1
=
create
(
:project
)
project2
=
create
(
:project
)
pipeline1
=
create
(
:ci_pipeline
,
:success
,
project:
project1
)
pipeline2
=
create
(
:ci_pipeline
,
:success
,
project:
project2
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline1
],
project:
project1
,
severity: :critical
)
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline2
],
project:
project2
,
severity: :high
)
project1_critical_count
=
described_class
.
batch_count_by_project_and_severity
(
project1
.
id
,
'critical'
)
project1_high_count
=
described_class
.
batch_count_by_project_and_severity
(
project1
.
id
,
'high'
)
project2_critical_count
=
described_class
.
batch_count_by_project_and_severity
(
project2
.
id
,
'critical'
)
project2_high_count
=
described_class
.
batch_count_by_project_and_severity
(
project2
.
id
,
'high'
)
expect
(
project1_critical_count
).
to
be
(
1
)
expect
(
project1_high_count
).
to
be
(
0
)
expect
(
project2_critical_count
).
to
be
(
0
)
expect
(
project2_high_count
).
to
be
(
1
)
end
end
context
'when there are finding links assigned to given finding'
do
let_it_be
(
:finding_link
)
{
create
(
:finding_link
,
name:
'finding_link'
,
url:
'https://link.gitlab.com'
,
finding:
finding
)
}
it
'returns links from finding link'
do
expect
(
links
).
to
eq
([{
'url'
=>
'https://link.gitlab.com'
,
'name'
=>
'finding_link'
}])
describe
'#links'
do
let_it_be
(
:finding
,
reload:
true
)
do
create
(
:vulnerabilities_finding
,
raw_metadata:
{
links:
[{
url:
'https://raw.gitlab.com'
,
name:
'raw_metadata_link'
}]
}.
to_json
)
end
end
end
describe
'#remediations'
do
let_it_be
(
:project
)
{
create_default
(
:project
)
}
let_it_be
(
:finding
,
refind:
true
)
{
create
(
:vulnerabilities_finding
)
}
subject
(
:links
)
{
finding
.
links
}
subject
{
finding
.
remediations
}
context
'when there are no finding links'
do
it
'returns links from raw_metadata'
do
expect
(
links
).
to
eq
([{
'url'
=>
'https://raw.gitlab.com'
,
'name'
=>
'raw_metadata_link'
}])
end
end
context
'when the finding has associated remediation records'
do
let_it_be
(
:persisted_remediation
)
{
create
(
:vulnerabilities_remediation
,
findings:
[
finding
])
}
let_it_be
(
:remediation_hash
)
{
{
'summary'
=>
persisted_remediation
.
summary
,
'diff'
=>
persisted_remediation
.
diff
}
}
context
'when there are finding links assigned to given finding'
do
let_it_be
(
:finding_link
)
{
create
(
:finding_link
,
name:
'finding_link'
,
url:
'https://link.gitlab.com'
,
finding:
finding
)
}
it
{
is_expected
.
to
eq
([
remediation_hash
])
}
it
'returns links from finding link'
do
expect
(
links
).
to
eq
([{
'url'
=>
'https://link.gitlab.com'
,
'name'
=>
'finding_link'
}])
end
end
end
context
'when the finding does not have associated remediation record
s'
do
context
'when the finding has remediations in `raw_metadata`'
do
let
(
:raw_remediation
)
{
{
summary:
'foo'
,
diff:
'bar'
}.
stringify_keys
}
describe
'#remediation
s'
do
let_it_be
(
:project
)
{
create_default
(
:project
)
}
let_it_be
(
:finding
,
refind:
true
)
{
create
(
:vulnerabilities_finding
)
}
before
do
raw_metadata
=
{
remediations:
[
raw_remediation
]
}.
to_json
finding
.
update!
(
raw_metadata:
raw_metadata
)
end
subject
{
finding
.
remediations
}
context
'when the finding has associated remediation records'
do
let_it_be
(
:persisted_remediation
)
{
create
(
:vulnerabilities_remediation
,
findings:
[
finding
])
}
let_it_be
(
:remediation_hash
)
{
{
'summary'
=>
persisted_remediation
.
summary
,
'diff'
=>
persisted_remediation
.
diff
}
}
it
{
is_expected
.
to
eq
([
r
aw_remediation
])
}
it
{
is_expected
.
to
eq
([
r
emediation_hash
])
}
end
context
'when the finding does not have remediations in `raw_metadata`'
do
before
do
finding
.
update!
(
raw_metadata:
{}.
to_json
)
context
'when the finding does not have associated remediation records'
do
context
'when the finding has remediations in `raw_metadata`'
do
let
(
:raw_remediation
)
{
{
summary:
'foo'
,
diff:
'bar'
}.
stringify_keys
}
before
do
raw_metadata
=
{
remediations:
[
raw_remediation
]
}.
to_json
finding
.
update!
(
raw_metadata:
raw_metadata
)
end
it
{
is_expected
.
to
eq
([
raw_remediation
])
}
end
it
{
is_expected
.
to
be_nil
}
context
'when the finding does not have remediations in `raw_metadata`'
do
before
do
finding
.
update!
(
raw_metadata:
{}.
to_json
)
end
it
{
is_expected
.
to
be_nil
}
end
end
end
end
describe
'feedback'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let
(
:finding
)
do
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
,
project:
project
)
end
describe
'feedback'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
describe
'#issue_feedback'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:issue_feedback
)
do
let
(
:finding
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:issue
,
issue:
issue
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
:vulnerabilities_finding
,
report_type: :dependency_scanning
,
project:
project
)
end
let
(
:vulnerability
)
{
create
(
:vulnerability
,
findings:
[
finding
])
}
let!
(
:issue_link
)
{
create
(
:vulnerabilities_issue_link
,
vulnerability:
vulnerability
,
issue:
issue
)}
describe
'#issue_feedback'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:issue_feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:issue
,
issue:
issue
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
)
end
it
'returns associated feedback'
do
feedback
=
finding
.
issue_feedback
let
(
:vulnerability
)
{
create
(
:vulnerability
,
findings:
[
finding
])
}
let!
(
:issue_link
)
{
create
(
:vulnerabilities_issue_link
,
vulnerability:
vulnerability
,
issue:
issue
)}
expect
(
feedback
).
to
be_present
expect
(
feedback
[
:project_id
]).
to
eq
project
.
id
expect
(
feedback
[
:feedback_type
]).
to
eq
'issue'
expect
(
feedback
[
:issue_id
]).
to
eq
issue
.
id
end
it
'returns associated feedback'
do
feedback
=
finding
.
issue_feedback
expect
(
feedback
).
to
be_present
expect
(
feedback
[
:project_id
]).
to
eq
project
.
id
expect
(
feedback
[
:feedback_type
]).
to
eq
'issue'
expect
(
feedback
[
:issue_id
]).
to
eq
issue
.
id
end
context
'when there is no feedback for the vulnerability'
do
let
(
:vulnerability_no_feedback
)
{
create
(
:vulnerability
,
findings:
[
finding_no_feedback
])
}
let!
(
:finding_no_feedback
)
{
create
(
:vulnerabilities_finding
,
:dependency_scanning
,
project:
project
)
}
context
'when there is no feedback for the vulnerability'
do
let
(
:vulnerability_no_feedback
)
{
create
(
:vulnerability
,
findings:
[
finding_no_feedback
])
}
let!
(
:finding_no_feedback
)
{
create
(
:vulnerabilities_finding
,
:dependency_scanning
,
project:
project
)
}
it
'does not return unassociated feedback'
do
feedback
=
finding_no_feedback
.
issue_feedback
it
'does not return unassociated feedback'
do
feedback
=
finding_no_feedback
.
issue_feedback
expect
(
feedback
).
not_to
be_present
expect
(
feedback
).
not_to
be_present
end
end
end
context
'when there is no vulnerability associated with the finding'
do
let!
(
:finding_no_vulnerability
)
{
create
(
:vulnerabilities_finding
,
:dependency_scanning
,
project:
project
)
}
context
'when there is no vulnerability associated with the finding'
do
let!
(
:finding_no_vulnerability
)
{
create
(
:vulnerabilities_finding
,
:dependency_scanning
,
project:
project
)
}
it
'does not return feedback'
do
feedback
=
finding_no_vulnerability
.
issue_feedback
it
'does not return feedback'
do
feedback
=
finding_no_vulnerability
.
issue_feedback
expect
(
feedback
).
not_to
be_present
expect
(
feedback
).
not_to
be_present
end
end
end
end
describe
'#dismissal_feedback'
do
let!
(
:dismissal_feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:dismissal
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
)
end
describe
'#dismissal_feedback'
do
let!
(
:dismissal_feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:dismissal
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
)
end
it
'returns associated feedback'
do
feedback
=
finding
.
dismissal_feedback
it
'returns associated feedback'
do
feedback
=
finding
.
dismissal_feedback
expect
(
feedback
).
to
be_present
expect
(
feedback
[
:project_id
]).
to
eq
project
.
id
expect
(
feedback
[
:feedback_type
]).
to
eq
'dismissal'
expect
(
feedback
).
to
be_present
expect
(
feedback
[
:project_id
]).
to
eq
project
.
id
expect
(
feedback
[
:feedback_type
]).
to
eq
'dismissal'
end
end
end
describe
'#merge_request_feedback'
do
let
(
:merge_request
)
{
create
(
:merge_request
,
source_project:
project
)
}
let!
(
:merge_request_feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:merge_request
,
merge_request:
merge_request
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
)
end
describe
'#merge_request_feedback'
do
let
(
:merge_request
)
{
create
(
:merge_request
,
source_project:
project
)
}
let!
(
:merge_request_feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:merge_request
,
merge_request:
merge_request
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
)
end
it
'returns associated feedback'
do
feedback
=
finding
.
merge_request_feedback
it
'returns associated feedback'
do
feedback
=
finding
.
merge_request_feedback
expect
(
feedback
).
to
be_present
expect
(
feedback
[
:project_id
]).
to
eq
project
.
id
expect
(
feedback
[
:feedback_type
]).
to
eq
'merge_request'
expect
(
feedback
[
:merge_request_id
]).
to
eq
merge_request
.
id
expect
(
feedback
).
to
be_present
expect
(
feedback
[
:project_id
]).
to
eq
project
.
id
expect
(
feedback
[
:feedback_type
]).
to
eq
'merge_request'
expect
(
feedback
[
:merge_request_id
]).
to
eq
merge_request
.
id
end
end
end
end
describe
'#load_feedback'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:finding
)
do
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
,
project:
project
)
end
let_it_be
(
:feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:dismissal
,
project:
project
,
project_fingerprint:
finding
.
project_fingerprint
)
end
let
(
:expected_feedback
)
{
[
feedback
]
}
subject
(
:load_feedback
)
{
finding
.
load_feedback
.
to_a
}
it
{
is_expected
.
to
eq
(
expected_feedback
)
}
context
'when you have multiple findings'
do
let_it_be
(
:finding_2
)
do
describe
'#load_feedback'
do
let_it_be
(
:project
)
{
create
(
:project
)
}
let_it_be
(
:finding
)
do
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
,
...
...
@@ -646,367 +627,500 @@ RSpec.describe Vulnerabilities::Finding do
)
end
let_it_be
(
:feedback
_2
)
do
let_it_be
(
:feedback
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:dismissal
,
project:
project
,
project_fingerprint:
finding
_2
.
project_fingerprint
project_fingerprint:
finding
.
project_fingerprint
)
end
let
(
:expected_feedback
)
{
[
[
feedback
],
[
feedback_2
]
]
}
let
(
:expected_feedback
)
{
[
feedback
]
}
subject
(
:load_feedback
)
{
[
finding
,
finding_2
].
map
(
&
:load_feedback
)
}
subject
(
:load_feedback
)
{
finding
.
load_feedback
.
to_a
}
it
{
is_expected
.
to
eq
(
expected_feedback
)
}
end
end
describe
'#state'
do
before
do
create
(
:vulnerability
,
:dismissed
,
project:
finding_with_issue
.
project
,
findings:
[
finding_with_issue
])
end
let
(
:unresolved_finding
)
{
create
(
:vulnerabilities_finding
)
}
let
(
:confirmed_finding
)
{
create
(
:vulnerabilities_finding
,
:confirmed
)
}
let
(
:resolved_finding
)
{
create
(
:vulnerabilities_finding
,
:resolved
)
}
let
(
:dismissed_finding
)
{
create
(
:vulnerabilities_finding
,
:dismissed
)
}
let
(
:detected_finding
)
{
create
(
:vulnerabilities_finding
,
:detected
)
}
let
(
:finding_with_issue
)
{
create
(
:vulnerabilities_finding
,
:with_issue_feedback
)
}
it
'returns the expected state for a unresolved finding'
do
expect
(
unresolved_finding
.
state
).
to
eq
'detected'
end
context
'when you have multiple findings'
do
let_it_be
(
:finding_2
)
do
create
(
:vulnerabilities_finding
,
report_type: :dependency_scanning
,
project:
project
)
end
it
'returns the expected state for a confirmed finding'
do
expect
(
confirmed_finding
.
state
).
to
eq
'confirmed'
end
let_it_be
(
:feedback_2
)
do
create
(
:vulnerability_feedback
,
:dependency_scanning
,
:dismissal
,
project:
project
,
project_fingerprint:
finding_2
.
project_fingerprint
)
end
it
'returns the expected state for a resolved finding'
do
expect
(
resolved_finding
.
state
).
to
eq
'resolved'
end
let
(
:expected_feedback
)
{
[[
feedback
],
[
feedback_2
]]
}
it
'returns the expected state for a dismissed finding'
do
expect
(
dismissed_finding
.
state
).
to
eq
'dismissed'
end
subject
(
:load_feedback
)
{
[
finding
,
finding_2
].
map
(
&
:load_feedback
)
}
it
'returns the expected state for a detected finding'
do
e
xpect
(
detected_finding
.
state
).
to
eq
'detected'
it
{
is_expected
.
to
eq
(
expected_feedback
)
}
e
nd
end
context
'when a vulnerability present for a dismissed finding
'
do
describe
'#state
'
do
before
do
create
(
:vulnerability
,
project:
dismissed_finding
.
project
,
findings:
[
dismissed_finding
])
create
(
:vulnerability
,
:dismissed
,
project:
finding_with_issue
.
project
,
findings:
[
finding_with_issue
])
end
it
'still reports a dismissed state'
do
expect
(
dismissed_finding
.
state
).
to
eq
'dismissed'
let
(
:unresolved_finding
)
{
create
(
:vulnerabilities_finding
)
}
let
(
:confirmed_finding
)
{
create
(
:vulnerabilities_finding
,
:confirmed
)
}
let
(
:resolved_finding
)
{
create
(
:vulnerabilities_finding
,
:resolved
)
}
let
(
:dismissed_finding
)
{
create
(
:vulnerabilities_finding
,
:dismissed
)
}
let
(
:finding_with_issue
)
{
create
(
:vulnerabilities_finding
,
:with_issue_feedback
)
}
it
'returns the expected state for a unresolved finding'
do
expect
(
unresolved_finding
.
state
).
to
eq
'detected'
end
end
context
'when a non-dismissal feedback present for a finding belonging to a closed vulnerability'
do
before
do
create
(
:vulnerability_feedback
,
:issue
,
project:
resolved_finding
.
project
)
it
'returns the expected state for a confirmed finding'
do
expect
(
confirmed_finding
.
state
).
to
eq
'confirmed'
end
it
're
ports as resolved
'
do
it
're
turns the expected state for a resolved finding
'
do
expect
(
resolved_finding
.
state
).
to
eq
'resolved'
end
end
end
describe
'#scanner_name'
do
let
(
:vulnerabilities_finding
)
{
create
(
:vulnerabilities_finding
)
}
subject
(
:scanner_name
)
{
vulnerabilities_finding
.
scanner_name
}
it
'returns the expected state for a dismissed finding'
do
expect
(
dismissed_finding
.
state
).
to
eq
'dismissed'
end
it
{
is_expected
.
to
eq
(
vulnerabilities_finding
.
scanner
.
name
)
}
end
context
'when a vulnerability present for a dismissed finding'
do
before
do
create
(
:vulnerability
,
project:
dismissed_finding
.
project
,
findings:
[
dismissed_finding
])
end
describe
'#description'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_description
)
{
finding
.
metadata
[
'description'
]
}
it
'still reports a dismissed state'
do
expect
(
dismissed_finding
.
state
).
to
eq
'dismissed'
end
end
subject
{
finding
.
description
}
context
'when a non-dismissal feedback present for a finding belonging to a closed vulnerability'
do
before
do
create
(
:vulnerability_feedback
,
:issue
,
project:
resolved_finding
.
project
)
end
context
'when description metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_description
)
}
it
'reports as resolved'
do
expect
(
resolved_finding
.
state
).
to
eq
'resolved'
end
end
end
context
'when description data is present
'
do
let
(
:
finding
)
{
build
(
:vulnerabilities_finding
,
description:
'Vulnerability description'
)
}
describe
'#scanner_name
'
do
let
(
:
vulnerabilities_finding
)
{
create
(
:vulnerabilities_finding
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability description'
)
}
end
end
subject
(
:scanner_name
)
{
vulnerabilities_finding
.
scanner_name
}
describe
'#solution'
do
subject
{
vulnerabilities_finding
.
solution
}
it
{
is_expected
.
to
eq
(
vulnerabilities_finding
.
scanner
.
name
)
}
end
context
'when solution metadata key is present'
do
let
(
:vulnerabilities_finding
)
{
build
(
:vulnerabilities_finding
)
}
describe
'#description'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_description
)
{
finding
.
metadata
[
'description'
]
}
it
{
is_expected
.
to
eq
(
vulnerabilities_finding
.
metadata
[
'solution'
])
}
end
subject
{
finding
.
description
}
context
'when remediations key is present in finding'
do
let
(
:vulnerabilities_finding
)
do
build
(
:vulnerabilities_finding_with_remediation
,
summary:
"Test remediation"
)
context
'when description metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_description
)
}
end
it
{
is_expected
.
to
eq
(
vulnerabilities_finding
.
remediations
.
dig
(
0
,
'summary'
))
}
context
'when description data is present'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
description:
'Vulnerability description'
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability description'
)
}
end
end
context
'when solution data is present
'
do
let
(
:vulnerabilities_finding
)
{
build
(
:vulnerabilities_finding
,
solution:
'Vulnerability solution'
)
}
describe
'#solution
'
do
subject
{
vulnerabilities_finding
.
solution
}
it
{
is_expected
.
to
eq
(
'Vulnerability solution'
)
}
end
end
context
'when solution metadata key is present'
do
let
(
:vulnerabilities_finding
)
{
build
(
:vulnerabilities_finding
)
}
describe
'#location'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_location
)
{
finding
.
metadata
[
'location'
]
}
it
{
is_expected
.
to
eq
(
vulnerabilities_finding
.
metadata
[
'solution'
])
}
end
subject
{
finding
.
location
}
context
'when remediations key is present in finding'
do
let
(
:vulnerabilities_finding
)
do
build
(
:vulnerabilities_finding_with_remediation
,
summary:
"Test remediation"
)
end
context
'when location metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_location
)
}
end
it
{
is_expected
.
to
eq
(
vulnerabilities_finding
.
remediations
.
dig
(
0
,
'summary'
))
}
end
context
'when location data is present'
do
let
(
:location
)
{
{
'class'
=>
'class'
,
'end_line'
=>
3
,
'file'
=>
'test_file.rb'
,
'start_line'
=>
1
}
}
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
location:
location
)
}
context
'when solution data is present'
do
let
(
:vulnerabilities_finding
)
{
build
(
:vulnerabilities_finding
,
solution:
'Vulnerability solution'
)
}
it
{
is_expected
.
to
eq
(
location
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability solution'
)
}
end
end
end
describe
'#evidence'
do
subject
{
finding
.
evidence
}
describe
'#location'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_location
)
{
finding
.
metadata
[
'location'
]
}
context
'has an evidence fields'
do
let
(
:finding
)
{
create
(
:vulnerabilities_finding
)
}
let
(
:evidence
)
{
finding
.
metadata
[
'evidence'
]
}
subject
{
finding
.
location
}
it
do
is_expected
.
to
match
a_hash_including
(
summary:
evidence
[
'summary'
],
request:
{
headers:
[
{
name:
evidence
[
'request'
][
'headers'
][
0
][
'name'
],
value:
evidence
[
'request'
][
'headers'
][
0
][
'value'
]
}
],
url:
evidence
[
'request'
][
'url'
],
method:
evidence
[
'request'
][
'method'
],
body:
evidence
[
'request'
][
'body'
]
},
response:
{
headers:
[
{
name:
evidence
[
'response'
][
'headers'
][
0
][
'name'
],
value:
evidence
[
'response'
][
'headers'
][
0
][
'value'
]
}
],
reason_phrase:
evidence
[
'response'
][
'reason_phrase'
],
status_code:
evidence
[
'response'
][
'status_code'
],
body:
evidence
[
'request'
][
'body'
]
},
source:
{
id:
evidence
.
dig
(
'source'
,
'id'
),
name:
evidence
.
dig
(
'source'
,
'name'
),
url:
evidence
.
dig
(
'source'
,
'url'
)
},
supporting_messages:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'name'
),
request:
{
headers:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'name'
),
value:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'value'
)
}
],
url:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'url'
),
method:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'method'
),
body:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'body'
)
},
response:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'response'
)
context
'when location metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_location
)
}
end
context
'when location data is present'
do
let
(
:location
)
{
{
'class'
=>
'class'
,
'end_line'
=>
3
,
'file'
=>
'test_file.rb'
,
'start_line'
=>
1
}
}
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
location:
location
)
}
it
{
is_expected
.
to
eq
(
location
)
}
end
end
describe
'#evidence'
do
subject
{
finding
.
evidence
}
context
'has an evidence fields'
do
let
(
:finding
)
{
create
(
:vulnerabilities_finding
)
}
let
(
:evidence
)
{
finding
.
metadata
[
'evidence'
]
}
it
do
is_expected
.
to
match
a_hash_including
(
summary:
evidence
[
'summary'
],
request:
{
headers:
[
{
name:
evidence
[
'request'
][
'headers'
][
0
][
'name'
],
value:
evidence
[
'request'
][
'headers'
][
0
][
'value'
]
}
],
url:
evidence
[
'request'
][
'url'
],
method:
evidence
[
'request'
][
'method'
],
body:
evidence
[
'request'
][
'body'
]
},
response:
{
headers:
[
{
name:
evidence
[
'response'
][
'headers'
][
0
][
'name'
],
value:
evidence
[
'response'
][
'headers'
][
0
][
'value'
]
}
],
reason_phrase:
evidence
[
'response'
][
'reason_phrase'
],
status_code:
evidence
[
'response'
][
'status_code'
],
body:
evidence
[
'request'
][
'body'
]
},
source:
{
id:
evidence
.
dig
(
'source'
,
'id'
),
name:
evidence
.
dig
(
'source'
,
'name'
),
url:
evidence
.
dig
(
'source'
,
'url'
)
},
{
name:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'name'
),
request:
{
headers:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'name'
),
value:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'value'
)
}
],
url:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'url'
),
method:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'method'
),
body:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'body'
)
supporting_messages:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'name'
),
request:
{
headers:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'name'
),
value:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'value'
)
}
],
url:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'url'
),
method:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'method'
),
body:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'request'
,
'body'
)
},
response:
evidence
.
dig
(
'supporting_messages'
)[
0
].
dig
(
'response'
)
},
response:
{
headers:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'headers'
)[
0
].
dig
(
'name'
),
value:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'headers'
)[
0
].
dig
(
'value'
)
}
],
reason_phrase:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'reason_phrase'
),
status_code:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'status_code'
),
body:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'body'
)
{
name:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'name'
),
request:
{
headers:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'name'
),
value:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'headers'
)[
0
].
dig
(
'value'
)
}
],
url:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'url'
),
method:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'method'
),
body:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'request'
,
'body'
)
},
response:
{
headers:
[
{
name:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'headers'
)[
0
].
dig
(
'name'
),
value:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'headers'
)[
0
].
dig
(
'value'
)
}
],
reason_phrase:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'reason_phrase'
),
status_code:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'status_code'
),
body:
evidence
.
dig
(
'supporting_messages'
)[
1
].
dig
(
'response'
,
'body'
)
}
}
}
]
)
]
)
end
end
end
context
'has no evidence summary when evidence is present, summary is not'
do
let
(
:finding
)
{
create
(
:vulnerabilities_finding
,
raw_metadata:
{
evidence:
{}
})
}
context
'has no evidence summary when evidence is present, summary is not'
do
let
(
:finding
)
{
create
(
:vulnerabilities_finding
,
raw_metadata:
{
evidence:
{}
})
}
it
do
is_expected
.
to
match
a_hash_including
(
summary:
nil
,
source:
nil
,
supporting_messages:
[],
request:
nil
,
response:
nil
)
it
do
is_expected
.
to
match
a_hash_including
(
summary:
nil
,
source:
nil
,
supporting_messages:
[],
request:
nil
,
response:
nil
)
end
end
end
end
describe
'#message'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_message
)
{
finding
.
metadata
[
'message'
]
}
describe
'#message'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_message
)
{
finding
.
metadata
[
'message'
]
}
subject
{
finding
.
message
}
subject
{
finding
.
message
}
context
'when message metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_message
)
}
end
context
'when message metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_message
)
}
end
context
'when message data is present'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
message:
'Vulnerability message'
)
}
context
'when message data is present'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
message:
'Vulnerability message'
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability message'
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability message'
)
}
end
end
end
describe
'#cve_value'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_cve
)
{
'CVE-2020-0000'
}
describe
'#cve_value'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_cve
)
{
'CVE-2020-0000'
}
subject
{
finding
.
cve_value
}
subject
{
finding
.
cve_value
}
before
do
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'cve'
,
name:
expected_cve
)
end
before
do
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'cve'
,
name:
expected_cve
)
end
context
'when cve metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_cve
)
}
end
context
'when cve metadata key is present'
do
it
{
is_expected
.
to
eql
(
expected_cve
)
}
end
context
'when cve data is present'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
cve:
'Vulnerability cve'
)
}
context
'when cve data is present'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
,
cve:
'Vulnerability cve'
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability cve'
)
}
it
{
is_expected
.
to
eq
(
'Vulnerability cve'
)
}
end
end
end
describe
'#cwe_value'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_cwe
)
{
'CWE-0000'
}
describe
'#cwe_value'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_cwe
)
{
'CWE-0000'
}
subject
{
finding
.
cwe_value
}
subject
{
finding
.
cwe_value
}
before
do
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'cwe'
,
name:
expected_cwe
)
before
do
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'cwe'
,
name:
expected_cwe
)
end
it
{
is_expected
.
to
eql
(
expected_cwe
)
}
end
it
{
is_expected
.
to
eql
(
expected_cwe
)
}
end
describe
'#other_identifier_values'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_values
)
{
[
'ID 1'
,
'ID 2'
]
}
describe
'#other_identifier_values'
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
let
(
:expected_values
)
{
[
'ID 1'
,
'ID 2'
]
}
subject
{
finding
.
other_identifier_values
}
subject
{
finding
.
other_identifier_values
}
before
do
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'foo'
,
name:
expected_values
.
first
)
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'bar'
,
name:
expected_values
.
second
)
end
before
do
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'foo'
,
name:
expected_values
.
first
)
finding
.
identifiers
<<
build
(
:vulnerabilities_identifier
,
external_type:
'bar'
,
name:
expected_values
.
second
)
it
{
is_expected
.
to
match_array
(
expected_values
)
}
end
it
{
is_expected
.
to
match_array
(
expected_values
)
}
end
describe
"#metadata"
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
describe
"#metadata"
do
let
(
:finding
)
{
build
(
:vulnerabilities_finding
)
}
subject
{
finding
.
metadata
}
subject
{
finding
.
metadata
}
it
"handles bool JSON data"
do
allow
(
finding
).
to
receive
(
:raw_metadata
)
{
"true"
}
it
"handles bool JSON data"
do
allow
(
finding
).
to
receive
(
:raw_metadata
)
{
"true"
}
expect
(
subject
).
to
eq
({})
end
expect
(
subject
).
to
eq
({})
end
it
"handles string JSON data"
do
allow
(
finding
).
to
receive
(
:raw_metadata
)
{
'"test"'
}
expect
(
subject
).
to
eq
({})
end
it
"handles string
JSON data"
do
allow
(
finding
).
to
receive
(
:raw_metadata
)
{
'"test"
'
}
it
"parses
JSON data"
do
allow
(
finding
).
to
receive
(
:raw_metadata
)
{
'{ "test": true }
'
}
expect
(
subject
).
to
eq
({})
expect
(
subject
).
to
eq
({
"test"
=>
true
})
end
end
it
"parses JSON data"
do
allow
(
finding
).
to
receive
(
:raw_metadata
)
{
'{ "test": true }'
}
describe
'#uuid_v5'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:report_type
)
{
:sast
}
let
(
:identifier_fingerprint
)
{
'fooo'
}
let
(
:location_fingerprint
)
{
'zooo'
}
let
(
:identifier
)
{
build
(
:vulnerabilities_identifier
,
fingerprint:
identifier_fingerprint
)
}
let
(
:expected_uuid
)
{
'this-is-supposed-to-a-uuid'
}
let
(
:finding
)
do
build
(
:vulnerabilities_finding
,
report_type
,
uuid:
uuid
,
project:
project
,
primary_identifier:
identifier
,
location_fingerprint:
location_fingerprint
)
end
expect
(
subject
).
to
eq
({
"test"
=>
true
})
end
end
subject
(
:uuid_v5
)
{
finding
.
uuid_v5
}
describe
'#uuid_v5'
do
let
(
:project
)
{
create
(
:project
)
}
let
(
:report_type
)
{
:sast
}
let
(
:identifier_fingerprint
)
{
'fooo'
}
let
(
:location_fingerprint
)
{
'zooo'
}
let
(
:identifier
)
{
build
(
:vulnerabilities_identifier
,
fingerprint:
identifier_fingerprint
)
}
let
(
:expected_uuid
)
{
'this-is-supposed-to-a-uuid'
}
let
(
:finding
)
do
build
(
:vulnerabilities_finding
,
report_type
,
uuid:
uuid
,
project:
project
,
primary_identifier:
identifier
,
location_fingerprint:
location_fingerprint
)
end
before
do
allow
(
::
Gitlab
::
UUID
).
to
receive
(
:v5
).
and_return
(
expected_uuid
)
end
subject
(
:uuid_v5
)
{
finding
.
uuid_v5
}
context
'when the finding has a version 4 uuid'
do
let
(
:uuid
)
{
SecureRandom
.
uuid
}
let
(
:uuid_name_value
)
{
"
#{
report_type
}
-
#{
identifier_fingerprint
}
-
#{
location_fingerprint
}
-
#{
project
.
id
}
"
}
before
do
allow
(
::
Gitlab
::
UUID
).
to
receive
(
:v5
).
and_return
(
expected_uuid
)
end
it
'returns the calculated uuid for the finding'
do
expect
(
uuid_v5
).
to
eq
(
expected_uuid
)
expect
(
::
Gitlab
::
UUID
).
to
have_received
(
:v5
).
with
(
uuid_name_value
)
end
end
context
'when the finding has a version 4 uuid'
do
let
(
:uuid
)
{
SecureRandom
.
uuid
}
let
(
:uuid_name_value
)
{
"
#{
report_type
}
-
#{
identifier_fingerprint
}
-
#{
location_fingerprint
}
-
#{
project
.
id
}
"
}
context
'when the finding has a version 5 uuid'
do
let
(
:uuid
)
{
'6756ebb6-8465-5c33-9af9-c5c8b117aefb'
}
it
'returns the calculated uuid for the finding'
do
expect
(
uuid_v5
).
to
eq
(
expected_uuid
)
expect
(
::
Gitlab
::
UUID
).
to
have_received
(
:v5
).
with
(
uuid_name_value
)
it
'returns the uuid of the finding'
do
expect
(
uuid_v5
).
to
eq
(
uuid
)
expect
(
::
Gitlab
::
UUID
).
not_to
have_received
(
:v5
)
end
end
end
context
'when the finding has a version 5 uuid'
do
let
(
:uuid
)
{
'6756ebb6-8465-5c33-9af9-c5c8b117aefb'
}
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
'returns the uuid of the finding'
do
expect
(
uuid_v5
).
to
eq
(
uuid
)
expect
(
::
Gitlab
::
UUID
).
not_to
have_received
(
:v5
)
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
...
...
ee/spec/requests/api/vulnerability_findings_spec.rb
View file @
56fcaba4
...
...
@@ -33,7 +33,8 @@ RSpec.describe API::VulnerabilityFindings do
project:
project
,
pipeline:
pipeline
,
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
...
...
ee/spec/serializers/vulnerabilities/feedback_entity_spec.rb
View file @
56fcaba4
...
...
@@ -164,7 +164,7 @@ RSpec.describe Vulnerabilities::FeedbackEntity do
end
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
expect
(
subject
[
:finding_uuid
]).
to
be_nil
...
...
ee/spec/services/ci/compare_security_reports_service_spec.rb
View file @
56fcaba4
...
...
@@ -11,265 +11,272 @@ RSpec.describe Ci::CompareSecurityReportsService do
collection
.
map
{
|
t
|
t
[
'identifiers'
].
first
[
'external_id'
]
}
end
describe
'#execute DS'
do
where
(
vulnerability_finding_signatures_enabled:
[
true
,
false
])
with_them
do
before
do
stub_
licensed_features
(
dependency_scanning:
true
)
stub_
feature_flags
(
vulnerability_finding_signatures:
vulnerability_finding_signatures_enabled
)
end
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'dependency_scanning'
)
}
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
context
'when head pipeline has dependency scanning reports'
do
let!
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let!
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dependency_scanning_report
,
project:
project
)
}
it
'reports new vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
4
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
describe
'#execute DS'
do
before
do
stub_licensed_features
(
dependency_scanning:
true
)
end
end
context
'when base and head pipelines have dependency scanning reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dependency_scanning_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dependency_scanning_feature_branch
,
project:
project
)
}
it
'reports status as parsed'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
end
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'dependency_scanning'
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
context
'when head pipeline has dependency scanning reports'
do
let!
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let!
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dependency_scanning_report
,
project:
project
)
}
it
'reports fixed vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
first
[
'identifiers'
]).
to
include
(
a_hash_including
(
'external_id'
=>
'CVE-2017-5946'
))
it
'reports new vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
4
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
end
end
it
'reports fixed dependency scanning vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
1
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(06565b64-486d-4326-b906-890d9915804d)
expect
(
compare_keys
).
to
match_array
(
expected_keys
)
context
'when base and head pipelines have dependency scanning reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dependency_scanning_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dependency_scanning_feature_branch
,
project:
project
)
}
it
'reports status as parsed'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
end
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
it
'reports fixed vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
first
[
'identifiers'
]).
to
include
(
a_hash_including
(
'external_id'
=>
'CVE-2017-5946'
))
end
it
'reports fixed dependency scanning vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
1
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(06565b64-486d-4326-b906-890d9915804d)
expect
(
compare_keys
).
to
match_array
(
expected_keys
)
end
end
end
context
'when head pipeline has corrupted dependency scanning vulnerability reports'
do
let_it_be
(
:base_pipeline
)
{
build
(
:ee_ci_pipeline
,
:with_corrupted_dependency_scanning_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
build
(
:ee_ci_pipeline
,
:with_corrupted_dependency_scanning_report
,
project:
project
)
}
context
'when head pipeline has corrupted dependency scanning vulnerability reports'
do
let_it_be
(
:base_pipeline
)
{
build
(
:ee_ci_pipeline
,
:with_corrupted_dependency_scanning_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
build
(
:ee_ci_pipeline
,
:with_corrupted_dependency_scanning_report
,
project:
project
)
}
it
'returns status and error message'
do
expect
(
subject
[
:status
]).
to
eq
(
:error
)
expect
(
subject
[
:status_reason
]).
to
include
(
'JSON parsing failed'
)
end
it
'returns status and error message'
do
expect
(
subject
[
:status
]).
to
eq
(
:error
)
expect
(
subject
[
:status_reason
]).
to
include
(
'JSON parsing failed'
)
end
it
'returns status and error message when pipeline is nil'
do
result
=
service
.
execute
(
nil
,
head_pipeline
)
it
'returns status and error message when pipeline is nil'
do
result
=
service
.
execute
(
nil
,
head_pipeline
)
expect
(
result
[
:status
]).
to
eq
(
:error
)
expect
(
result
[
:status_reason
]).
to
include
(
'JSON parsing failed'
)
expect
(
result
[
:status
]).
to
eq
(
:error
)
expect
(
result
[
:status_reason
]).
to
include
(
'JSON parsing failed'
)
end
end
end
end
describe
'#execute CS'
do
before
do
stub_licensed_features
(
container_scanning:
true
)
end
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'container_scanning'
)
}
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
describe
'#execute CS'
do
before
do
stub_licensed_features
(
container_scanning:
true
)
end
context
'when head pipeline has container scanning reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_container_scanning_report
,
project:
project
)
}
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'container_scanning'
)
}
it
'reports new and fixed vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
8
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
end
end
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
context
'when base and head pipelines have container scanning reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_container_scanning_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_container_scanning_feature_branch
,
project:
project
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
context
'when head pipeline has container scanning reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_container_scanning_report
,
project:
project
)
}
it
'reports new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
first
[
'identifiers'
]).
to
include
(
a_hash_including
(
'external_id'
=>
'CVE-2017-15650'
))
it
'reports new and fixed vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
8
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
end
end
it
'reports fixed container scanning vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
8
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(CVE-2017-16997 CVE-2017-18269 CVE-2018-1000001 CVE-2016-10228 CVE-2010-4052 CVE-2018-18520 CVE-2018-16869 CVE-2018-18311)
expect
(
compare_keys
).
to
match_array
(
expected_keys
)
context
'when base and head pipelines have container scanning reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_container_scanning_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_container_scanning_feature_branch
,
project:
project
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
it
'reports new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
first
[
'identifiers'
]).
to
include
(
a_hash_including
(
'external_id'
=>
'CVE-2017-15650'
))
end
it
'reports fixed container scanning vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
8
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(CVE-2017-16997 CVE-2017-18269 CVE-2018-1000001 CVE-2016-10228 CVE-2010-4052 CVE-2018-18520 CVE-2018-16869 CVE-2018-18311)
expect
(
compare_keys
).
to
match_array
(
expected_keys
)
end
end
end
end
describe
'#execute DAST'
do
before
do
stub_licensed_features
(
dast:
true
)
end
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'dast'
)
}
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
context
'when head pipeline has DAST reports containing some vulnerabilities'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dast_report
,
project:
project
)
}
it
'reports the new vulnerabilities, while not changing the counts of fixed vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
20
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
describe
'#execute DAST'
do
before
do
stub_licensed_features
(
dast:
true
)
end
end
context
'when base and head pipelines have DAST reports containing vulnerabilities'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dast_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dast_feature_branch
,
project:
project
)
}
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'dast'
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'fixed'
].
first
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
expect
(
payload
).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
context
'when head pipeline has DAST reports containing some vulnerabilities'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dast_report
,
project:
project
)
}
it
'reports new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
last
[
'identifiers'
]).
to
include
(
a_hash_including
(
'name'
=>
'CWE-201'
))
it
'reports the new vulnerabilities, while not changing the counts of fixed vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
20
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
end
end
it
'reports fixed DAST vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
19
)
expect
(
subject
[
:data
][
'fixed'
]).
to
include
(
a_hash_including
(
{
'identifiers'
=>
a_collection_including
(
a_hash_including
(
"name"
=>
"CWE-352"
context
'when base and head pipelines have DAST reports containing vulnerabilities'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dast_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_dast_feature_branch
,
project:
project
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'fixed'
].
first
expect
(
payload
).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
it
'reports new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
last
[
'identifiers'
]).
to
include
(
a_hash_including
(
'name'
=>
'CWE-201'
))
end
it
'reports fixed DAST vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
19
)
expect
(
subject
[
:data
][
'fixed'
]).
to
include
(
a_hash_including
(
{
'identifiers'
=>
a_collection_including
(
a_hash_including
(
"name"
=>
"CWE-352"
)
)
)
}
)
)
}
)
)
end
end
end
end
describe
'#execute SAST'
do
before
do
stub_licensed_features
(
sast:
true
)
end
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'sast'
)
}
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
context
'when head pipeline has sast reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_sast_report
,
project:
project
)
}
it
'reports new vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
5
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
describe
'#execute SAST'
do
before
do
stub_licensed_features
(
sast:
true
)
end
end
context
'when base and head pipelines have sast reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_sast_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_sast_feature_branch
,
project:
project
)
}
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'sast'
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
context
'when head pipeline has sast reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_sast_report
,
project:
project
)
}
it
'reports new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
first
[
'identifiers'
]).
to
include
(
a_hash_including
(
'name'
=>
'CWE-327'
))
it
'reports new vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
5
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
end
end
it
'reports fixed sast vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
1
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(CIPHER_INTEGRITY)
expect
(
compare_keys
-
expected_keys
).
to
eq
([])
context
'when base and head pipelines have sast reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_sast_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_sast_feature_branch
,
project:
project
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_merge_request_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_dismissal_path'
]).
to
be_present
expect
(
payload
[
'create_vulnerability_feedback_issue_path'
]).
to
be_present
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
it
'reports new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'added'
].
first
[
'identifiers'
]).
to
include
(
a_hash_including
(
'name'
=>
'CWE-327'
))
end
it
'reports fixed sast vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
1
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(CIPHER_INTEGRITY)
expect
(
compare_keys
-
expected_keys
).
to
eq
([])
end
end
end
end
describe
'#execute SECRET DETECTION'
do
before
do
stub_licensed_features
(
secret_detection:
true
)
end
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'secret_detection'
)
}
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
context
'when head pipeline has secret_detection reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_secret_detection_report
,
project:
project
)
}
it
'reports new vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
describe
'#execute SECRET DETECTION'
do
before
do
stub_licensed_features
(
secret_detection:
true
)
end
end
context
'when base and head pipelines have secret_detection reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_secret_detection_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_secret_detection_feature_branch
,
project:
project
)
}
let
(
:service
)
{
described_class
.
new
(
project
,
current_user
,
report_type:
'secret_detection'
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
expect
(
payload
).
to
be_nil
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
subject
{
service
.
execute
(
base_pipeline
,
head_pipeline
)
}
context
'when head pipeline has secret_detection reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_secret_detection_report
,
project:
project
)
}
it
'does not report any new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
0
)
it
'reports new vulnerabilities'
do
expect
(
subject
[
:status
]).
to
eq
(
:parsed
)
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
1
)
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
0
)
end
end
it
'reports fixed secret_detection vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
1
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(AWS)
expect
(
compare_keys
).
to
match_array
(
expected_keys
)
context
'when base and head pipelines have secret_detection reports'
do
let_it_be
(
:base_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_secret_detection_report
,
project:
project
)
}
let_it_be
(
:head_pipeline
)
{
create
(
:ee_ci_pipeline
,
:with_secret_detection_feature_branch
,
project:
project
)
}
it
'populates fields based on current_user'
do
payload
=
subject
[
:data
][
'added'
].
first
expect
(
payload
).
to
be_nil
expect
(
service
.
current_user
).
to
eq
(
current_user
)
end
it
'does not report any new vulnerability'
do
expect
(
subject
[
:data
][
'added'
].
count
).
to
eq
(
0
)
end
it
'reports fixed secret_detection vulnerabilities'
do
expect
(
subject
[
:data
][
'fixed'
].
count
).
to
eq
(
1
)
compare_keys
=
collect_ids
(
subject
[
:data
][
'fixed'
])
expected_keys
=
%w(AWS)
expect
(
compare_keys
).
to
match_array
(
expected_keys
)
end
end
end
end
...
...
ee/spec/services/security/store_report_service_spec.rb
View file @
56fcaba4
...
...
@@ -3,6 +3,8 @@
require
'spec_helper'
RSpec
.
describe
Security
::
StoreReportService
,
'#execute'
do
using
RSpec
::
Parameterized
::
TableSyntax
let_it_be
(
:user
)
{
create
(
:user
)
}
let
(
:artifact
)
{
create
(
:ee_ci_job_artifact
,
trait
)
}
...
...
@@ -11,456 +13,462 @@ RSpec.describe Security::StoreReportService, '#execute' do
let
(
:pipeline
)
{
artifact
.
job
.
pipeline
}
let
(
:report
)
{
pipeline
.
security_reports
.
get_report
(
report_type
.
to_s
,
artifact
)
}
before
do
stub_licensed_features
(
sast:
true
,
dependency_scanning:
true
,
container_scanning:
true
,
security_dashboard:
true
)
allow
(
Security
::
AutoFixWorker
).
to
receive
(
:perform_async
)
end
subject
{
described_class
.
new
(
pipeline
,
report
).
execute
}
context
'without existing data'
do
before
(
:all
)
do
checksum
=
'f00bc6261fa512f0960b7fc3bfcce7fb31997cf32b96fa647bed5668b2c77fee'
create
(
:vulnerabilities_remediation
,
checksum:
checksum
)
end
where
(
vulnerability_finding_signatures_enabled:
[
true
,
false
])
with_them
do
before
do
project
.
add_developer
(
user
)
allow
(
pipeline
).
to
receive
(
:user
).
and_return
(
user
)
stub_feature_flags
(
vulnerability_finding_signatures:
vulnerability_finding_signatures_enabled
)
stub_licensed_features
(
sast:
true
,
dependency_scanning:
true
,
container_scanning:
true
,
security_dashboard:
true
)
allow
(
Security
::
AutoFixWorker
).
to
receive
(
:perform_async
)
end
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
context
'without existing data'
do
before
(
:all
)
do
checksum
=
'f00bc6261fa512f0960b7fc3bfcce7fb31997cf32b96fa647bed5668b2c77fee'
create
(
:vulnerabilities_remediation
,
checksum:
checksum
)
end
with_them
do
before
do
stub_feature_flags
(
optimize_sql_query_for_security_report:
optimize_sql_query_for_security_report_ff
)
end
before
do
project
.
add_developer
(
user
)
allow
(
pipeline
).
to
receive
(
:user
).
and_return
(
user
)
end
it
'inserts all scanners'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Scanner
.
count
}.
by
(
scanners
)
end
context
'for different security reports'
do
with_them
do
before
do
stub_feature_flags
(
optimize_sql_query_for_security_report:
optimize_sql_query_for_security_report_ff
)
end
it
'inserts all identifiers'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Identifier
.
count
}.
by
(
identifiers
)
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 finding
s'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Finding
.
count
}.
by
(
finding
s
)
end
it
'inserts all scanner
s'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Scanner
.
count
}.
by
(
scanner
s
)
end
it
'inserts all finding identifiers (join model)
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingIdentifier
.
count
}.
by
(
finding_
identifiers
)
end
it
'inserts all identifiers
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Identifier
.
count
}.
by
(
identifiers
)
end
it
'inserts all finding pipelines (join model)
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingPipeline
.
count
}.
by
(
finding_pipeline
s
)
end
it
'inserts all findings
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Finding
.
count
}.
by
(
finding
s
)
end
it
'inserts all remediations
'
do
expect
{
subject
}.
to
change
{
project
.
vulnerability_remediations
.
count
}.
by
(
remediation
s
)
end
it
'inserts all finding identifiers (join model)
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingIdentifier
.
count
}.
by
(
finding_identifier
s
)
end
it
'inserts all vulnerabilities
'
do
expect
{
subject
}.
to
change
{
Vulnerability
.
count
}.
by
(
finding
s
)
end
it
'inserts all finding pipelines (join model)
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingPipeline
.
count
}.
by
(
finding_pipeline
s
)
end
it
'inserts all signatures'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingSignature
.
count
}.
by
(
signatures
)
it
'inserts all remediations'
do
expect
{
subject
}.
to
change
{
project
.
vulnerability_remediations
.
count
}.
by
(
remediations
)
end
it
'inserts all vulnerabilities'
do
expect
{
subject
}.
to
change
{
Vulnerability
.
count
}.
by
(
findings
)
end
it
'inserts all signatures'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingSignature
.
count
}.
by
(
signatures
)
end
end
end
end
context
'when there is an exception'
do
let
(
:trait
)
{
:sast
}
context
'when there is an exception'
do
let
(
:trait
)
{
:sast
}
subject
{
described_class
.
new
(
pipeline
,
report
)
}
subject
{
described_class
.
new
(
pipeline
,
report
)
}
it
'does not insert any scanner'
do
allow
(
Vulnerabilities
::
Scanner
).
to
receive
(
:insert_all
).
with
(
anything
).
and_raise
(
StandardError
)
expect
{
subject
.
send
(
:update_vulnerability_scanners!
,
report
.
findings
)
}.
to
change
{
Vulnerabilities
::
Scanner
.
count
}.
by
(
0
)
it
'does not insert any scanner'
do
allow
(
Vulnerabilities
::
Scanner
).
to
receive
(
:insert_all
).
with
(
anything
).
and_raise
(
StandardError
)
expect
{
subject
.
send
(
:update_vulnerability_scanners!
,
report
.
findings
)
}.
to
change
{
Vulnerabilities
::
Scanner
.
count
}.
by
(
0
)
end
end
end
context
'when N+1 database queries have been removed'
do
let
(
:trait
)
{
:sast
}
let
(
:bandit_scanner
)
{
build
(
:ci_reports_security_scanner
,
external_id:
'bandit'
,
name:
'Bandit'
)
}
context
'when N+1 database queries have been removed'
do
let
(
:trait
)
{
:sast
}
let
(
:bandit_scanner
)
{
build
(
:ci_reports_security_scanner
,
external_id:
'bandit'
,
name:
'Bandit'
)
}
subject
{
described_class
.
new
(
pipeline
,
report
)
}
subject
{
described_class
.
new
(
pipeline
,
report
)
}
it
"avoids N+1 database queries for updating vulnerability scanners"
,
:use_sql_query_cache
do
report
.
add_scanner
(
bandit_scanner
)
it
"avoids N+1 database queries for updating vulnerability scanners"
,
:use_sql_query_cache
do
report
.
add_scanner
(
bandit_scanner
)
control_count
=
ActiveRecord
::
QueryRecorder
.
new
(
skip_cached:
false
)
{
subject
.
send
(
:update_vulnerability_scanners!
,
report
.
findings
)
}.
count
control_count
=
ActiveRecord
::
QueryRecorder
.
new
(
skip_cached:
false
)
{
subject
.
send
(
:update_vulnerability_scanners!
,
report
.
findings
)
}.
count
5
.
times
{
report
.
add_finding
(
build
(
:ci_reports_security_finding
,
scanner:
bandit_scanner
))
}
5
.
times
{
report
.
add_finding
(
build
(
:ci_reports_security_finding
,
scanner:
bandit_scanner
))
}
expect
{
described_class
.
new
(
pipeline
,
report
).
send
(
:update_vulnerability_scanners!
,
report
.
findings
)
}.
not_to
exceed_query_limit
(
control_count
)
expect
{
described_class
.
new
(
pipeline
,
report
).
send
(
:update_vulnerability_scanners!
,
report
.
findings
)
}.
not_to
exceed_query_limit
(
control_count
)
end
end
end
context
'when report data includes all raw_metadata'
do
let
(
:trait
)
{
:dependency_scanning_remediation
}
context
'when report data includes all raw_metadata'
do
let
(
:trait
)
{
:dependency_scanning_remediation
}
it
'inserts top level finding data'
,
:aggregate_failures
do
subject
it
'inserts top level finding data'
,
:aggregate_failures
do
subject
finding
=
Vulnerabilities
::
Finding
.
last
finding
.
raw_metadata
=
nil
finding
=
Vulnerabilities
::
Finding
.
last
finding
.
raw_metadata
=
nil
expect
(
finding
.
metadata
).
to
be_blank
expect
(
finding
.
cve
).
not_to
be_nil
expect
(
finding
.
description
).
not_to
be_nil
expect
(
finding
.
location
).
not_to
be_nil
expect
(
finding
.
message
).
not_to
be_nil
expect
(
finding
.
solution
).
not_to
be_nil
expect
(
finding
.
metadata
).
to
be_blank
expect
(
finding
.
cve
).
not_to
be_nil
expect
(
finding
.
description
).
not_to
be_nil
expect
(
finding
.
location
).
not_to
be_nil
expect
(
finding
.
message
).
not_to
be_nil
expect
(
finding
.
solution
).
not_to
be_nil
end
end
end
context
'invalid data'
do
let
(
:artifact
)
{
create
(
:ee_ci_job_artifact
,
:sast
)
}
let
(
:finding_without_name
)
{
build
(
:ci_reports_security_finding
,
name:
nil
)
}
let
(
:report
)
{
Gitlab
::
Ci
::
Reports
::
Security
::
Report
.
new
(
'container_scanning'
,
nil
,
nil
)
}
context
'invalid data'
do
let
(
:artifact
)
{
create
(
:ee_ci_job_artifact
,
:sast
)
}
let
(
:finding_without_name
)
{
build
(
:ci_reports_security_finding
,
name:
nil
)
}
let
(
:report
)
{
Gitlab
::
Ci
::
Reports
::
Security
::
Report
.
new
(
'container_scanning'
,
nil
,
nil
)
}
before
do
allow
(
Gitlab
::
ErrorTracking
).
to
receive
(
:track_and_raise_exception
).
and_call_original
report
.
add_finding
(
finding_without_name
)
end
before
do
allow
(
Gitlab
::
ErrorTracking
).
to
receive
(
:track_and_raise_exception
).
and_call_original
report
.
add_finding
(
finding_without_name
)
end
it
'raises invalid record error'
do
expect
{
subject
.
execute
}.
to
raise_error
(
ActiveRecord
::
RecordInvalid
)
end
it
'raises invalid record error'
do
expect
{
subject
.
execute
}.
to
raise_error
(
ActiveRecord
::
RecordInvalid
)
end
it
'reports the error correctly'
do
expected_params
=
finding_without_name
.
to_hash
.
dig
(
:raw_metadata
)
expect
{
subject
.
execute
}.
to
raise_error
{
|
error
|
expect
(
Gitlab
::
ErrorTracking
).
to
have_received
(
:track_and_raise_exception
).
with
(
error
,
create_params:
expected_params
)
}
it
'reports the error correctly'
do
expected_params
=
finding_without_name
.
to_hash
.
dig
(
:raw_metadata
)
expect
{
subject
.
execute
}.
to
raise_error
{
|
error
|
expect
(
Gitlab
::
ErrorTracking
).
to
have_received
(
:track_and_raise_exception
).
with
(
error
,
create_params:
expected_params
)
}
end
end
end
end
context
'with existing data from previous pipeline'
do
let
(
:finding_identifier_fingerprint
)
do
build
(
:ci_reports_security_identifier
,
external_id:
"CIPHER_INTEGRITY"
).
fingerprint
end
context
'with existing data from previous pipeline'
do
let
(
:finding_identifier_fingerprint
)
do
build
(
:ci_reports_security_identifier
,
external_id:
"CIPHER_INTEGRITY"
).
fingerprint
end
let
(
:scanner
)
{
build
(
:vulnerabilities_scanner
,
project:
project
,
external_id:
'find_sec_bugs'
,
name:
'Find Security Bugs'
)
}
let
(
:identifier
)
{
build
(
:vulnerabilities_identifier
,
project:
project
,
fingerprint:
finding_identifier_fingerprint
)
}
let
(
:different_identifier
)
{
build
(
:vulnerabilities_identifier
,
project:
project
)
}
let!
(
:new_artifact
)
{
create
(
:ee_ci_job_artifact
,
:sast
,
job:
new_build
)
}
let
(
:new_build
)
{
create
(
:ci_build
,
pipeline:
new_pipeline
)
}
let
(
:new_pipeline
)
{
create
(
:ci_pipeline
,
project:
project
)
}
let
(
:new_report
)
{
new_pipeline
.
security_reports
.
get_report
(
report_type
.
to_s
,
artifact
)
}
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
(
:scanner
)
{
build
(
:vulnerabilities_scanner
,
project:
project
,
external_id:
'find_sec_bugs'
,
name:
'Find Security Bugs'
)
}
let
(
:identifier
)
{
build
(
:vulnerabilities_identifier
,
project:
project
,
fingerprint:
finding_identifier_fingerprint
)
}
let
(
:different_identifier
)
{
build
(
:vulnerabilities_identifier
,
project:
project
)
}
let!
(
:new_artifact
)
{
create
(
:ee_ci_job_artifact
,
:sast
,
job:
new_build
)
}
let
(
:new_build
)
{
create
(
:ci_build
,
pipeline:
new_pipeline
)
}
let
(
:new_pipeline
)
{
create
(
:ci_pipeline
,
project:
project
)
}
let
(
:new_report
)
{
new_pipeline
.
security_reports
.
get_report
(
report_type
.
to_s
,
artifact
)
}
let
(
:existing_signature
)
{
create
(
:vulnerabilities_finding_signature
,
finding:
finding
)
}
let
(
:trait
)
{
:sast
}
let
(
:trait
)
{
:sast
}
let
(
:finding_location_fingerprint
)
do
build
(
:ci_reports_security_locations_sast
,
file_path:
"groovy/src/main/java/com/gitlab/security_products/tests/App.groovy"
,
start_line:
"29"
,
end_line:
"29"
).
fingerprint
end
let
(
:finding_location_fingerprint
)
do
build
(
:ci_reports_security_locations_sast
,
file_path:
"groovy/src/main/java/com/gitlab/security_products/tests/App.groovy"
,
start_line:
"29"
,
end_line:
"29"
).
fingerprint
end
let!
(
:finding
)
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
identifiers:
[
identifier
],
primary_identifier:
identifier
,
scanner:
scanner
,
project:
project
,
uuid:
"e5388f40-18f5-566d-95c6-d64c6f46a00a"
,
location_fingerprint:
finding_location_fingerprint
)
end
let!
(
:finding
)
do
created_finding
=
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
identifiers:
[
identifier
],
primary_identifier:
identifier
,
scanner:
scanner
,
project:
project
,
uuid:
"e5388f40-18f5-566d-95c6-d64c6f46a00a"
,
location_fingerprint:
finding_location_fingerprint
)
let!
(
:vulnerability
)
{
create
(
:vulnerability
,
findings:
[
finding
],
project:
project
)
}
existing_finding
=
report
.
findings
.
find
{
|
f
|
f
.
location
.
fingerprint
==
created_finding
.
location_fingerprint
}
let
(
:desired_uuid
)
do
Security
::
VulnerabilityUUID
.
generate
(
report_type:
finding
.
report_type
,
primary_identifier_fingerprint:
finding
.
primary_identifier
.
fingerprint
,
location_fingerprint:
finding
.
location_fingerprint
,
project_id:
finding
.
project_id
)
end
create
(
:vulnerabilities_finding_signature
,
finding:
created_finding
,
algorithm_type:
existing_finding
.
signatures
.
first
.
algorithm_type
,
signature_sha:
existing_finding
.
signatures
.
first
.
signature_sha
)
let!
(
:finding_with_uuidv5
)
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
identifiers:
[
different_identifier
],
primary_identifier:
different_identifier
,
scanner:
scanner
,
project:
project
,
location_fingerprint:
'34661e23abcf78ff80dfcc89d0700437612e3f88'
)
end
created_finding
end
let!
(
:vulnerability_with_uuid5
)
{
create
(
:vulnerability
,
findings:
[
finding_with_uuidv5
],
project:
project
)
}
let!
(
:vulnerability
)
{
create
(
:vulnerability
,
findings:
[
finding
],
project:
project
)
}
before
do
project
.
add_developer
(
user
)
allow
(
new_pipeline
).
to
receive
(
:user
).
and_return
(
user
)
end
let
(
:desired_uuid
)
do
Security
::
VulnerabilityUUID
.
generate
(
report_type:
finding
.
report_type
,
primary_identifier_fingerprint:
finding
.
primary_identifier
.
fingerprint
,
location_fingerprint:
finding
.
location_fingerprint
,
project_id:
finding
.
project_id
)
end
subject
{
described_class
.
new
(
new_pipeline
,
new_report
).
execute
}
let!
(
:finding_with_uuidv5
)
do
create
(
:vulnerabilities_finding
,
pipelines:
[
pipeline
],
identifiers:
[
different_identifier
],
primary_identifier:
different_identifier
,
scanner:
scanner
,
project:
project
,
location_fingerprint:
'34661e23abcf78ff80dfcc89d0700437612e3f88'
)
end
it
'does not change existing UUIDv5'
do
expect
{
subject
}.
not_to
change
(
finding_with_uuidv5
,
:uuid
)
end
let!
(
:vulnerability_with_uuid5
)
{
create
(
:vulnerability
,
findings:
[
finding_with_uuidv5
],
project:
project
)
}
it
'updates UUIDv4 to UUIDv5'
do
subject
before
do
project
.
add_developer
(
user
)
allow
(
new_pipeline
).
to
receive
(
:user
).
and_return
(
user
)
end
expect
(
finding
.
reload
.
uuid
).
to
eq
(
desired_uuid
)
end
subject
{
described_class
.
new
(
new_pipeline
,
new_report
).
execute
}
it
'reuses existing scanner
'
do
expect
{
subject
}.
not_to
change
{
Vulnerabilities
::
Scanner
.
count
}
end
it
'does not change existing UUIDv5
'
do
expect
{
subject
}.
not_to
change
(
finding_with_uuidv5
,
:uuid
)
end
it
'inserts only new identifiers and reuse existing ones
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Identifier
.
count
}.
by
(
5
)
end
it
'updates UUIDv4 to UUIDv5
'
do
finding
.
uuid
=
'00000000-1111-2222-3333-444444444444'
finding
.
save!
it
'inserts only new findings and reuse existing ones'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Finding
.
count
}.
by
(
4
)
end
# 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
it
'inserts all finding pipelines (join model) for this new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingPipeline
.
where
(
pipeline:
new_pipeline
).
count
}.
by
(
5
)
end
subject
it
'inserts new vulnerabilities with data from findings from this new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerability
.
count
}.
by
(
4
)
end
expect
(
finding
.
reload
.
uuid
).
to
eq
(
desired_uuid
)
end
it
'updates existing findings with new data'
do
subject
it
'reuses existing scanner'
do
expect
{
subject
}.
not_to
change
{
Vulnerabilities
::
Scanner
.
count
}
end
expect
(
finding
.
reload
).
to
have_attributes
(
severity:
'medium'
,
name:
'Cipher with no integrity'
)
end
it
'inserts only new identifiers and reuse existing ones'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Identifier
.
count
}.
by
(
5
)
end
it
'updates signatures to match new valu
es'
do
existing_signature
unsupported_signature
it
'inserts only new findings and reuse existing on
es'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
Finding
.
count
}.
by
(
4
)
end
expect
(
finding
.
signatures
.
count
).
to
eq
(
2
)
signature_algs
=
finding
.
signatures
.
map
(
&
:algorithm_type
).
sort
e
xpect
(
signature_algs
).
to
eq
(
%w[hash location]
)
it
'inserts all finding pipelines (join model) for this new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
FindingPipeline
.
where
(
pipeline:
new_pipeline
).
count
}.
by
(
5
)
e
nd
subject
it
'inserts new vulnerabilities with data from findings from this new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerability
.
count
}.
by
(
4
)
end
finding
.
reload
existing_signature
.
reload
it
'updates existing findings with new data'
do
subject
# check that unsupported algorithm is not deleted
expect
(
finding
.
signatures
.
count
).
to
eq
(
3
)
signature_algs
=
finding
.
signatures
.
sort
.
map
(
&
:algorithm_type
)
expect
(
signature_algs
).
to
eq
(
%w[hash location scope_offset]
)
expect
(
finding
.
reload
).
to
have_attributes
(
severity:
'medium'
,
name:
'Cipher with no integrity'
)
end
# check that the existing hash signature was updated/reused
expect
(
existing_signature
.
id
).
to
eq
(
finding
.
signatures
.
min
.
id
)
it
'updates signatures to match new values'
do
next
unless
vulnerability_finding_signatures_enabled
# check that the unsupported signature was not deleted
expect
(
::
Vulnerabilities
::
FindingSignature
.
exists?
(
unsupported_signature
.
id
)).
to
eq
(
true
)
end
expect
(
finding
.
signatures
.
count
).
to
eq
(
1
)
expect
(
finding
.
signatures
.
first
.
algorithm_type
).
to
eq
(
'hash'
)
it
'updates existing vulnerability with new data'
do
subject
existing_signature
=
finding
.
signatures
.
first
expect
(
vulnerability
.
reload
).
to
have_attributes
(
severity:
'medium'
,
title:
'Cipher with no integrity'
,
title_html:
'Cipher with no integrity'
)
end
subject
context
'when the existing vulnerability is resolved with the latest report'
do
let!
(
:existing_vulnerability
)
{
create
(
:vulnerability
,
report_type:
report_type
,
project:
project
)
}
finding
.
reload
existing_signature
.
reload
it
'marks the vulnerability as resolved on default branch'
do
expect
{
subject
}.
to
change
{
existing_vulnerability
.
reload
.
resolved_on_default_branch
}.
from
(
false
).
to
(
true
)
end
end
expect
(
finding
.
signatures
.
count
).
to
eq
(
2
)
signature_algs
=
finding
.
signatures
.
sort_by
(
&
:priority
).
map
(
&
:algorithm_type
)
expect
(
signature_algs
).
to
eq
(
%w[hash scope_offset]
)
context
'when the existing resolved vulnerability is discovered again on the latest report'
do
before
do
vulnerability
.
update!
(
resolved_on_default_branch:
true
)
# check that the existing hash signature was updated/reused
expect
(
existing_signature
.
id
).
to
eq
(
finding
.
signatures
.
find
(
&
:algorithm_hash?
).
id
)
end
it
'marks the vulnerability as not resolved on default branch'
do
expect
{
subject
}.
to
change
{
vulnerability
.
reload
.
resolved_on_default_branch
}.
from
(
true
).
to
(
false
)
end
end
it
'updates existing vulnerability with new data'
do
subject
context
'when the finding is not valid'
do
before
do
allow
(
Gitlab
::
AppLogger
).
to
receive
(
:warn
)
allow_next_instance_of
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Finding
)
do
|
finding
|
allow
(
finding
).
to
receive
(
:valid?
).
and_return
(
false
)
end
expect
(
vulnerability
.
reload
).
to
have_attributes
(
severity:
'medium'
,
title:
'Cipher with no integrity'
,
title_html:
'Cipher with no integrity'
)
end
it
'does not create a new finding'
do
expect
{
subject
}.
not_to
change
{
Vulnerabilities
::
Finding
.
count
}
end
context
'when the existing vulnerability is resolved with the latest report'
do
let!
(
:existing_vulnerability
)
{
create
(
:vulnerability
,
report_type:
report_type
,
project:
project
)
}
it
'does not raise an error'
do
expect
{
subject
}.
not_to
raise_error
it
'marks the vulnerability as resolved on default branch'
do
expect
{
subject
}.
to
change
{
existing_vulnerability
.
reload
.
resolved_on_default_branch
}.
from
(
false
).
to
(
true
)
end
end
it
'puts a warning log'
do
subject
context
'when the existing resolved vulnerability is discovered again on the latest report'
do
before
do
vulnerability
.
update_column
(
:resolved_on_default_branch
,
true
)
end
expect
(
Gitlab
::
AppLogger
).
to
have_received
(
:warn
).
exactly
(
new_report
.
findings
.
length
).
times
it
'marks the vulnerability as not resolved on default branch'
do
expect
{
subject
}.
to
change
{
vulnerability
.
reload
.
resolved_on_default_branch
}.
from
(
true
).
to
(
false
)
end
end
end
context
'vulnerability issue link'
do
context
'when there is no associated issue feedback with finding'
do
it
'does not insert issue links from the new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
IssueLink
.
count
}.
by
(
0
)
context
'when the finding is not valid'
do
before
do
allow
(
Gitlab
::
AppLogger
).
to
receive
(
:warn
)
allow_next_instance_of
(
::
Gitlab
::
Ci
::
Reports
::
Security
::
Finding
)
do
|
finding
|
allow
(
finding
).
to
receive
(
:valid?
).
and_return
(
false
)
end
end
end
context
'when there is an associated issue feedback with finding'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:issue_feedback
)
do
create
(
:vulnerability_feedback
,
:sast
,
:issue
,
issue:
issue
,
project:
project
,
project_fingerprint:
new_report
.
findings
.
first
.
project_fingerprint
)
it
'does not create a new finding'
do
expect
{
subject
}.
not_to
change
{
Vulnerabilities
::
Finding
.
count
}
end
it
'
inserts issue links from the new pipeline
'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
IssueLink
.
count
}.
by
(
1
)
it
'
does not raise an error
'
do
expect
{
subject
}.
not_to
raise_error
end
it
'
the issue link is valid
'
do
it
'
puts a warning log
'
do
subject
finding
=
Vulnerabilities
::
Finding
.
find_by
(
uuid:
new_report
.
findings
.
first
.
uuid
)
vulnerability_id
=
finding
.
vulnerability_id
issue_id
=
issue
.
id
issue_link
=
Vulnerabilities
::
IssueLink
.
find_by
(
vulnerability_id:
vulnerability_id
,
issue_id:
issue_id
)
expect
(
issue_link
).
not_to
be_nil
expect
(
Gitlab
::
AppLogger
).
to
have_received
(
:warn
).
exactly
(
new_report
.
findings
.
length
).
times
end
end
end
end
context
'with existing data from same pipeline'
do
let!
(
:finding
)
{
create
(
:vulnerabilities_finding
,
project:
project
,
pipelines:
[
pipeline
])
}
let
(
:trait
)
{
:sast
}
context
'vulnerability issue link'
do
context
'when there is no assoiciated issue feedback with finding'
do
it
'does not insert issue links from the new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
IssueLink
.
count
}.
by
(
0
)
end
end
it
'skips report'
do
expect
(
subject
).
to
eq
({
status: :error
,
message:
"sast report already stored for this pipeline, skipping..."
})
end
end
context
'when there is an associated issue feedback with finding'
do
let
(
:issue
)
{
create
(
:issue
,
project:
project
)
}
let!
(
:issue_feedback
)
do
create
(
:vulnerability_feedback
,
:sast
,
:issue
,
issue:
issue
,
project:
project
,
project_fingerprint:
new_report
.
findings
.
first
.
project_fingerprint
)
end
context
'start auto_fix'
do
before
do
stub_licensed_features
(
vulnerability_auto_fix:
true
)
end
it
'inserts issue links from the new pipeline'
do
expect
{
subject
}.
to
change
{
Vulnerabilities
::
IssueLink
.
count
}.
by
(
1
)
end
context
'with auto fix supported report type
'
do
let
(
:trait
)
{
:dependency_scanning
}
it
'the issue link is valid
'
do
subject
context
'when auto fix enabled'
do
it
'start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
to
receive
(
:perform_async
).
with
(
pipeline
.
id
)
finding
=
Vulnerabilities
::
Finding
.
find_by
(
uuid:
new_report
.
findings
.
first
.
uuid
)
vulnerability_id
=
finding
.
vulnerability_id
issue_id
=
issue
.
id
issue_link
=
Vulnerabilities
::
IssueLink
.
find_by
(
vulnerability_id:
vulnerability_id
,
issue_id:
issue_id
)
subject
expect
(
issue_link
).
not_to
be_nil
end
end
end
end
context
'when auto fix disabled'
do
context
'when feature flag is disabled'
do
before
do
stub_feature_flags
(
security_auto_fix:
false
)
end
context
'with existing data from same pipeline'
do
let!
(
:finding
)
{
create
(
:vulnerabilities_finding
,
project:
project
,
pipelines:
[
pipeline
])
}
let
(
:trait
)
{
:sast
}
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
it
'skips report'
do
expect
(
subject
).
to
eq
({
status: :error
,
message:
"sast report already stored for this pipeline, skipping..."
})
end
end
subject
end
end
context
'start auto_fix'
do
before
do
stub_licensed_features
(
vulnerability_auto_fix:
true
)
end
context
'when auto fix feature is disabled'
do
before
do
project
.
security_setting
.
update!
(
auto_fix_dependency_scanning:
false
)
end
context
'with auto fix supported report type'
do
let
(
:trait
)
{
:dependency_scanning
}
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
context
'when auto fix enabled'
do
it
'start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
to
receive
(
:perform_async
).
with
(
pipeline
.
id
)
subject
end
end
context
'when licensed feature is unavailable'
do
before
do
stub_licensed_features
(
vulnerability_auto_fix:
false
)
context
'when auto fix disabled'
do
context
'when feature flag is disabled'
do
before
do
stub_feature_flags
(
security_auto_fix:
false
)
end
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
subject
end
end
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
context
'when auto fix feature is disabled'
do
before
do
project
.
security_setting
.
update_column
(
:auto_fix_dependency_scanning
,
false
)
end
subject
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
subject
end
end
end
context
'when security setting is not created'
do
before
do
project
.
security_setting
.
destroy!
project
.
reload
context
'when licensed feature is unavailable'
do
before
do
stub_licensed_features
(
vulnerability_auto_fix:
false
)
end
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
subject
end
end
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
expect
(
subject
[
:status
]).
to
eq
(
:success
)
context
'when security setting is not created'
do
before
do
project
.
security_setting
.
destroy!
project
.
reload
end
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
expect
(
subject
[
:status
]).
to
eq
(
:success
)
end
end
end
end
end
context
'with auto fix not supported report type'
do
let
(
:trait
)
{
:sast
}
context
'with auto fix not supported report type'
do
let
(
:trait
)
{
:sast
}
before
do
stub_licensed_features
(
vulnerability_auto_fix:
true
)
end
before
do
stub_licensed_features
(
vulnerability_auto_fix:
true
)
end
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
it
'does not start auto fix worker'
do
expect
(
Security
::
AutoFixWorker
).
not_to
receive
(
:perform_async
)
subject
subject
end
end
end
end
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment