Commit 216f8214 authored by Dmytro Zaporozhets's avatar Dmytro Zaporozhets

Merge branch '210327-add-scanner-vendor-graphql-type' into 'master'

Add VulnerabilityScanner to be queried as single GraphQL type

See merge request gitlab-org/gitlab!35109
parents a8ade726 e587d114
...@@ -5398,6 +5398,31 @@ type Group { ...@@ -5398,6 +5398,31 @@ type Group {
startDate: ISO8601Date! startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection ): 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 Web URL of the group
""" """
...@@ -5525,6 +5550,31 @@ type InstanceSecurityDashboard { ...@@ -5525,6 +5550,31 @@ type InstanceSecurityDashboard {
""" """
last: Int last: Int
): ProjectConnection! ): 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
} }
""" """
...@@ -9776,6 +9826,31 @@ type Project { ...@@ -9776,6 +9826,31 @@ type Project {
state: [VulnerabilityState!] state: [VulnerabilityState!]
): VulnerabilityConnection ): 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 Counts for each severity of vulnerability of the project
""" """
...@@ -14458,6 +14533,51 @@ type VulnerabilityScanner { ...@@ -14458,6 +14533,51 @@ type VulnerabilityScanner {
Name of the vulnerability scanner Name of the vulnerability scanner
""" """
name: String 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
} }
""" """
......
...@@ -2222,6 +2222,8 @@ Represents a vulnerability scanner. ...@@ -2222,6 +2222,8 @@ Represents a vulnerability scanner.
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `externalId` | String | External ID of the vulnerability scanner | | `externalId` | String | External ID of the vulnerability scanner |
| `name` | String | Name 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 ## VulnerabilitySeveritiesCount
......
...@@ -36,6 +36,12 @@ module EE ...@@ -36,6 +36,12 @@ module EE
description: 'Vulnerabilities reported on the projects in the group and its subgroups', description: 'Vulnerabilities reported on the projects in the group and its subgroups',
resolver: ::Resolvers::VulnerabilitiesResolver 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, field :vulnerabilities_count_by_day_and_severity,
::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type, ::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type,
null: true, null: true,
......
...@@ -18,6 +18,12 @@ module EE ...@@ -18,6 +18,12 @@ module EE
description: 'Vulnerabilities reported on the project', description: 'Vulnerabilities reported on the project',
resolver: ::Resolvers::VulnerabilitiesResolver 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, field :vulnerability_severities_count, ::Types::VulnerabilitySeveritiesCountType, null: true,
description: 'Counts for each severity of vulnerability of the project', description: 'Counts for each severity of vulnerability of the project',
resolve: -> (obj, _args, ctx) do 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 ...@@ -29,7 +29,7 @@ module Resolvers
def resolve(**args) def resolve(**args)
return Vulnerability.none unless vulnerable return Vulnerability.none unless vulnerable
vulnerabilities(args).with_findings.ordered vulnerabilities(args).with_findings_and_scanner.ordered
end end
private private
......
...@@ -11,5 +11,11 @@ module Types ...@@ -11,5 +11,11 @@ module Types
null: false, null: false,
authorize: :read_project, authorize: :read_project,
description: 'Projects selected in Instance Security Dashboard' 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
end end
# frozen_string_literal: true # frozen_string_literal: true
module Types module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerabilityScannerType < BaseObject class VulnerabilityScannerType < BaseObject
graphql_name 'VulnerabilityScanner' graphql_name 'VulnerabilityScanner'
description 'Represents a vulnerability scanner.' description 'Represents a vulnerability scanner.'
authorize :read_vulnerability_scanner
field :name, GraphQL::STRING_TYPE, null: true, field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the vulnerability scanner' description: 'Name of the vulnerability scanner'
field :external_id, GraphQL::STRING_TYPE, null: true, field :external_id, GraphQL::STRING_TYPE, null: true,
description: 'External ID of the vulnerability scanner' 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 end
# rubocop: enable Graphql/AuthorizeTypes
end end
...@@ -44,7 +44,9 @@ module Types ...@@ -44,7 +44,9 @@ module Types
field :scanner, VulnerabilityScannerType, null: true, field :scanner, VulnerabilityScannerType, null: true,
description: 'Scanner metadata for the vulnerability.', 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, field :primary_identifier, VulnerabilityIdentifierType, null: true,
description: 'Primary identifier of the vulnerability.', description: 'Primary identifier of the vulnerability.',
......
...@@ -335,6 +335,12 @@ module EE ...@@ -335,6 +335,12 @@ module EE
) )
end 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 def max_personal_access_token_lifetime_from_now
if max_personal_access_token_lifetime.present? if max_personal_access_token_lifetime.present?
max_personal_access_token_lifetime.days.from_now max_personal_access_token_lifetime.days.from_now
......
...@@ -32,6 +32,12 @@ class InstanceSecurityDashboard ...@@ -32,6 +32,12 @@ class InstanceSecurityDashboard
Vulnerability.for_projects(projects) Vulnerability.for_projects(projects)
end end
def vulnerability_scanners
return Vulnerabilities::Scanner.none if projects.empty?
Vulnerabilities::Scanner.for_projects(projects)
end
private private
attr_reader :project_ids, :user attr_reader :project_ids, :user
......
...@@ -14,5 +14,12 @@ module Vulnerabilities ...@@ -14,5 +14,12 @@ module Vulnerabilities
validates :vendor, length: { maximum: 255, allow_nil: false } validates :vendor, length: { maximum: 255, allow_nil: false }
scope :with_external_id, -> (external_ids) { where(external_id: external_ids) } 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
end end
...@@ -257,7 +257,10 @@ module EE ...@@ -257,7 +257,10 @@ module EE
rule { can?(:read_project) & iterations_available }.enable :read_iteration 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 rule { on_demand_scans_enabled & can?(:developer_access) }.enable :read_on_demand_scans
...@@ -323,6 +326,7 @@ module EE ...@@ -323,6 +326,7 @@ module EE
rule { auditor & security_dashboard_enabled }.policy do rule { auditor & security_dashboard_enabled }.policy do
enable :read_vulnerability enable :read_vulnerability
enable :read_vulnerability_scanner
end end
rule { auditor & ~developer }.policy do 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 ...@@ -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(:groupTimelogsEnabled) }
it { expect(described_class).to have_graphql_field(:timelogs, complexity: 5) } 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(: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) } it { expect(described_class).to have_graphql_field(:vulnerabilities_count_by_day_and_severity) }
describe 'timelogs field' do 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 ...@@ -8,7 +8,7 @@ RSpec.describe GitlabSchema.types['InstanceSecurityDashboard'] do
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) } let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let(:fields) do let(:fields) do
%i[projects] %i[projects vulnerability_scanners]
end end
before do before do
......
...@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do it 'includes the ee specific fields' do
expected_fields = %w[ 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 requirement_states_count vulnerability_severities_count packages
compliance_frameworks compliance_frameworks
] ]
......
...@@ -3,5 +3,21 @@ ...@@ -3,5 +3,21 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityScanner'] do 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 end
...@@ -316,6 +316,24 @@ RSpec.describe Group do ...@@ -316,6 +316,24 @@ RSpec.describe Group do
end end
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 describe '#mark_ldap_sync_as_failed' do
it 'sets the state to failed' do it 'sets the state to failed' do
group.start_ldap_sync group.start_ldap_sync
......
...@@ -119,6 +119,25 @@ RSpec.describe InstanceSecurityDashboard do ...@@ -119,6 +119,25 @@ RSpec.describe InstanceSecurityDashboard do
end end
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 describe '#full_path' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -33,6 +33,7 @@ RSpec.describe Project do ...@@ -33,6 +33,7 @@ RSpec.describe Project do
it { is_expected.to have_many(:path_locks) } it { is_expected.to have_many(:path_locks) }
it { is_expected.to have_many(:vulnerability_feedback) } it { is_expected.to have_many(:vulnerability_feedback) }
it { is_expected.to have_many(:vulnerability_exports) } 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(:audit_events).dependent(false) }
it { is_expected.to have_many(:protected_environments) } it { is_expected.to have_many(:protected_environments) }
it { is_expected.to have_many(:approvers).dependent(:destroy) } it { is_expected.to have_many(:approvers).dependent(:destroy) }
......
...@@ -37,7 +37,7 @@ RSpec.describe ProjectPolicy do ...@@ -37,7 +37,7 @@ RSpec.describe ProjectPolicy do
let(:additional_developer_permissions) do let(:additional_developer_permissions) do
%i[ %i[
admin_vulnerability_feedback read_project_security_dashboard read_feature_flag 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 admin_vulnerability_issue_link read_merge_train
] ]
end end
...@@ -53,7 +53,7 @@ RSpec.describe ProjectPolicy do ...@@ -53,7 +53,7 @@ RSpec.describe ProjectPolicy do
read_pipeline read_build read_commit_status read_container_image read_pipeline read_build read_commit_status read_container_image
read_environment read_deployment read_merge_request read_pages read_environment read_deployment read_merge_request read_pages
create_merge_request_in award_emoji 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_software_license_policy
read_threat_monitoring read_merge_train 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