Commit dae82ca6 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Igor Drozdov

Add scanner filter in Vulnerability GraphQL API

This change adds a new filter that allows querying vulnerabilities by
the scanner that discovered them.
parent c7266a46
...@@ -5122,6 +5122,11 @@ type Group { ...@@ -5122,6 +5122,11 @@ type Group {
""" """
reportType: [VulnerabilityReportType!] reportType: [VulnerabilityReportType!]
"""
Filter vulnerabilities by scanner
"""
scanner: [String!]
""" """
Filter vulnerabilities by severity Filter vulnerabilities by severity
""" """
...@@ -9384,6 +9389,11 @@ type Project { ...@@ -9384,6 +9389,11 @@ type Project {
""" """
reportType: [VulnerabilityReportType!] reportType: [VulnerabilityReportType!]
"""
Filter vulnerabilities by scanner
"""
scanner: [String!]
""" """
Filter vulnerabilities by severity Filter vulnerabilities by severity
""" """
...@@ -10030,6 +10040,11 @@ type Query { ...@@ -10030,6 +10040,11 @@ type Query {
""" """
reportType: [VulnerabilityReportType!] reportType: [VulnerabilityReportType!]
"""
Filter vulnerabilities by scanner
"""
scanner: [String!]
""" """
Filter vulnerabilities by severity Filter vulnerabilities by severity
""" """
......
...@@ -13989,6 +13989,24 @@ ...@@ -13989,6 +13989,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "scanner",
"description": "Filter vulnerabilities by scanner",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Returns the elements in the list that come after the specified cursor.",
...@@ -27428,6 +27446,24 @@ ...@@ -27428,6 +27446,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "scanner",
"description": "Filter vulnerabilities by scanner",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Returns the elements in the list that come after the specified cursor.",
...@@ -29412,6 +29448,24 @@ ...@@ -29412,6 +29448,24 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "scanner",
"description": "Filter vulnerabilities by scanner",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Returns the elements in the list that come after the specified cursor.",
...@@ -27,6 +27,7 @@ module Security ...@@ -27,6 +27,7 @@ module Security
filter_by_report_types filter_by_report_types
filter_by_severities filter_by_severities
filter_by_states filter_by_states
filter_by_scanners
vulnerabilities vulnerabilities
end end
...@@ -58,5 +59,11 @@ module Security ...@@ -58,5 +59,11 @@ module Security
@vulnerabilities = vulnerabilities.with_states(filters[:state]) @vulnerabilities = vulnerabilities.with_states(filters[:state])
end end
end end
def filter_by_scanners
if filters[:scanner].present?
@vulnerabilities = vulnerabilities.with_scanners(filters[:scanner])
end
end
end end
end end
...@@ -22,6 +22,10 @@ module Resolvers ...@@ -22,6 +22,10 @@ module Resolvers
required: false, required: false,
description: 'Filter vulnerabilities by state' description: 'Filter vulnerabilities by state'
argument :scanner, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter vulnerabilities by scanner'
def resolve(**args) def resolve(**args)
return Vulnerability.none unless vulnerable return Vulnerability.none unless vulnerable
......
...@@ -79,7 +79,7 @@ module Vulnerabilities ...@@ -79,7 +79,7 @@ module Vulnerabilities
validates :metadata_version, presence: true validates :metadata_version, presence: true
validates :raw_metadata, presence: true validates :raw_metadata, presence: true
delegate :name, to: :scanner, prefix: true, allow_nil: true delegate :name, :external_id, to: :scanner, prefix: true, allow_nil: true
scope :report_type, -> (type) { where(report_type: report_types[type]) } scope :report_type, -> (type) { where(report_type: report_types[type]) }
scope :ordered, -> { order(severity: :desc, confidence: :desc, id: :asc) } scope :ordered, -> { order(severity: :desc, confidence: :desc, id: :asc) }
......
...@@ -65,6 +65,7 @@ class Vulnerability < ApplicationRecord ...@@ -65,6 +65,7 @@ class Vulnerability < ApplicationRecord
scope :with_report_types, -> (report_types) { where(report_type: report_types) } scope :with_report_types, -> (report_types) { where(report_type: report_types) }
scope :with_severities, -> (severities) { where(severity: severities) } scope :with_severities, -> (severities) { where(severity: severities) }
scope :with_states, -> (states) { where(state: states) } scope :with_states, -> (states) { where(state: states) }
scope :with_scanners, -> (scanners) { joins(findings: :scanner).merge(Vulnerabilities::Scanner.with_external_id(scanners)) }
scope :counts_by_severity, -> { group(:severity).count } scope :counts_by_severity, -> { group(:severity).count }
class << self class << self
...@@ -110,7 +111,7 @@ class Vulnerability < ApplicationRecord ...@@ -110,7 +111,7 @@ class Vulnerability < ApplicationRecord
super || finding&.dismissal_feedback&.author_id super || finding&.dismissal_feedback&.author_id
end end
delegate :scanner_name, :metadata, :message, :cve, delegate :scanner_name, :scanner_external_id, :metadata, :message, :cve,
to: :finding, prefix: true, allow_nil: true to: :finding, prefix: true, allow_nil: true
delegate :default_branch, :name, to: :project, prefix: true, allow_nil: true delegate :default_branch, :name, to: :project, prefix: true, allow_nil: true
......
---
title: Add scanner filter in Vulnerability GraphQL API
merge_request: 34824
author:
type: added
...@@ -6,15 +6,15 @@ RSpec.describe Security::VulnerabilitiesFinder do ...@@ -6,15 +6,15 @@ RSpec.describe Security::VulnerabilitiesFinder do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:vulnerability1) do let_it_be(:vulnerability1) do
create(:vulnerability, severity: :low, report_type: :sast, state: :detected, project: project) create(:vulnerability, :with_findings, severity: :low, report_type: :sast, state: :detected, project: project)
end end
let_it_be(:vulnerability2) do let_it_be(:vulnerability2) do
create(:vulnerability, severity: :medium, report_type: :dast, state: :dismissed, project: project) create(:vulnerability, :with_findings, severity: :medium, report_type: :dast, state: :dismissed, project: project)
end end
let_it_be(:vulnerability3) do let_it_be(:vulnerability3) do
create(:vulnerability, severity: :high, report_type: :dependency_scanning, state: :confirmed, project: project) create(:vulnerability, :with_findings, severity: :high, report_type: :dependency_scanning, state: :confirmed, project: project)
end end
let(:filters) { {} } let(:filters) { {} }
...@@ -58,6 +58,14 @@ RSpec.describe Security::VulnerabilitiesFinder do ...@@ -58,6 +58,14 @@ RSpec.describe Security::VulnerabilitiesFinder do
end end
end end
context 'when filtered by scanner' do
let(:filters) { { scanner: [vulnerability1.finding_scanner_external_id, vulnerability3.finding_scanner_external_id] } }
it 'only returns vulnerabilities matching the given scanners' do
is_expected.to contain_exactly(vulnerability1, vulnerability3)
end
end
context 'when filtered by project' do context 'when filtered by project' do
let(:group) { create(:group) } let(:group) { create(:group) }
let(:another_project) { create(:project, namespace: group) } let(:another_project) { create(:project, namespace: group) }
......
...@@ -12,15 +12,15 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do ...@@ -12,15 +12,15 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) } let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let_it_be(:low_vulnerability) do let_it_be(:low_vulnerability) do
create(:vulnerability, :detected, :low, :dast, project: project) create(:vulnerability, :with_findings, :detected, :low, :dast, project: project)
end end
let_it_be(:critical_vulnerability) do let_it_be(:critical_vulnerability) do
create(:vulnerability, :detected, :critical, :sast, project: project) create(:vulnerability, :with_findings, :detected, :critical, :sast, project: project)
end end
let_it_be(:high_vulnerability) do let_it_be(:high_vulnerability) do
create(:vulnerability, :dismissed, :high, :container_scanning, project: project) create(:vulnerability, :with_findings, :dismissed, :high, :container_scanning, project: project)
end end
let(:current_user) { user } let(:current_user) { user }
...@@ -49,6 +49,14 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do ...@@ -49,6 +49,14 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do
end end
end end
context 'when given scanner' do
let(:filters) { { scanner: [high_vulnerability.finding_scanner_external_id] } }
it 'only returns vulnerabilities of the given scanner' do
is_expected.to contain_exactly(high_vulnerability)
end
end
context 'when given report types' do context 'when given report types' do
let(:filters) { { report_type: %i[dast sast] } } let(:filters) { { report_type: %i[dast sast] } }
......
...@@ -140,6 +140,19 @@ RSpec.describe Vulnerability do ...@@ -140,6 +140,19 @@ RSpec.describe Vulnerability do
end end
end end
describe '.with_scanners' do
let!(:detected_vulnerability) { create(:vulnerability, :detected, :with_findings) }
let!(:dismissed_vulnerability) { create(:vulnerability, :dismissed, :with_findings) }
let!(:confirmed_vulnerability) { create(:vulnerability, :confirmed, :with_findings) }
let(:scanners) { [detected_vulnerability.finding_scanner_external_id, confirmed_vulnerability.finding_scanner_external_id] }
subject { described_class.with_scanners(scanners) }
it 'returns vulnerabilities matching the given scanners' do
is_expected.to contain_exactly(detected_vulnerability, confirmed_vulnerability)
end
end
describe '.ordered' do describe '.ordered' do
subject { described_class.ordered } subject { described_class.ordered }
......
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