Commit 7b5525e2 authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Rémy Coutable

Filter vulnerabilities by severity

* Adds filtering by severity to VulnerabilityFinder
* Adds Vulnerability.with_severities
parent 8962f4f8
...@@ -5,18 +5,56 @@ ...@@ -5,18 +5,56 @@
# Used to filter Vulnerability records for Vulnerabilities API # Used to filter Vulnerability records for Vulnerabilities API
# #
# Arguments: # Arguments:
# project: a Project to query for Vulnerabilities # vulnerable: any object that has a #vulnerabilities method that returns a collection of `Vulnerability`s
# filters: optional! a hash with one or more of the following:
# project_ids: if `vulnerable` includes multiple projects (like a Group), this filter will restrict
# the vulnerabilities returned to those in the group's projects that also match these IDs
# report_types: only return vulnerabilities from these report types
# severities: only return vulnerabilities with these severities
# states: only return vulnerabilities in these states
module Security module Security
class VulnerabilitiesFinder class VulnerabilitiesFinder
attr_reader :project def initialize(vulnerable, filters = {})
@filters = filters
def initialize(project) @vulnerabilities = vulnerable.vulnerabilities
@project = project
end end
def execute def execute
project.vulnerabilities filter_by_projects
filter_by_report_types
filter_by_severities
filter_by_states
vulnerabilities
end
private
attr_reader :filters, :vulnerabilities
def filter_by_projects
if filters[:project_ids].present?
@vulnerabilities = vulnerabilities.for_projects(filters[:project_ids])
end
end
def filter_by_report_types
if filters[:report_types].present?
@vulnerabilities = vulnerabilities.with_report_types(filters[:report_types])
end
end
def filter_by_severities
if filters[:severities].present?
@vulnerabilities = vulnerabilities.with_severities(filters[:severities])
end
end
def filter_by_states
if filters[:states].present?
@vulnerabilities = vulnerabilities.with_states(filters[:states])
end
end end
end end
end end
...@@ -305,6 +305,10 @@ module EE ...@@ -305,6 +305,10 @@ module EE
::Gitlab::CurrentSettings.deletion_adjourned_period > 0 ::Gitlab::CurrentSettings.deletion_adjourned_period > 0
end end
def vulnerabilities
::Vulnerability.where(project: ::Project.for_group_and_its_subgroups(self))
end
private private
def custom_project_templates_group_allowed def custom_project_templates_group_allowed
......
...@@ -47,6 +47,11 @@ class Vulnerability < ApplicationRecord ...@@ -47,6 +47,11 @@ class Vulnerability < ApplicationRecord
scope :with_findings, -> { includes(:findings) } scope :with_findings, -> { includes(:findings) }
scope :for_projects, -> (project_ids) { where(project_id: project_ids) }
scope :with_report_types, -> (report_types) { where(report_type: report_types) }
scope :with_severities, -> (severities) { where(severity: severities) }
scope :with_states, -> (states) { where(state: states) }
# There will only be one finding associated with a vulnerability for the foreseeable future # There will only be one finding associated with a vulnerability for the foreseeable future
def finding def finding
findings.first findings.first
......
...@@ -3,11 +3,86 @@ ...@@ -3,11 +3,86 @@
require 'spec_helper' require 'spec_helper'
describe Security::VulnerabilitiesFinder do describe Security::VulnerabilitiesFinder do
let(:project) { create(:project, :with_vulnerabilities) } let_it_be(:project) { create(:project) }
subject { described_class.new(project).execute } let_it_be(:vulnerability1) do
create(:vulnerability, severity: :low, report_type: :sast, state: :detected, project: project)
end
let_it_be(:vulnerability2) do
create(:vulnerability, severity: :medium, report_type: :dast, state: :dismissed, project: project)
end
let_it_be(:vulnerability3) do
create(:vulnerability, severity: :high, report_type: :dependency_scanning, state: :confirmed, project: project)
end
let(:filters) { {} }
let(:vulnerable) { project }
subject { described_class.new(vulnerable, filters).execute }
it 'returns vulnerabilities of a project' do it 'returns vulnerabilities of a project' do
expect(subject).to match_array(project.vulnerabilities) expect(subject).to match_array(project.vulnerabilities)
end end
context 'when not given a second argument' do
subject { described_class.new(project).execute }
it 'does not filter the vulnerability list' do
expect(subject).to match_array(project.vulnerabilities)
end
end
context 'when filtered by report type' do
let(:filters) { { report_types: %w[sast dast] } }
it 'only returns vulnerabilities matching the given report types' do
is_expected.to contain_exactly(vulnerability1, vulnerability2)
end
end
context 'when filtered by severity' do
let(:filters) { { severities: %w[medium high] } }
it 'only returns vulnerabilities matching the given severities' do
is_expected.to contain_exactly(vulnerability2, vulnerability3)
end
end
context 'when filtered by state' do
let(:filters) { { states: %w[detected confirmed] } }
it 'only returns vulnerabilities matching the given states' do
is_expected.to contain_exactly(vulnerability1, vulnerability3)
end
end
context 'when filtered by project' do
let(:group) { create(:group) }
let(:another_project) { create(:project, namespace: group) }
let!(:another_vulnerability) { create(:vulnerability, project: another_project) }
let(:filters) { { project_ids: [another_project.id] } }
let(:vulnerable) { group }
before do
project.update(namespace: group)
end
it 'only returns vulnerabilities matching the given projects' do
is_expected.to contain_exactly(another_vulnerability)
end
end
context 'when filtered by more than one property' do
let_it_be(:vulnerability4) do
create(:vulnerability, severity: :medium, report_type: :sast, state: :detected, project: project)
end
let(:filters) { { report_types: %w[sast], severities: %w[medium] } }
it 'only returns vulnerabilities matching all of the given filters' do
is_expected.to contain_exactly(vulnerability4)
end
end
end end
...@@ -257,6 +257,20 @@ describe Group do ...@@ -257,6 +257,20 @@ describe Group do
end end
end end
describe '#vulnerabilities' do
let(:subgroup) { create(:group, parent: group) }
let(:group_project) { create(:project, namespace: group) }
let(:subgroup_project) { create(:project, namespace: subgroup) }
let!(:group_vulnerability) { create(:vulnerability, project: group_project) }
let!(:subgroup_vulnerability) { create(:vulnerability, project: subgroup_project) }
subject { group.vulnerabilities }
it 'returns vulnerabilities for all projects in the group and its subgroups' do
is_expected.to contain_exactly(group_vulnerability, subgroup_vulnerability)
end
end
describe '#mark_ldap_sync_as_failed' do describe '#mark_ldap_sync_as_failed' do
it 'sets the state to failed' do it 'sets the state to failed' do
group.start_ldap_sync group.start_ldap_sync
......
...@@ -83,6 +83,58 @@ describe Vulnerability do ...@@ -83,6 +83,58 @@ describe Vulnerability do
end end
end end
describe '.for_projects' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let!(:vulnerability1) { create(:vulnerability, project: project1) }
let!(:vulnerability2) { create(:vulnerability, project: project2) }
subject { described_class.for_projects([project1.id]) }
it 'returns vulnerabilities related to the given project IDs' do
is_expected.to contain_exactly(vulnerability1)
end
end
describe '.with_report_types' do
let!(:sast_vulnerability) { create(:vulnerability, report_type: :sast) }
let!(:dast_vulnerability) { create(:vulnerability, report_type: :dast) }
let!(:dependency_scanning_vulnerability) { create(:vulnerability, report_type: :dependency_scanning) }
let(:report_types) { %w[sast dast] }
subject { described_class.with_report_types(report_types) }
it 'returns vulnerabilities matching the given report_types' do
is_expected.to contain_exactly(sast_vulnerability, dast_vulnerability)
end
end
describe '.with_severities' do
let!(:high_vulnerability) { create(:vulnerability, severity: :high) }
let!(:medium_vulnerability) { create(:vulnerability, severity: :medium) }
let!(:low_vulnerability) { create(:vulnerability, severity: :low) }
let(:severities) { %w[medium low] }
subject { described_class.with_severities(severities) }
it 'returns vulnerabilities matching the given severities' do
is_expected.to contain_exactly(medium_vulnerability, low_vulnerability)
end
end
describe '.with_states' do
let!(:detected_vulnerability) { create(:vulnerability, :detected) }
let!(:dismissed_vulnerability) { create(:vulnerability, :dismissed) }
let!(:confirmed_vulnerability) { create(:vulnerability, :confirmed) }
let(:states) { %w[detected confirmed] }
subject { described_class.with_states(states) }
it 'returns vulnerabilities matching the given states' do
is_expected.to contain_exactly(detected_vulnerability, confirmed_vulnerability)
end
end
describe '#finding' do describe '#finding' do
let_it_be(:project) { create(:project, :with_vulnerabilities) } let_it_be(:project) { create(:project, :with_vulnerabilities) }
let_it_be(:vulnerability) { project.vulnerabilities.first } let_it_be(:vulnerability) { project.vulnerabilities.first }
......
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