Commit 9e3e43be authored by Olivier Gonzalez's avatar Olivier Gonzalez Committed by Dmitriy Zaporozhets

Add locations POROs for vulnerabilities

Move fingerprint logic into the location class for later reuse
parent 18edd650
......@@ -50,11 +50,11 @@ module Security
find_params = {
scanner: scanners_objects[occurrence.scanner.key],
primary_identifier: identifiers_objects[occurrence.primary_identifier.key],
location_fingerprint: occurrence.location_fingerprint
location_fingerprint: occurrence.location.fingerprint
}
create_params = occurrence.to_hash
.except(:compare_key, :identifiers, :scanner) # rubocop: disable CodeReuse/ActiveRecord
.except(:compare_key, :identifiers, :location, :scanner) # rubocop: disable CodeReuse/ActiveRecord
begin
project.vulnerabilities
......
......@@ -50,7 +50,7 @@ module Gitlab
report_type: report.type,
name: data['message'],
compare_key: data['cve'],
location_fingerprint: generate_location_fingerprint(data['location']),
location: create_location(data['location']),
severity: parse_level(data['severity']),
confidence: parse_level(data['confidence']),
scanner: scanner,
......@@ -96,7 +96,7 @@ module Gitlab
input.blank? ? 'undefined' : input.downcase
end
def generate_location_fingerprint(location)
def create_location(location_data)
raise NotImplementedError
end
end
......
......@@ -131,8 +131,12 @@ module Gitlab
nil
end
def generate_location_fingerprint(location)
Digest::SHA1.hexdigest("#{location['operating_system']}:#{location.dig('dependency', 'package', 'name')}")
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::ContainerScanning.new(
image: location_data['image'],
operating_system: location_data['operating_system'],
package_name: location_data.dig('dependency', 'package', 'name'),
package_version: location_data.dig('dependency', 'version'))
end
end
end
......
......@@ -45,8 +45,12 @@ module Gitlab
end
end
def generate_location_fingerprint(location)
Digest::SHA1.hexdigest("#{location['path']}:#{location['method']}:#{location['param']}")
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::Dast.new(
hostname: location_data['hostname'],
method_name: location_data['method'],
param: location_data['param'],
path: location_data['path'])
end
end
end
......
......@@ -11,8 +11,11 @@ module Gitlab
private
def generate_location_fingerprint(location)
Digest::SHA1.hexdigest("#{location['file']}:#{location.dig('dependency', 'package', 'name')}")
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::DependencyScanning.new(
file_path: location_data['file'],
package_name: location_data.dig('dependency', 'package', 'name'),
package_version: location_data.dig('dependency', 'version'))
end
end
end
......
......@@ -11,8 +11,13 @@ module Gitlab
private
def generate_location_fingerprint(location)
Digest::SHA1.hexdigest("#{location['file']}:#{location['start_line']}:#{location['end_line']}")
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::Sast.new(
file_path: location_data['file'],
start_line: location_data['start_line'],
end_line: location_data['end_line'],
class_name: location_data['class'],
method_name: location_data['method'])
end
end
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class Base
include ::Gitlab::Utils::StrongMemoize
def ==(other)
other.fingerprint == fingerprint
end
def fingerprint
strong_memoize(:fingerprint) do
Digest::SHA1.hexdigest(fingerprint_data)
end
end
private
def fingerprint_data
raise NotImplemented
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class ContainerScanning < Base
attr_reader :image
attr_reader :operating_system
attr_reader :package_name
attr_reader :package_version
def initialize(image:, operating_system:, package_name: nil, package_version: nil)
@image = image
@operating_system = operating_system
@package_name = package_name
@package_version = package_version
end
private
def fingerprint_data
"#{operating_system}:#{package_name}"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class Dast < Base
attr_reader :hostname
attr_reader :method_name
attr_reader :param
attr_reader :path
def initialize(hostname:, method_name:, path:, param: nil)
@hostname = hostname
@method_name = method_name
@param = param
@path = path
end
private
def fingerprint_data
"#{path}:#{method_name}:#{param}"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class DependencyScanning < Base
attr_reader :file_path
attr_reader :package_name
attr_reader :package_version
def initialize(file_path:, package_name:, package_version: nil)
@file_path = file_path
@package_name = package_name
@package_version = package_version
end
private
def fingerprint_data
"#{file_path}:#{package_name}"
end
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class Sast < Base
attr_reader :class_name
attr_reader :end_line
attr_reader :file_path
attr_reader :method_name
attr_reader :start_line
def initialize(file_path:, start_line:, end_line: nil, class_name: nil, method_name: nil)
@class_name = class_name
@end_line = end_line
@file_path = file_path
@method_name = method_name
@start_line = start_line
end
private
def fingerprint_data
"#{file_path}:#{start_line}:#{end_line}"
end
end
end
end
end
end
end
......@@ -8,7 +8,7 @@ module Gitlab
attr_reader :compare_key
attr_reader :confidence
attr_reader :identifiers
attr_reader :location_fingerprint
attr_reader :location
attr_reader :metadata_version
attr_reader :name
attr_reader :project_fingerprint
......@@ -18,11 +18,11 @@ module Gitlab
attr_reader :severity
attr_reader :uuid
def initialize(compare_key:, identifiers:, location_fingerprint:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, uuid:, confidence: nil, severity: nil) # rubocop:disable Metrics/ParameterLists
def initialize(compare_key:, identifiers:, location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, uuid:, confidence: nil, severity: nil) # rubocop:disable Metrics/ParameterLists
@compare_key = compare_key
@confidence = confidence
@identifiers = identifiers
@location_fingerprint = location_fingerprint
@location = location
@metadata_version = metadata_version
@name = name
@raw_metadata = raw_metadata
......@@ -39,7 +39,7 @@ module Gitlab
compare_key
confidence
identifiers
location_fingerprint
location
metadata_version
name
project_fingerprint
......@@ -59,7 +59,7 @@ module Gitlab
def ==(other)
other.report_type == report_type &&
other.location_fingerprint == location_fingerprint &&
other.location == location &&
other.primary_identifier == primary_identifier
end
......
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_locations_container_scanning, class: ::Gitlab::Ci::Reports::Security::Locations::ContainerScanning do
image 'registry.gitlab.com/my/project:latest'
operating_system 'debian:9'
package_name 'glibc'
package_version '1.2.3'
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Locations::ContainerScanning.new(attributes)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_locations_dast, class: ::Gitlab::Ci::Reports::Security::Locations::Dast do
hostname 'my-app.com'
method_name 'GET'
param 'X-Content-Type-Options'
path '/some/path'
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Locations::Dast.new(attributes)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_locations_dependency_scanning, class: ::Gitlab::Ci::Reports::Security::Locations::DependencyScanning do
file_path 'app/pom.xml'
package_name 'io.netty/netty'
package_version '1.2.3'
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Locations::DependencyScanning.new(attributes)
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_security_locations_sast, class: ::Gitlab::Ci::Reports::Security::Locations::Sast do
file_path 'maven/src/main/java/com/gitlab/security_products/tests/App.java'
start_line 29
end_line 31
class_name 'com.gitlab.security_products.tests.App'
method_name 'insecureCypher'
skip_create
initialize_with do
::Gitlab::Ci::Reports::Security::Locations::Sast.new(attributes)
end
end
end
......@@ -5,7 +5,7 @@ FactoryBot.define do
compare_key 'this_is_supposed_to_be_a_unique_value'
confidence :medium
identifiers { Array.new(1) { FactoryBot.build(:ci_reports_security_identifier) } }
location_fingerprint '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
location factory: :ci_reports_security_locations_sast
metadata_version 'sast:1.0'
name 'Cipher with no integrity'
report_type :sast
......
......@@ -31,10 +31,16 @@ describe Gitlab::Ci::Parsers::Security::ContainerScanning do
expect(report.scanners.length).to eq(1)
end
it "generates expected location fingerprint" do
expected = Digest::SHA1.hexdigest('debian:9:glibc')
expect(report.occurrences.first.location_fingerprint).to eq(expected)
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning)
expect(location).to have_attributes(
image: 'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '2.24-11+deb9u3'
)
end
it "generates expected metadata_version" do
......
......@@ -23,12 +23,16 @@ describe Gitlab::Ci::Parsers::Security::Dast do
expect(report.scanners.length).to eq(1)
end
it 'generates expected location fingerprint' do
expected1 = Digest::SHA1.hexdigest(':GET:X-Content-Type-Options')
expected2 = Digest::SHA1.hexdigest('/:GET:X-Content-Type-Options')
expect(report.occurrences.first.location_fingerprint).to eq(expected1)
expect(report.occurrences.last.location_fingerprint).to eq(expected2)
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Dast)
expect(location).to have_attributes(
hostname: 'http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io',
method_name: 'GET',
param: 'X-Content-Type-Options',
path: ''
)
end
describe 'occurrence properties' do
......
......@@ -12,10 +12,10 @@ describe Gitlab::Ci::Parsers::Security::DependencyScanning do
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type) }
let(:parser) { described_class.new }
where(:report_format, :occurrence_count, :identifier_count, :scanner_count, :fingerprint, :version) do
:dependency_scanning | 4 | 7 | 2 | '2773f8cc955346ab1f756b94aa310db8e17c0944' | '1.3'
:dependency_scanning_deprecated | 4 | 7 | 2 | '2773f8cc955346ab1f756b94aa310db8e17c0944' | '1.3'
:dependency_scanning_remediation | 2 | 3 | 1 | '228998b5db51d86d3b091939e2f5873ada0a14a1' | '2.0'
where(:report_format, :occurrence_count, :identifier_count, :scanner_count, :file_path, :package_name, :package_version, :version) do
:dependency_scanning | 4 | 7 | 2 | 'app/pom.xml' | 'io.netty/netty' | '3.9.1.Final' | '1.3'
:dependency_scanning_deprecated | 4 | 7 | 2 | 'app/pom.xml' | 'io.netty/netty' | '3.9.1.Final' | '1.3'
:dependency_scanning_remediation | 2 | 3 | 1 | 'yarn.lock' | 'debug' | '1.0.5' | '2.0'
end
with_them do
......@@ -33,8 +33,15 @@ describe Gitlab::Ci::Parsers::Security::DependencyScanning do
expect(report.scanners.length).to eq(scanner_count)
end
it "generates expected location fingerprint" do
expect(report.occurrences.first.location_fingerprint).to eq(fingerprint)
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::DependencyScanning)
expect(location).to have_attributes(
file_path: file_path,
package_name: package_name,
package_version: package_version
)
end
it "generates expected metadata_version" do
......
......@@ -26,8 +26,17 @@ describe Gitlab::Ci::Parsers::Security::Sast do
expect(report.scanners.length).to eq(3)
end
it "generates expected location fingerprint" do
expect(report.occurrences.first.location_fingerprint).to eq('d869ba3f0b3347eb2749135a437dc07c8ae0f420')
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Sast)
expect(location).to have_attributes(
file_path: 'python/hardcoded/hardcoded-tmp.py',
start_line: 1,
end_line: 1,
class_name: nil,
method_name: nil
)
end
it "generates expected metadata_version" do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Locations::ContainerScanning do
let(:params) do
{
image: 'registry.gitlab.com/my/project:latest',
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '1.2.3'
}
end
let(:mandatory_params) { %i[image operating_system] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('debian:9:glibc') }
it_behaves_like 'vulnerability location'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Locations::Dast do
let(:params) do
{
hostname: 'my-app.com',
method_name: 'GET',
param: 'X-Content-Type-Options',
path: '/some/path'
}
end
let(:mandatory_params) { %i[path method_name] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('/some/path:GET:X-Content-Type-Options') }
it_behaves_like 'vulnerability location'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Locations::DependencyScanning do
let(:params) do
{
file_path: 'app/pom.xml',
package_name: 'io.netty/netty',
package_version: '1.2.3'
}
end
let(:mandatory_params) { %i[file_path package_name] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('app/pom.xml:io.netty/netty') }
it_behaves_like 'vulnerability location'
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Locations::Sast do
let(:params) do
{
file_path: 'src/main/App.java',
start_line: 29,
end_line: 31,
class_name: 'com.gitlab.security_products.tests.App',
method_name: 'insecureCypher'
}
end
let(:mandatory_params) { %i[file_path start_line] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') }
it_behaves_like 'vulnerability location'
end
......@@ -9,13 +9,14 @@ describe Gitlab::Ci::Reports::Security::Occurrence do
let(:primary_identifier) { create(:ci_reports_security_identifier) }
let(:other_identifier) { create(:ci_reports_security_identifier) }
let(:scanner) { create(:ci_reports_security_scanner) }
let(:location) { create(:ci_reports_security_locations_sast) }
let(:params) do
{
compare_key: 'this_is_supposed_to_be_a_unique_value',
confidence: :medium,
identifiers: [primary_identifier, other_identifier],
location_fingerprint: '4e5b6966dd100170b4b1ad599c7058cce91b57b4',
location: location,
metadata_version: 'sast:1.0',
name: 'Cipher with no integrity',
raw_metadata: 'I am a stringified json object',
......@@ -35,7 +36,7 @@ describe Gitlab::Ci::Reports::Security::Occurrence do
confidence: :medium,
project_fingerprint: '9a73f32d58d87d94e3dc61c4c1a94803f6014258',
identifiers: [primary_identifier, other_identifier],
location_fingerprint: '4e5b6966dd100170b4b1ad599c7058cce91b57b4',
location: location,
metadata_version: 'sast:1.0',
name: 'Cipher with no integrity',
raw_metadata: 'I am a stringified json object',
......@@ -47,7 +48,7 @@ describe Gitlab::Ci::Reports::Security::Occurrence do
end
end
%i[compare_key identifiers location_fingerprint metadata_version name raw_metadata report_type scanner uuid].each do |attribute|
%i[compare_key identifiers location metadata_version name raw_metadata report_type scanner uuid].each do |attribute|
context "when attribute #{attribute} is missing" do
before do
params.delete(attribute)
......@@ -70,7 +71,7 @@ describe Gitlab::Ci::Reports::Security::Occurrence do
compare_key: occurrence.compare_key,
confidence: occurrence.confidence,
identifiers: occurrence.identifiers,
location_fingerprint: occurrence.location_fingerprint,
location: occurrence.location,
metadata_version: occurrence.metadata_version,
name: occurrence.name,
project_fingerprint: occurrence.project_fingerprint,
......@@ -101,22 +102,19 @@ describe Gitlab::Ci::Reports::Security::Occurrence do
let(:identifier) { create(:ci_reports_security_identifier) }
let(:other_identifier) { create(:ci_reports_security_identifier, external_type: 'other_identifier') }
let(:location) { create(:ci_reports_security_locations_sast) }
let(:other_location) { create(:ci_reports_security_locations_sast, file_path: 'other/file.rb') }
report_type = 'sast'
fingerprint = '4e5b6966dd100170b4b1ad599c7058cce91b57b4'
other_report_type = 'dependency_scanning'
other_fingerprint = '368d8604fb8c0g455d129274f5773aa2f31d4f7q'
where(:report_type_1, :location_fingerprint_1, :identifier_1, :report_type_2, :location_fingerprint_2, :identifier_2, :equal, :case_name) do
report_type | fingerprint | -> { identifier } | report_type | fingerprint | -> { identifier } | true | 'when report_type, location_fingerprint and primary identifier are equal'
report_type | fingerprint | -> { identifier } | other_report_type | fingerprint | -> { identifier } | false | 'when report_type is different'
report_type | fingerprint | -> { identifier } | report_type | other_fingerprint | -> { identifier } | false | 'when location_fingerprint is different'
report_type | fingerprint | -> { identifier } | report_type | fingerprint | -> { other_identifier } | false | 'when primary identifier is different'
where(:report_type_1, :location_1, :identifier_1, :report_type_2, :location_2, :identifier_2, :equal, :case_name) do
'sast' | -> { location } | -> { identifier } | 'sast' | -> { location } | -> { identifier } | true | 'when report_type, location and primary identifier are equal'
'sast' | -> { location } | -> { identifier } | 'dependency_scanning' | -> { location } | -> { identifier } | false | 'when report_type is different'
'sast' | -> { location } | -> { identifier } | 'sast' | -> { other_location } | -> { identifier } | false | 'when location is different'
'sast' | -> { location } | -> { identifier } | 'sast' | -> { location } | -> { other_identifier } | false | 'when primary identifier is different'
end
with_them do
let(:occurrence_1) { create(:ci_reports_security_occurrence, report_type: report_type_1, location_fingerprint: location_fingerprint_1, identifiers: [identifier_1.call]) }
let(:occurrence_2) { create(:ci_reports_security_occurrence, report_type: report_type_2, location_fingerprint: location_fingerprint_2, identifiers: [identifier_2.call]) }
let(:occurrence_1) { create(:ci_reports_security_occurrence, report_type: report_type_1, location: location_1.call, identifiers: [identifier_1.call]) }
let(:occurrence_2) { create(:ci_reports_security_occurrence, report_type: report_type_2, location: location_2.call, identifiers: [identifier_2.call]) }
it "returns #{params[:equal]}" do
expect(occurrence_1 == occurrence_2).to eq(equal)
......
# frozen_string_literal: true
shared_examples 'vulnerability location' do
describe '#initialize' do
subject { described_class.new(**params) }
context 'when all params are given' do
it 'initializes an instance' do
expect { subject }.not_to raise_error
expect(subject).to have_attributes(**params)
end
end
where(:param) do
mandatory_params
end
with_them do
context "when param #{params[:param]} is missing" do
before do
params.delete(param)
end
it 'raises an error' do
expect { subject }.to raise_error(ArgumentError)
end
end
end
end
describe '#fingerprint' do
subject { described_class.new(**params).fingerprint }
it "generates expected fingerprint" do
expect(subject).to eq(expected_fingerprint)
end
end
describe '#==' do
let(:location_1) { create(:ci_reports_security_locations_sast) }
let(:location_2) { create(:ci_reports_security_locations_sast) }
subject { location_1 == location_2 }
it "returns true when fingerprints are equal" do
allow(location_1).to receive(:fingerprint).and_return('fingerprint')
allow(location_2).to receive(:fingerprint).and_return('fingerprint')
expect(subject).to eq(true)
end
it "returns false when fingerprints are different" do
allow(location_1).to receive(:fingerprint).and_return('fingerprint')
allow(location_2).to receive(:fingerprint).and_return('another_fingerprint')
expect(subject).to eq(false)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment