Commit a9616d3e authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Shinya Maeda

Add ability to sort vulnerabilities by state

This change adds ability to sort vulnerabilities by state in
GraphQL.
parent d617e6e0
# frozen_string_literal: true
class AddIndexOnVulnerabilitiesStateCase < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_vulnerabilities_on_state_case_id'
STATE_ORDER_ARRAY_POSITION = 'ARRAY_POSITION(ARRAY[1, 4, 3, 2]::smallint[], state)'
disable_ddl_transaction!
def up
add_concurrent_index :vulnerabilities, "#{STATE_ORDER_ARRAY_POSITION}, id DESC", name: INDEX_NAME
add_concurrent_index :vulnerabilities, "#{STATE_ORDER_ARRAY_POSITION} DESC, id DESC", name: "#{INDEX_NAME}_desc"
end
def down
remove_concurrent_index_by_name :vulnerabilities, "#{INDEX_NAME}_desc"
remove_concurrent_index_by_name :vulnerabilities, INDEX_NAME
end
end
346d0e913212d6e84528d47228ba7e6d0cf4a396e7fc921f7c684acfaaeeedb8
\ No newline at end of file
......@@ -21603,6 +21603,10 @@ CREATE INDEX index_vulnerabilities_on_resolved_by_id ON vulnerabilities USING bt
CREATE INDEX index_vulnerabilities_on_start_date_sourcing_milestone_id ON vulnerabilities USING btree (start_date_sourcing_milestone_id);
CREATE INDEX index_vulnerabilities_on_state_case_id ON vulnerabilities USING btree (array_position(ARRAY[(1)::smallint, (4)::smallint, (3)::smallint, (2)::smallint], state), id DESC);
CREATE INDEX index_vulnerabilities_on_state_case_id_desc ON vulnerabilities USING btree (array_position(ARRAY[(1)::smallint, (4)::smallint, (3)::smallint, (2)::smallint], state) DESC, id DESC);
CREATE INDEX index_vulnerabilities_on_updated_by_id ON vulnerabilities USING btree (updated_by_id);
CREATE INDEX index_vulnerability_exports_on_author_id ON vulnerability_exports USING btree (author_id);
......
......@@ -20158,7 +20158,7 @@ type Vulnerability implements Noteable {
severity: VulnerabilitySeverity
"""
State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED)
State of the vulnerability (DETECTED, CONFIRMED, RESOLVED, DISMISSED)
"""
state: VulnerabilityState
......@@ -20820,6 +20820,16 @@ enum VulnerabilitySort {
"""
severity_desc
"""
State in ascending order
"""
state_asc
"""
State in descending order
"""
state_desc
"""
Title in ascending order
"""
......
......@@ -58503,7 +58503,7 @@
},
{
"name": "state",
"description": "State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED)",
"description": "State of the vulnerability (DETECTED, CONFIRMED, RESOLVED, DISMISSED)",
"args": [
],
......@@ -60482,6 +60482,18 @@
"description": "Report Type in ascending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state_desc",
"description": "State in descending order",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state_asc",
"description": "State in ascending order",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
......@@ -60501,7 +60513,7 @@
"deprecationReason": null
},
{
"name": "DISMISSED",
"name": "CONFIRMED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
......@@ -60513,7 +60525,7 @@
"deprecationReason": null
},
{
"name": "CONFIRMED",
"name": "DISMISSED",
"description": null,
"isDeprecated": false,
"deprecationReason": null
......@@ -2821,7 +2821,7 @@ Represents a vulnerability.
| `resolvedOnDefaultBranch` | Boolean! | Indicates whether the vulnerability is fixed on the default branch or not |
| `scanner` | VulnerabilityScanner | Scanner metadata for the vulnerability. |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, CONFIRMED, RESOLVED, DISMISSED) |
| `title` | String | Title of the vulnerability |
| `userNotesCount` | Int! | Number of user notes attached to the vulnerability |
| `userPermissions` | VulnerabilityPermissions! | Permissions for the current user on the resource |
......@@ -3767,6 +3767,8 @@ Vulnerability sort values.
| `report_type_desc` | Report Type in descending order |
| `severity_asc` | Severity in ascending order |
| `severity_desc` | Severity in descending order |
| `state_asc` | State in ascending order |
| `state_desc` | State in descending order |
| `title_asc` | Title in ascending order |
| `title_desc` | Title in descending order |
......
......@@ -13,5 +13,7 @@ module Types
value 'detected_asc', 'Detection timestamp in ascending order'
value 'report_type_desc', 'Report Type in descending order'
value 'report_type_asc', 'Report Type in ascending order'
value 'state_desc', 'State in descending order'
value 'state_asc', 'State in ascending order'
end
end
......@@ -54,7 +54,9 @@ module EE
has_many :notes, as: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :user_mentions, class_name: 'VulnerabilityUserMention'
enum state: { detected: 1, dismissed: 2, resolved: 3, confirmed: 4 }
# keep the order of the values in the state enum, it is used in state_order method to properly order vulnerabilities based on state
# remember to recreate index_vulnerabilities_on_state_case_id index when you update or extend this enum
enum state: { detected: 1, confirmed: 4, resolved: 3, dismissed: 2 }
enum severity: ::Vulnerabilities::Finding::SEVERITY_LEVELS, _prefix: :severity
enum confidence: ::Vulnerabilities::Finding::CONFIDENCE_LEVELS, _prefix: :confidence
enum report_type: ::Vulnerabilities::Finding::REPORT_TYPES
......@@ -105,6 +107,8 @@ module EE
scope :order_created_at_desc, -> { reorder(created_at: :desc, id: :desc) }
scope :order_report_type_asc, -> { select(*arel.projections, report_type_order.as('case_order_value')).reorder(report_type_order.asc, id: :desc) }
scope :order_report_type_desc, -> { select(*arel.projections, report_type_order.as('case_order_value')).reorder(report_type_order.desc, id: :desc) }
scope :order_state_asc, -> { select(*arel.projections, state_order.as('array_position')).reorder(state_order.asc, id: :desc) }
scope :order_state_desc, -> { select(*arel.projections, state_order.as('array_position')).reorder(state_order.desc, id: :desc) }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :with_limit, -> (maximum) { limit(maximum) }
......@@ -208,6 +212,16 @@ module EE
end
end
def state_order
Arel::Nodes::NamedFunction.new(
'ARRAY_POSITION',
[
Arel.sql("ARRAY#{states.values}::smallint[]"),
arel_table[:state]
]
)
end
def active_states
ACTIVE_STATES
end
......@@ -230,6 +244,8 @@ module EE
when 'detected_asc' then order_created_at_asc
when 'report_type_desc' then order_report_type_desc
when 'report_type_asc' then order_report_type_asc
when 'state_desc' then order_state_desc
when 'state_asc' then order_state_asc
else
order_severity_desc
end
......
---
title: Add ability to sort vulnerabilities by state
merge_request: 42973
author:
type: added
......@@ -6,6 +6,6 @@ RSpec.describe GitlabSchema.types['VulnerabilitySort'] do
it { expect(described_class.graphql_name).to eq('VulnerabilitySort') }
it 'exposes all the existing Vulnerability sort orders' do
expect(described_class.values.keys).to include(*%w[severity_desc severity_asc title_desc title_asc detected_desc detected_asc report_type_desc report_type_asc])
expect(described_class.values.keys).to include(*%w[severity_desc severity_asc title_desc title_asc detected_desc detected_asc report_type_desc report_type_asc state_desc state_asc])
end
end
......@@ -301,7 +301,7 @@ RSpec.describe Vulnerability do
end
end
describe '.order_report_type' do
describe '.order_report_type_' do
let_it_be(:vulnerability_dast) { create(:vulnerability, :dast) }
let_it_be(:vulnerability_secret_detection) { create(:vulnerability, :secret_detection) }
let_it_be(:vulnerability_sast) { create(:vulnerability, :sast) }
......@@ -324,6 +324,29 @@ RSpec.describe Vulnerability do
end
end
describe '.order_state_' do
let_it_be(:vulnerability_confirmed) { create(:vulnerability, :confirmed) }
let_it_be(:vulnerability_detected) { create(:vulnerability, :detected) }
let_it_be(:vulnerability_dismissed) { create(:vulnerability, :dismissed) }
let_it_be(:vulnerability_resolved) { create(:vulnerability, :resolved) }
describe 'asc' do
subject { described_class.order_state_asc }
it 'returns vulnerabilities ordered by state' do
is_expected.to eq([vulnerability_detected, vulnerability_confirmed, vulnerability_resolved, vulnerability_dismissed])
end
end
describe 'desc' do
subject { described_class.order_state_desc }
it 'returns vulnerabilities ordered by state' do
is_expected.to eq([vulnerability_dismissed, vulnerability_resolved, vulnerability_confirmed, vulnerability_detected])
end
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) }
......
......@@ -96,6 +96,8 @@ module Gitlab
['similarity', order_value.direction, order_value.expr]
elsif ordering_by_case?(order_value)
['case_order_value', order_value.direction, order_value.expr]
elsif ordering_by_array_position?(order_value)
['array_position', order_value.direction, order_value.expr]
else
[order_value.expr.name, order_value.direction, nil]
end
......@@ -106,6 +108,11 @@ module Gitlab
order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'lower'
end
# determine if ordering using ARRAY_POSITION, eg. "ORDER BY ARRAY_POSITION(Array[4,3,1,2]::smallint, state)"
def ordering_by_array_position?(order_value)
order_value.expr.is_a?(Arel::Nodes::NamedFunction) && order_value.expr&.name&.downcase == 'array_position'
end
# determine if ordering using SIMILARITY scoring based on Gitlab::Database::SimilarityScore
def ordering_by_similarity?(order_value)
Gitlab::Database::SimilarityScore.order_by_similarity?(order_value)
......
......@@ -74,6 +74,18 @@ RSpec.describe Gitlab::Graphql::Pagination::Keyset::OrderInfo do
expect(order_list.first.sort_direction).to eq :asc
end
end
context 'when ordering by ARRAY_POSITION', :aggregate_failuers do
let(:array_position) { Arel::Nodes::NamedFunction.new('ARRAY_POSITION', [Arel.sql("ARRAY[1,0]::smallint[]"), Project.arel_table[:auto_cancel_pending_pipelines]]) }
let(:relation) { Project.order(array_position.asc) }
it 'assigns the right attribute name, named function, and direction' do
expect(order_list.count).to eq 1
expect(order_list.first.attribute_name).to eq 'array_position'
expect(order_list.first.named_function).to be_kind_of(Arel::Nodes::NamedFunction)
expect(order_list.first.sort_direction).to eq :asc
end
end
end
describe '#validate_ordering' do
......
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