Commit 9aa864dd authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '222483-add-filtering-vulnerabilities-visible-by-user' into 'master'

Add filtering vulnerabilities for autocomplete in Vulnerabilities Finder

See merge request gitlab-org/gitlab!42200
parents 8ede953e d90de582
# frozen_string_literal: true
module Autocomplete
class VulnerabilitiesAutocompleteFinder
attr_reader :current_user, :vulnerable, :params
# current_user - the User object of the user that wants to view the list of Vulnerabilities
#
# vulnerable - any object that has a #vulnerabilities method that returns a collection of vulnerabilitie
# params - a Hash containing additional parameters to set
#
# The supported parameters are those supported by
# `Security::VulnerabilitiesFinder`.
def initialize(current_user, vulnerable, params = {})
@current_user = current_user
@vulnerable = vulnerable
@params = params
end
DEFAULT_AUTOCOMPLETE_LIMIT = 5
def execute
return [] unless vulnerable.feature_available?(:security_dashboard)
::Security::VulnerabilitiesFinder # rubocop: disable CodeReuse/Finder
.new(vulnerable, params)
.execute
.autocomplete_search(params[:search].to_s)
.with_limit(DEFAULT_AUTOCOMPLETE_LIMIT)
.order_id_desc
.visible_to_user_and_access_level(current_user, ::Gitlab::Access::DEVELOPER)
end
end
end
......@@ -12,6 +12,7 @@ module EE
include ::Awardable
include ::Referable
include ::Presentable
include ::Gitlab::SQL::Pattern
TooManyDaysError = Class.new(StandardError)
......@@ -71,6 +72,7 @@ module EE
scope :with_findings_scanner_and_identifiers, -> { includes(findings: [:scanner, :identifiers, finding_identifiers: :identifier]) }
scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) }
scope :visible_to_user_and_access_level, -> (user, access_level) { where(project_id: ::Project.visible_to_user_and_access_level(user, access_level)) }
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) }
......@@ -86,12 +88,24 @@ module EE
where(exist_query, ::Vulnerabilities::IssueLink.select(1).where(issue_links[:vulnerability_id].eq(arel_table[:id])))
end
scope :autocomplete_search, -> (query) do
return self if query.blank?
id_as_text = Arel::Nodes::NamedFunction.new('CAST', [arel_table[:id].as('TEXT')])
fuzzy_search(query, [:title])
.or(where(id_as_text.matches("%#{sanitize_sql_like(query.squish)}%")))
end
scope :order_severity_asc, -> { reorder(severity: :asc, id: :desc) }
scope :order_severity_desc, -> { reorder(severity: :desc, id: :desc) }
scope :order_title_asc, -> { reorder(title: :asc, id: :desc) }
scope :order_title_desc, -> { reorder(title: :desc, id: :desc) }
scope :order_created_at_asc, -> { reorder(created_at: :asc, id: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc, id: :desc) }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :with_limit, -> (maximum) { limit(maximum) }
delegate :scanner_name, :scanner_external_id, :metadata, :message, :cve, :description,
to: :finding, prefix: true, allow_nil: true
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Autocomplete::VulnerabilitiesAutocompleteFinder do
describe '#execute' do
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:project, refind: true) { create(:project, group: group) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
let(:params) { {} }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, vulnerable, params).execute }
shared_examples 'autocomplete vulnerabilities finder' do
context 'when user does not have access to project' do
it { is_expected.to be_empty }
end
context 'when user has access to project' do
before do
vulnerable.add_developer(user)
end
context 'when security dashboards are not enabled' do
it { is_expected.to be_empty }
end
context 'when security dashboards are enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it { is_expected.to match_array([vulnerability]) }
context 'when multiple vulnerabilities are found' do
before do
create_list(:vulnerability, 10, project: project)
end
it 'returns max 5 items' do
expect(subject.count).to eq(5)
end
it 'is sorted descending by id' do
expect(subject).to be_sorted(:id, :desc)
end
end
context 'when search is provided in params' do
context 'and it matches ID of vulnerability' do
let(:params) { { search: vulnerability.id.to_s } }
it { is_expected.to match_array([vulnerability]) }
end
context 'and it matches title of vulnerability' do
let(:params) { { search: vulnerability.title } }
it { is_expected.to match_array([vulnerability]) }
end
context 'and it does not match neither title or id of vulnerability' do
let(:params) { { search: non_existing_record_id.to_s } }
it { is_expected.to be_empty }
end
end
end
end
end
context 'when vulnerable is project' do
let(:vulnerable) { project }
it_behaves_like 'autocomplete vulnerabilities finder'
end
context 'when vulnerable is group' do
let(:vulnerable) { group }
it_behaves_like 'autocomplete vulnerabilities finder'
end
end
end
......@@ -92,6 +92,70 @@ RSpec.describe Vulnerability do
end
end
describe '.visible_to_user_and_access_level' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let!(:vulnerability1) { create(:vulnerability, project: project1) }
let!(:vulnerability2) { create(:vulnerability, project: project2) }
let(:user) { create(:user) }
before do
project1.add_developer(user)
end
subject { described_class.visible_to_user_and_access_level(user, ::Gitlab::Access::DEVELOPER) }
it 'returns vulnerabilities visible for given user with provided access level' do
is_expected.to contain_exactly(vulnerability1)
end
end
describe '.with_limit' do
let(:project) { create(:project) }
let!(:vulnerabilities) { create_list(:vulnerability, 10, project: project) }
subject { described_class.with_limit(5) }
it 'returns vulnerabilities limited by provided value' do
expect(subject.count).to eq(5)
end
end
describe '.autocomplete_search' do
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project) }
let!(:vulnerability_1) { create(:vulnerability, title: 'Predictable pseudorandom number generator') }
let!(:vulnerability_2) { create(:vulnerability, title: 'Use of pseudorandom MD2, MD4, or MD5 hash function.') }
subject { described_class.autocomplete_search(search) }
where(:search, :filtered_vulnerabilities) do
'PSEUDORANDOM' | [:vulnerability_1, :vulnerability_2]
'Predictable PSEUDORANDOM' | [:vulnerability_1]
'mD2' | [:vulnerability_2]
end
with_them do
it 'returns the vulnerabilities filtered' do
expect(subject).to match_array(filtered_vulnerabilities.map { |name| public_send(name) })
end
end
context 'when id is used in search params' do
let(:search) { vulnerability_1.id.to_s }
it { is_expected.to match_array([vulnerability_1]) }
end
context 'when query is empty' do
let(:search) { '' }
it { is_expected.to match_array([vulnerability_1, vulnerability_2]) }
end
end
describe '.for_projects' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
......@@ -225,6 +289,18 @@ RSpec.describe Vulnerability do
end
end
describe '.order_id_desc' do
subject { described_class.order_id_desc }
before do
create_list(:vulnerability, 10)
end
it 'returns vulnerabilities ordered by id' do
is_expected.to be_sorted(:id, :desc)
end
end
describe '.with_resolution' do
let_it_be(:vulnerability_with_resolution) { create(:vulnerability, resolved_on_default_branch: true) }
let_it_be(:vulnerability_without_resolution) { create(:vulnerability, resolved_on_default_branch: false) }
......
# frozen_string_literal: true
# Assert that this collection is sorted by argument and order
#
# By default, this checks that the collection is sorted ascending
# but you can check order by specific field and order by passing
# them, eg:
#
# ```
# expect(collection).to be_sorted(:field, :desc)
# ```
RSpec::Matchers.define :be_sorted do |by, order = :asc|
match do |actual|
next true unless actual.present? # emtpy collection is sorted
actual
.then { |collection| by ? collection.sort_by(&by) : collection.sort }
.then { |sorted_collection| order.to_sym == :desc ? sorted_collection.reverse : sorted_collection }
.then { |sorted_collection| sorted_collection == actual }
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