Commit 9abd3281 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by James Lopez

Add filtering by activity (has_resolution, has_issues) to Vulnerability

This change adds new filters to GraphQL API for Vulnerabilities that
allows to filter vulnerabilities with created/related issues and
vulnerabilities with and without resolution.
parent 0a8ed206
......@@ -6907,6 +6907,16 @@ type Group {
"""
first: Int
"""
Returns only the vulnerabilities which have linked issues
"""
hasIssues: Boolean
"""
Returns only the vulnerabilities which have been resolved on default branch
"""
hasResolution: Boolean
"""
Returns the last _n_ elements from the list.
"""
......@@ -12709,6 +12719,16 @@ type Project {
"""
first: Int
"""
Returns only the vulnerabilities which have linked issues
"""
hasIssues: Boolean
"""
Returns only the vulnerabilities which have been resolved on default branch
"""
hasResolution: Boolean
"""
Returns the last _n_ elements from the list.
"""
......@@ -13476,6 +13496,16 @@ type Query {
"""
first: Int
"""
Returns only the vulnerabilities which have linked issues
"""
hasIssues: Boolean
"""
Returns only the vulnerabilities which have been resolved on default branch
"""
hasResolution: Boolean
"""
Returns the last _n_ elements from the list.
"""
......
......@@ -19071,6 +19071,26 @@
},
"defaultValue": "severity_desc"
},
{
"name": "hasResolution",
"description": "Returns only the vulnerabilities which have been resolved on default branch",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "hasIssues",
"description": "Returns only the vulnerabilities which have linked issues",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -37221,6 +37241,26 @@
},
"defaultValue": "severity_desc"
},
{
"name": "hasResolution",
"description": "Returns only the vulnerabilities which have been resolved on default branch",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "hasIssues",
"description": "Returns only the vulnerabilities which have linked issues",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -39560,6 +39600,26 @@
},
"defaultValue": "severity_desc"
},
{
"name": "hasResolution",
"description": "Returns only the vulnerabilities which have been resolved on default branch",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "hasIssues",
"description": "Returns only the vulnerabilities which have linked issues",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -12,6 +12,8 @@
# report_types: only return vulnerabilities from these report types
# severities: only return vulnerabilities with these severities
# states: only return vulnerabilities in these states
# has_resolution: only return vulnerabilities thah have resolution
# has_issues: only return vulnerabilities that have issues linked
# sort: return vulnerabilities ordered by severity_asc or severity_desc
module Security
......@@ -29,6 +31,8 @@ module Security
filter_by_severities
filter_by_states
filter_by_scanners
filter_by_resolution
filter_by_issues
sort(vulnerabilities)
end
......@@ -67,6 +71,18 @@ module Security
end
end
def filter_by_resolution
if params[:has_resolution].in?([true, false])
@vulnerabilities = vulnerabilities.with_resolution(params[:has_resolution])
end
end
def filter_by_issues
if params[:has_issues].in?([true, false])
@vulnerabilities = vulnerabilities.with_issues(params[:has_issues])
end
end
def sort(items)
items.order_by(params[:sort])
end
......
......@@ -31,6 +31,14 @@ module Resolvers
default_value: 'severity_desc',
description: 'List vulnerabilities by sort order'
argument :has_resolution, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Returns only the vulnerabilities which have been resolved on default branch'
argument :has_issues, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Returns only the vulnerabilities which have linked issues'
def resolve(**args)
return Vulnerability.none unless vulnerable
......
......@@ -71,6 +71,14 @@ class Vulnerability < ApplicationRecord
scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(Vulnerabilities::Scanner.with_external_id(scanners)) }
scope :grouped_by_severity, -> { reorder(severity: :desc).group(:severity) }
scope :with_resolution, -> (has_resolution = true) { where(resolved_on_default_branch: has_resolution) }
scope :with_issues, -> (has_issues = true) do
exist_query = has_issues ? 'EXISTS (?)' : 'NOT EXISTS (?)'
issue_links = Vulnerabilities::IssueLink.arel_table
where(exist_query, Vulnerabilities::IssueLink.select(1).where(issue_links[:vulnerability_id].eq(arel_table[:id])))
end
scope :order_severity_asc, -> { reorder(severity: :asc, id: :desc) }
scope :order_severity_desc, -> { reorder(severity: :desc, id: :desc) }
......
---
title: Add filtering by activity (has_resolution, has_issues) to Vulnerability
merge_request: 41650
author:
type: added
......@@ -6,11 +6,11 @@ RSpec.describe Security::VulnerabilitiesFinder do
let_it_be(:project) { create(:project) }
let_it_be(:vulnerability1) do
create(:vulnerability, :with_findings, severity: :low, report_type: :sast, state: :detected, project: project)
create(:vulnerability, :with_findings, :with_issue_links, severity: :low, report_type: :sast, state: :detected, project: project)
end
let_it_be(:vulnerability2) do
create(:vulnerability, :with_findings, severity: :high, report_type: :dependency_scanning, state: :confirmed, project: project)
create(:vulnerability, :with_findings, resolved_on_default_branch: true, severity: :high, report_type: :dependency_scanning, state: :confirmed, project: project)
end
let_it_be(:vulnerability3) do
......@@ -98,6 +98,46 @@ RSpec.describe Security::VulnerabilitiesFinder do
end
end
context 'when filtered by has_issues argument' do
let(:filters) { { has_issues: has_issues } }
context 'when has_issues is set to true' do
let(:has_issues) { true }
it 'only returns vulnerabilities that have issues' do
is_expected.to contain_exactly(vulnerability1)
end
end
context 'when has_issues is set to false' do
let(:has_issues) { false }
it 'only returns vulnerabilities that does not have issues' do
is_expected.to contain_exactly(vulnerability2, vulnerability3)
end
end
end
context 'when filtered by has_resolution argument' do
let(:filters) { { has_resolution: has_resolution } }
context 'when has_resolution is set to true' do
let(:has_resolution) { true }
it 'only returns vulnerabilities that have resolution' do
is_expected.to contain_exactly(vulnerability2)
end
end
context 'when has_resolution is set to false' do
let(:has_resolution) { false }
it 'only returns vulnerabilities that do not have resolution' do
is_expected.to contain_exactly(vulnerability1, vulnerability3)
end
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)
......
......@@ -12,11 +12,11 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let_it_be(:low_vulnerability) do
create(:vulnerability, :with_findings, :detected, :low, :dast, project: project)
create(:vulnerability, :with_findings, :detected, :low, :dast, :with_issue_links, project: project)
end
let_it_be(:critical_vulnerability) do
create(:vulnerability, :with_findings, :detected, :critical, :sast, project: project)
create(:vulnerability, :with_findings, :detected, :critical, :sast, resolved_on_default_branch: true, project: project)
end
let_it_be(:high_vulnerability) do
......@@ -85,6 +85,46 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do
end
end
context 'when given value for hasIssues argument' do
let(:params) { { has_issues: has_issues } }
context 'when has_issues is set to true' do
let(:has_issues) { true }
it 'only returns vulnerabilities that have issues' do
is_expected.to contain_exactly(low_vulnerability)
end
end
context 'when has_issues is set to false' do
let(:has_issues) { false }
it 'only returns vulnerabilities that does not have issues' do
is_expected.to contain_exactly(critical_vulnerability, high_vulnerability)
end
end
end
context 'when given value for has_resolution argument' do
let(:params) { { has_resolution: has_resolution } }
context 'when has_resolution is set to true' do
let(:has_resolution) { true }
it 'only returns resolution that have resolution' do
is_expected.to contain_exactly(critical_vulnerability)
end
end
context 'when has_resolution is set to false' do
let(:has_resolution) { false }
it 'only returns resolution that does not have resolution' do
is_expected.to contain_exactly(low_vulnerability, high_vulnerability)
end
end
end
context 'when given project IDs' do
let_it_be(:group) { create(:group) }
let_it_be(:project2) { create(:project, namespace: group) }
......
......@@ -180,6 +180,56 @@ RSpec.describe Vulnerability do
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) }
subject { described_class.with_resolution(with_resolution) }
context 'when no argument is provided' do
subject { described_class.with_resolution }
it { is_expected.to eq([vulnerability_with_resolution]) }
end
context 'when true argument is provided' do
let(:with_resolution) { true }
it { is_expected.to eq([vulnerability_with_resolution]) }
end
context 'when false argument is provided' do
let(:with_resolution) { false }
it { is_expected.to eq([vulnerability_without_resolution]) }
end
end
describe '.with_issues' do
let_it_be(:vulnerability_with_issues) { create(:vulnerability, :with_issue_links) }
let_it_be(:vulnerability_without_issues) { create(:vulnerability) }
subject { described_class.with_issues(with_issues) }
context 'when no argument is provided' do
subject { described_class.with_issues }
it { is_expected.to eq([vulnerability_with_issues]) }
end
context 'when true argument is provided' do
let(:with_issues) { true }
it { is_expected.to eq([vulnerability_with_issues]) }
end
context 'when false argument is provided' do
let(:with_issues) { false }
it { is_expected.to eq([vulnerability_without_issues]) }
end
end
describe '.order_by' do
subject { described_class.order_by(method) }
......
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