Commit 8e2844e4 authored by Victor Zagorodny's avatar Victor Zagorodny

Add VulnerabilitiesFinder for new Vulnerability

Rename old VulnerabilitiesFinder to
VulnerabilityFindingsFinder because it finds
Occurrences (Findings in new terminology) and
update its usages. Add new VulnerabilitiesFinder
that finds Vulnerabilities of a Project and
respects the pagination of results.
parent a8482a92
...@@ -40,6 +40,6 @@ module VulnerabilityFindingsActions ...@@ -40,6 +40,6 @@ module VulnerabilityFindingsActions
end end
def vulnerability_findings(collection = :latest) def vulnerability_findings(collection = :latest)
::Security::VulnerabilitiesFinder.new(vulnerable, params: filter_params).execute(collection) ::Security::VulnerabilityFindingsFinder.new(vulnerable, params: filter_params).execute(collection)
end end
end end
...@@ -2,75 +2,27 @@ ...@@ -2,75 +2,27 @@
# Security::VulnerabilitiesFinder # Security::VulnerabilitiesFinder
# #
# Used to filter Vulnerabilities::Occurrences by set of params for Security Dashboard # Used to filter Vulnerability record by set of params for Vulnerabilities API
# #
# Arguments: # Arguments:
# vulnerable - object to filter vulnerabilities # project: a Project to query for Vulnerabilities
# params: # params:
# severity: Array<String> # page: Integer
# confidence: Array<String> # per_page: Integer
# project: Array<String>
# report_type: Array<String>
module Security module Security
class VulnerabilitiesFinder class VulnerabilitiesFinder
attr_accessor :params attr_reader :project
attr_reader :vulnerable attr_reader :page, :per_page
def initialize(vulnerable, params: {}) def initialize(project, params = {})
@vulnerable = vulnerable @project = project
@params = params @page = params[:page] || 1
@per_page = params[:per_page] || 20
end end
def execute(scope = :latest) def execute
collection = init_collection(scope) project.vulnerabilities.page(page).per(per_page)
collection = by_report_type(collection)
collection = by_project(collection)
collection = by_severity(collection)
collection = by_confidence(collection)
collection
end
private
def by_report_type(items)
return items unless params[:report_type].present?
items.by_report_types(
Vulnerabilities::Occurrence::REPORT_TYPES.values_at(
*params[:report_type]).compact)
end
def by_project(items)
return items unless params[:project_id].present?
items.by_projects(params[:project_id])
end
def by_severity(items)
return items unless params[:severity].present?
items.by_severities(
Vulnerabilities::Occurrence::SEVERITY_LEVELS.values_at(
*params[:severity]).compact)
end
def by_confidence(items)
return items unless params[:confidence].present?
items.by_confidences(
Vulnerabilities::Occurrence::CONFIDENCE_LEVELS.values_at(
*params[:confidence]).compact)
end
def init_collection(scope)
if scope == :all
vulnerable.all_vulnerabilities
elsif scope == :with_sha
vulnerable.latest_vulnerabilities_with_sha
else
vulnerable.latest_vulnerabilities
end
end end
end end
end end
# frozen_string_literal: true
# Security::VulnerabilityFindingsFinder
#
# Used to filter Vulnerabilities::Occurrences by set of params for Security Dashboard
#
# Arguments:
# vulnerable - object to filter vulnerabilities
# params:
# severity: Array<String>
# confidence: Array<String>
# project: Array<String>
# report_type: Array<String>
module Security
# TODO: check usages and rename `vulnerable` associations to mention the term "finding";
# probably, add new changes to https://gitlab.com/gitlab-org/gitlab/merge_requests/17351
class VulnerabilityFindingsFinder
attr_accessor :params
attr_reader :vulnerable
def initialize(vulnerable, params: {})
@vulnerable = vulnerable
@params = params
end
def execute(scope = :latest)
collection = init_collection(scope)
collection = by_report_type(collection)
collection = by_project(collection)
collection = by_severity(collection)
collection = by_confidence(collection)
collection
end
private
def by_report_type(items)
return items unless params[:report_type].present?
items.by_report_types(
Vulnerabilities::Occurrence::REPORT_TYPES.values_at(
*params[:report_type]).compact)
end
def by_project(items)
return items unless params[:project_id].present?
items.by_projects(params[:project_id])
end
def by_severity(items)
return items unless params[:severity].present?
items.by_severities(
Vulnerabilities::Occurrence::SEVERITY_LEVELS.values_at(
*params[:severity]).compact)
end
def by_confidence(items)
return items unless params[:confidence].present?
items.by_confidences(
Vulnerabilities::Occurrence::CONFIDENCE_LEVELS.values_at(
*params[:confidence]).compact)
end
def init_collection(scope)
if scope == :all
vulnerable.all_vulnerabilities
elsif scope == :with_sha
vulnerable.latest_vulnerabilities_with_sha
else
vulnerable.latest_vulnerabilities
end
end
end
end
...@@ -24,7 +24,7 @@ module Gitlab ...@@ -24,7 +24,7 @@ module Gitlab
private private
def found_vulnerabilities def found_vulnerabilities
::Security::VulnerabilitiesFinder.new(group, params: filters).execute(:all) ::Security::VulnerabilityFindingsFinder.new(group, params: filters).execute(:all)
end end
def cached_vulnerability_history def cached_vulnerability_history
......
...@@ -12,7 +12,7 @@ module Gitlab ...@@ -12,7 +12,7 @@ module Gitlab
def fetch(range, force: false) def fetch(range, force: false)
Rails.cache.fetch(cache_key, force: force, expires_in: 1.day) do Rails.cache.fetch(cache_key, force: force, expires_in: 1.day) do
vulnerabilities = ::Security::VulnerabilitiesFinder vulnerabilities = ::Security::VulnerabilityFindingsFinder
.new(group, params: { project_id: [project_id] }) .new(group, params: { project_id: [project_id] })
.execute(:all) .execute(:all)
.count_by_day_and_severity(range) .count_by_day_and_severity(range)
......
...@@ -89,5 +89,11 @@ FactoryBot.modify do ...@@ -89,5 +89,11 @@ FactoryBot.modify do
trait :github_imported do trait :github_imported do
import_type { 'github' } import_type { 'github' }
end end
trait :with_vulnerabilities do
after(:create) do |project|
create_list(:vulnerability, 3, :opened, project: project)
end
end
end end
end end
...@@ -3,132 +3,12 @@ ...@@ -3,132 +3,12 @@
require 'spec_helper' require 'spec_helper'
describe Security::VulnerabilitiesFinder do describe Security::VulnerabilitiesFinder do
describe '#execute' do let(:project) { create(:project, :with_vulnerabilities) }
set(:group) { create(:group) } let(:params) { { page: 2, per_page: 1 } }
set(:project1) { create(:project, :private, :repository, group: group) }
set(:project2) { create(:project, :private, :repository, group: group) }
set(:pipeline1) { create(:ci_pipeline, :success, project: project1) }
set(:pipeline2) { create(:ci_pipeline, :success, project: project2) }
set(:vulnerability1) { create(:vulnerabilities_occurrence, report_type: :sast, severity: :high, confidence: :high, pipelines: [pipeline1], project: project1) } subject { described_class.new(project, params).execute }
set(:vulnerability2) { create(:vulnerabilities_occurrence, report_type: :dependency_scanning, severity: :medium, confidence: :low, pipelines: [pipeline2], project: project2) }
set(:vulnerability3) { create(:vulnerabilities_occurrence, report_type: :sast, severity: :low, pipelines: [pipeline2], project: project2) }
set(:vulnerability4) { create(:vulnerabilities_occurrence, report_type: :dast, severity: :medium, pipelines: [pipeline1], project: project1) }
subject { described_class.new(group, params: params).execute } it 'returns vulnerabilities of a project and respects pagination params' do
expect(subject).to contain_exactly(project.vulnerabilities.drop(1).take(1).first)
context 'by report type' do
context 'when sast' do
let(:params) { { report_type: %w[sast] } }
it 'includes only sast' do
is_expected.to contain_exactly(vulnerability1, vulnerability3)
end
end
context 'when dependency_scanning' do
let(:params) { { report_type: %w[dependency_scanning] } }
it 'includes only depscan' do
is_expected.to contain_exactly(vulnerability2)
end
end
end
context 'by severity' do
context 'when high' do
let(:params) { { severity: %w[high] } }
it 'includes only high' do
is_expected.to contain_exactly(vulnerability1)
end
end
context 'when medium' do
let(:params) { { severity: %w[medium] } }
it 'includes only medium' do
is_expected.to contain_exactly(vulnerability2, vulnerability4)
end
end
end
context 'by confidence' do
context 'when high' do
let(:params) { { confidence: %w[high] } }
it 'includes only high confidence vulnerabilities' do
is_expected.to contain_exactly(vulnerability1)
end
end
context 'when low' do
let(:params) { { confidence: %w[low] } }
it 'includes only low confidence vulnerabilities' do
is_expected.to contain_exactly(vulnerability2)
end
end
end
context 'by project' do
let(:params) { { project_id: [project2.id] } }
it 'includes only vulnerabilities for one project' do
is_expected.to contain_exactly(vulnerability2, vulnerability3)
end
end
# FIXME: unskip when this filter is implemented
context 'by dismissals' do
let!(:dismissal) do
create(:vulnerability_feedback, :sast, :dismissal,
pipeline: pipeline1,
project: project1,
project_fingerprint: vulnerability1.project_fingerprint)
end
let(:params) { { hide_dismissed: true } }
skip 'exclude dismissal' do
is_expected.to contain_exactly(vulnerability2, vulnerability3, vulnerability4)
end
end
context 'by all filters' do
context 'with found entity' do
let(:params) { { severity: %w[high medium low], project_id: [project1.id, project2.id], report_type: %w[sast dast] } }
it 'filters by all params' do
is_expected.to contain_exactly(vulnerability1, vulnerability3, vulnerability4)
end
end
context 'without found entity' do
let(:params) { { severity: %w[low], project_id: [project1.id], report_type: %w[sast] } }
it 'did not find anything' do
is_expected.to be_empty
end
end
end
context 'by some filters' do
context 'with found entity' do
let(:params) { { project_id: [project2.id], severity: %w[medium low] } }
it 'filters by all params' do
is_expected.to contain_exactly(vulnerability2, vulnerability3)
end
end
context 'without found entity' do
let(:params) { { project_id: project1.id, severity: %w[low] } }
it 'did not find anything' do
is_expected.to be_empty
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Security::VulnerabilityFindingsFinder do
describe '#execute' do
set(:group) { create(:group) }
set(:project1) { create(:project, :private, :repository, group: group) }
set(:project2) { create(:project, :private, :repository, group: group) }
set(:pipeline1) { create(:ci_pipeline, :success, project: project1) }
set(:pipeline2) { create(:ci_pipeline, :success, project: project2) }
set(:vulnerability1) { create(:vulnerabilities_occurrence, report_type: :sast, severity: :high, confidence: :high, pipelines: [pipeline1], project: project1) }
set(:vulnerability2) { create(:vulnerabilities_occurrence, report_type: :dependency_scanning, severity: :medium, confidence: :low, pipelines: [pipeline2], project: project2) }
set(:vulnerability3) { create(:vulnerabilities_occurrence, report_type: :sast, severity: :low, pipelines: [pipeline2], project: project2) }
set(:vulnerability4) { create(:vulnerabilities_occurrence, report_type: :dast, severity: :medium, pipelines: [pipeline1], project: project1) }
subject { described_class.new(group, params: params).execute }
context 'by report type' do
context 'when sast' do
let(:params) { { report_type: %w[sast] } }
it 'includes only sast' do
is_expected.to contain_exactly(vulnerability1, vulnerability3)
end
end
context 'when dependency_scanning' do
let(:params) { { report_type: %w[dependency_scanning] } }
it 'includes only depscan' do
is_expected.to contain_exactly(vulnerability2)
end
end
end
context 'by severity' do
context 'when high' do
let(:params) { { severity: %w[high] } }
it 'includes only high' do
is_expected.to contain_exactly(vulnerability1)
end
end
context 'when medium' do
let(:params) { { severity: %w[medium] } }
it 'includes only medium' do
is_expected.to contain_exactly(vulnerability2, vulnerability4)
end
end
end
context 'by confidence' do
context 'when high' do
let(:params) { { confidence: %w[high] } }
it 'includes only high confidence vulnerabilities' do
is_expected.to contain_exactly(vulnerability1)
end
end
context 'when low' do
let(:params) { { confidence: %w[low] } }
it 'includes only low confidence vulnerabilities' do
is_expected.to contain_exactly(vulnerability2)
end
end
end
context 'by project' do
let(:params) { { project_id: [project2.id] } }
it 'includes only vulnerabilities for one project' do
is_expected.to contain_exactly(vulnerability2, vulnerability3)
end
end
# FIXME: unskip when this filter is implemented
context 'by dismissals' do
let!(:dismissal) do
create(:vulnerability_feedback, :sast, :dismissal,
pipeline: pipeline1,
project: project1,
project_fingerprint: vulnerability1.project_fingerprint)
end
let(:params) { { hide_dismissed: true } }
skip 'exclude dismissal' do
is_expected.to contain_exactly(vulnerability2, vulnerability3, vulnerability4)
end
end
context 'by all filters' do
context 'with found entity' do
let(:params) { { severity: %w[high medium low], project_id: [project1.id, project2.id], report_type: %w[sast dast] } }
it 'filters by all params' do
is_expected.to contain_exactly(vulnerability1, vulnerability3, vulnerability4)
end
end
context 'without found entity' do
let(:params) { { severity: %w[low], project_id: [project1.id], report_type: %w[sast] } }
it 'did not find anything' do
is_expected.to be_empty
end
end
end
context 'by some filters' do
context 'with found entity' do
let(:params) { { project_id: [project2.id], severity: %w[medium low] } }
it 'filters by all params' do
is_expected.to contain_exactly(vulnerability2, vulnerability3)
end
end
context 'without found entity' do
let(:params) { { project_id: project1.id, severity: %w[low] } }
it 'did not find anything' do
is_expected.to be_empty
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment