Commit e587d114 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Dmytro Zaporozhets

Add VulnerabilityScanner to be queried as single GraphQL type

This change extends VulnerabilityScanner with new vendor field and
allows user to query for VulnerabilityScanners used in detected
Vulnerabilities.
parent c21d8b6d
......@@ -5393,6 +5393,31 @@ type Group {
startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection
"""
Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups
"""
vulnerabilityScanners(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): VulnerabilityScannerConnection
"""
Web URL of the group
"""
......@@ -5520,6 +5545,31 @@ type InstanceSecurityDashboard {
"""
last: Int
): ProjectConnection!
"""
Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard
"""
vulnerabilityScanners(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): VulnerabilityScannerConnection
}
"""
......@@ -9771,6 +9821,31 @@ type Project {
state: [VulnerabilityState!]
): VulnerabilityConnection
"""
Vulnerability scanners reported on the project vulnerabilties
"""
vulnerabilityScanners(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): VulnerabilityScannerConnection
"""
Counts for each severity of vulnerability of the project
"""
......@@ -14363,6 +14438,51 @@ type VulnerabilityScanner {
Name of the vulnerability scanner
"""
name: String
"""
Type of the vulnerability report
"""
reportType: VulnerabilityReportType
"""
Vendor of the vulnerability scanner
"""
vendor: String
}
"""
The connection type for VulnerabilityScanner.
"""
type VulnerabilityScannerConnection {
"""
A list of edges.
"""
edges: [VulnerabilityScannerEdge]
"""
A list of nodes.
"""
nodes: [VulnerabilityScanner]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type VulnerabilityScannerEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: VulnerabilityScanner
}
"""
......
......@@ -14808,6 +14808,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityScanners",
"description": "Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityScannerConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "webUrl",
"description": "Web URL of the group",
......@@ -15206,6 +15259,59 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityScanners",
"description": "Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityScannerConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
......@@ -28685,6 +28791,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityScanners",
"description": "Vulnerability scanners reported on the project vulnerabilties",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityScannerConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilitySeveritiesCount",
"description": "Counts for each severity of vulnerability of the project",
......@@ -42279,6 +42438,146 @@
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "reportType",
"description": "Type of the vulnerability report",
"args": [
],
"type": {
"kind": "ENUM",
"name": "VulnerabilityReportType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vendor",
"description": "Vendor of the vulnerability scanner",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityScannerConnection",
"description": "The connection type for VulnerabilityScanner.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilityScannerEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilityScanner",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityScannerEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilityScanner",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
......@@ -2209,6 +2209,8 @@ Represents a vulnerability scanner.
| --- | ---- | ---------- |
| `externalId` | String | External ID of the vulnerability scanner |
| `name` | String | Name of the vulnerability scanner |
| `reportType` | VulnerabilityReportType | Type of the vulnerability report |
| `vendor` | String | Vendor of the vulnerability scanner |
## VulnerabilitySeveritiesCount
......
......@@ -36,6 +36,12 @@ module EE
description: 'Vulnerabilities reported on the projects in the group and its subgroups',
resolver: ::Resolvers::VulnerabilitiesResolver
field :vulnerability_scanners,
::Types::VulnerabilityScannerType.connection_type,
null: true,
description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerabilities_count_by_day_and_severity,
::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type,
null: true,
......
......@@ -18,6 +18,12 @@ module EE
description: 'Vulnerabilities reported on the project',
resolver: ::Resolvers::VulnerabilitiesResolver
field :vulnerability_scanners,
::Types::VulnerabilityScannerType.connection_type,
null: true,
description: 'Vulnerability scanners reported on the project vulnerabilties',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each severity of vulnerability of the project',
resolve: -> (obj, _args, ctx) do
......
# frozen_string_literal: true
module Representation
class VulnerabilityScannerEntry < SimpleDelegator
def initialize(raw_entry, report_type = raw_entry[:report_type])
@report_type = report_type
super(raw_entry)
end
attr_reader :raw_entry
def report_type
::Vulnerabilities::Occurrence::REPORT_TYPES.key(@report_type) || @report_type
end
def ==(other)
self.class === other && id == other.id && report_type == other.report_type
end
def self.declarative_policy_class
'Vulnerabilities::ScannerPolicy'
end
end
end
# frozen_string_literal: true
module Resolvers
module Vulnerabilities
class ScannersResolver < VulnerabilitiesBaseResolver
type Types::VulnerabilityScannerType, null: true
def resolve(**args)
return ::Vulnerabilities::Scanner.none unless vulnerable
vulnerable
.vulnerability_scanners
.with_report_type
.map(&Representation::VulnerabilityScannerEntry.method(:new))
end
end
end
end
......@@ -29,7 +29,7 @@ module Resolvers
def resolve(**args)
return Vulnerability.none unless vulnerable
vulnerabilities(args).with_findings.ordered
vulnerabilities(args).with_findings_and_scanner.ordered
end
private
......
......@@ -11,5 +11,11 @@ module Types
null: false,
authorize: :read_project,
description: 'Projects selected in Instance Security Dashboard'
field :vulnerability_scanners,
::Types::VulnerabilityScannerType.connection_type,
null: true,
description: 'Vulnerability scanners reported on the vulnerabilties from projects selected in Instance Security Dashboard',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerabilityScannerType < BaseObject
graphql_name 'VulnerabilityScanner'
description 'Represents a vulnerability scanner.'
authorize :read_vulnerability_scanner
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the vulnerability scanner'
field :external_id, GraphQL::STRING_TYPE, null: true,
description: 'External ID of the vulnerability scanner'
field :vendor, GraphQL::STRING_TYPE, null: true,
description: 'Vendor of the vulnerability scanner'
field :report_type, VulnerabilityReportTypeEnum, null: true,
description: 'Type of the vulnerability report'
end
# rubocop: enable Graphql/AuthorizeTypes
end
......@@ -44,7 +44,9 @@ module Types
field :scanner, VulnerabilityScannerType, null: true,
description: 'Scanner metadata for the vulnerability.',
resolve: -> (obj, _args, _ctx) { obj.finding&.scanner }
resolve: -> (obj, _args, _ctx) do
Representation::VulnerabilityScannerEntry.new(obj.finding&.scanner, obj.report_type)
end
field :primary_identifier, VulnerabilityIdentifierType, null: true,
description: 'Primary identifier of the vulnerability.',
......
......@@ -335,6 +335,12 @@ module EE
)
end
def vulnerability_scanners
::Vulnerabilities::Scanner.where(
project: ::Project.for_group_and_its_subgroups(self).non_archived.without_deleted
)
end
def max_personal_access_token_lifetime_from_now
if max_personal_access_token_lifetime.present?
max_personal_access_token_lifetime.days.from_now
......
......@@ -32,6 +32,12 @@ class InstanceSecurityDashboard
Vulnerability.for_projects(projects)
end
def vulnerability_scanners
return Vulnerabilities::Scanner.none if projects.empty?
Vulnerabilities::Scanner.for_projects(projects)
end
private
attr_reader :project_ids, :user
......
......@@ -14,5 +14,12 @@ module Vulnerabilities
validates :vendor, length: { maximum: 255, allow_nil: false }
scope :with_external_id, -> (external_ids) { where(external_id: external_ids) }
scope :for_projects, -> (project_ids) { where(project_id: project_ids) }
scope :with_report_type, -> do
joins(:occurrences)
.select('DISTINCT ON ("vulnerability_scanners"."external_id", "vulnerability_occurrences"."report_type") "vulnerability_scanners".*, "vulnerability_occurrences"."report_type" AS "report_type"')
.order('"vulnerability_scanners"."external_id" ASC, "vulnerability_occurrences"."report_type" ASC')
end
end
end
......@@ -257,7 +257,10 @@ module EE
rule { can?(:read_project) & iterations_available }.enable :read_iteration
rule { security_dashboard_enabled & can?(:developer_access) }.enable :read_vulnerability
rule { security_dashboard_enabled & can?(:developer_access) }.policy do
enable :read_vulnerability
enable :read_vulnerability_scanner
end
rule { on_demand_scans_enabled & can?(:developer_access) }.enable :read_on_demand_scans
......@@ -323,6 +326,7 @@ module EE
rule { auditor & security_dashboard_enabled }.policy do
enable :read_vulnerability
enable :read_vulnerability_scanner
end
rule { auditor & ~developer }.policy do
......
# frozen_string_literal: true
module Vulnerabilities
class ScannerPolicy < BasePolicy
delegate { @subject.project }
end
end
---
title: Add vulnerability scanner query to GraphQL API
merge_request: 35109
author:
type: added
......@@ -13,6 +13,7 @@ RSpec.describe GitlabSchema.types['Group'] do
it { expect(described_class).to have_graphql_field(:groupTimelogsEnabled) }
it { expect(described_class).to have_graphql_field(:timelogs, complexity: 5) }
it { expect(described_class).to have_graphql_field(:vulnerabilities) }
it { expect(described_class).to have_graphql_field(:vulnerability_scanners) }
it { expect(described_class).to have_graphql_field(:vulnerabilities_count_by_day_and_severity) }
describe 'timelogs field' do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Representation::VulnerabilityScannerEntry do
let(:project) { create(:project) }
let(:vulnerability_scanner) { create(:vulnerabilities_scanner, project: project) }
describe '.declarative_policy_class' do
subject { described_class.declarative_policy_class }
it { is_expected.to eq('Vulnerabilities::ScannerPolicy') }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Vulnerabilities::ScannersResolver do
include GraphqlHelpers
describe '#resolve' do
subject { resolve(described_class, obj: vulnerable, args: {}, ctx: { current_user: current_user }) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:project_with_no_group) { create(:project) }
let_it_be(:user) { create(:user, security_dashboard_projects: [project_with_no_group]) }
let_it_be(:vulnerability_scanner_1) { create(:vulnerabilities_scanner, project: project) }
let_it_be(:finding_1) { create(:vulnerabilities_occurrence, project: project, scanner: vulnerability_scanner_1) }
let_it_be(:vulnerability_scanner_2) { create(:vulnerabilities_scanner, project: project_with_no_group) }
let_it_be(:finding_2) { create(:vulnerabilities_occurrence, project: project_with_no_group, scanner: vulnerability_scanner_2) }
let(:current_user) { user }
let(:vulnerable) { nil }
context 'when listing scanners for group' do
let(:vulnerable) { group }
it { is_expected.to contain_exactly(Representation::VulnerabilityScannerEntry.new(vulnerability_scanner_1, finding_1.report_type)) }
end
context 'when listing scanners for project' do
let(:vulnerable) { project_with_no_group }
it { is_expected.to contain_exactly(Representation::VulnerabilityScannerEntry.new(vulnerability_scanner_2, finding_2.report_type)) }
end
context 'when listing scanners for instance dashboard' do
let(:vulnerable) { nil }
before do
project_with_no_group.add_developer(current_user)
end
it { is_expected.to contain_exactly(Representation::VulnerabilityScannerEntry.new(vulnerability_scanner_2, finding_2.report_type)) }
end
end
end
......@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['InstanceSecurityDashboard'] do
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let(:fields) do
%i[projects]
%i[projects vulnerability_scanners]
end
before do
......
......@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do
expected_fields = %w[
service_desk_enabled service_desk_address vulnerabilities
service_desk_enabled service_desk_address vulnerabilities vulnerability_scanners
requirement_states_count vulnerability_severities_count packages
compliance_frameworks
]
......
......@@ -3,5 +3,21 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityScanner'] do
it { expect(described_class).to have_graphql_fields(:name, :external_id) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:fields) do
%i[name external_id vendor report_type]
end
before do
stub_licensed_features(security_dashboard: true)
project.add_developer(user)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to require_graphql_authorizations(:read_vulnerability_scanner) }
end
......@@ -316,6 +316,24 @@ RSpec.describe Group do
end
end
describe '#vulnerability_scanners' do
subject { group.vulnerability_scanners }
let(:subgroup) { create(:group, parent: group) }
let(:group_project) { create(:project, namespace: group) }
let(:subgroup_project) { create(:project, namespace: subgroup) }
let(:archived_project) { create(:project, :archived, namespace: group) }
let(:deleted_project) { create(:project, pending_delete: true, namespace: group) }
let!(:group_vulnerability_scanner) { create(:vulnerabilities_scanner, project: group_project) }
let!(:subgroup_vulnerability_scanner) { create(:vulnerabilities_scanner, project: subgroup_project) }
let!(:archived_vulnerability_scanner) { create(:vulnerabilities_scanner, project: archived_project) }
let!(:deleted_vulnerability_scanner) { create(:vulnerabilities_scanner, project: deleted_project) }
it 'returns vulnerability scanners for all non-archived, non-deleted projects in the group and its subgroups' do
is_expected.to contain_exactly(group_vulnerability_scanner, subgroup_vulnerability_scanner)
end
end
describe '#mark_ldap_sync_as_failed' do
it 'sets the state to failed' do
group.start_ldap_sync
......
......@@ -116,6 +116,25 @@ RSpec.describe InstanceSecurityDashboard do
end
end
describe '#vulnerability_scanners' do
let_it_be(:vulnerability_scanner1) { create(:vulnerabilities_scanner, project: project1) }
let_it_be(:vulnerability_scanner2) { create(:vulnerabilities_scanner, project: project2) }
context 'when the user cannot read all resources' do
it 'returns only vulnerability scanners from projects on their dashboard that they can read' do
expect(subject.vulnerability_scanners).to contain_exactly(vulnerability_scanner1)
end
end
context 'when the user can read all resources' do
let(:user) { create(:auditor) }
it "returns vulnerability scanners from all projects on the user's dashboard" do
expect(subject.vulnerability_scanners).to contain_exactly(vulnerability_scanner1, vulnerability_scanner2)
end
end
end
describe '#full_path' do
let(:user) { create(:user) }
......
......@@ -33,6 +33,7 @@ RSpec.describe Project do
it { is_expected.to have_many(:path_locks) }
it { is_expected.to have_many(:vulnerability_feedback) }
it { is_expected.to have_many(:vulnerability_exports) }
it { is_expected.to have_many(:vulnerability_scanners) }
it { is_expected.to have_many(:audit_events).dependent(false) }
it { is_expected.to have_many(:protected_environments) }
it { is_expected.to have_many(:approvers).dependent(:destroy) }
......
......@@ -37,7 +37,7 @@ RSpec.describe ProjectPolicy do
let(:additional_developer_permissions) do
%i[
admin_vulnerability_feedback read_project_security_dashboard read_feature_flag
read_vulnerability create_vulnerability create_vulnerability_export admin_vulnerability
read_vulnerability read_vulnerability_scanner create_vulnerability create_vulnerability_export admin_vulnerability
admin_vulnerability_issue_link read_merge_train
]
end
......@@ -53,7 +53,7 @@ RSpec.describe ProjectPolicy do
read_pipeline read_build read_commit_status read_container_image
read_environment read_deployment read_merge_request read_pages
create_merge_request_in award_emoji
read_project_security_dashboard read_vulnerability
read_project_security_dashboard read_vulnerability read_vulnerability_scanner
read_software_license_policy
read_threat_monitoring read_merge_train
]
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::ScannerPolicy do
describe 'read_vulnerability_scanner' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:vulnerability_scanner) { create(:vulnerabilities_scanner, project: project) }
subject { described_class.new(user, vulnerability_scanner) }
context 'when the security_dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
context "when the current user has developer access to the vulnerability's project" do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:read_vulnerability_scanner) }
end
context "when the current user does not have developer access to the vulnerability's project" do
it { is_expected.to be_disallowed(:read_vulnerability_scanner) }
end
end
context 'when the security_dashboard feature is disabled' do
before do
stub_licensed_features(security_dashboard: false)
project.add_developer(user)
end
it { is_expected.to be_disallowed(:read_vulnerability_scanner) }
end
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