Commit dbffe56e authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Robert Speicher

Add mutation to Dismiss Vulnerability GraphQL API

Add ability to dismiss vulnerability through GraphQL API.
parent d44eebc1
...@@ -1832,6 +1832,46 @@ type DiscussionEdge { ...@@ -1832,6 +1832,46 @@ type DiscussionEdge {
node: Discussion node: Discussion
} }
"""
Autogenerated input type of DismissVulnerability
"""
input DismissVulnerabilityInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reason why vulnerability should be dismissed
"""
comment: String
"""
ID of the vulnerability to be dismissed
"""
id: ID!
}
"""
Autogenerated return type of DismissVulnerability
"""
type DismissVulnerabilityPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The vulnerability after dismissal
"""
vulnerability: Vulnerability
}
interface Entry { interface Entry {
""" """
Flat path of the entry Flat path of the entry
...@@ -5413,6 +5453,7 @@ type Mutation { ...@@ -5413,6 +5453,7 @@ type Mutation {
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
dismissVulnerability(input: DismissVulnerabilityInput!): DismissVulnerabilityPayload
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
...@@ -9534,6 +9575,11 @@ type Vulnerability { ...@@ -9534,6 +9575,11 @@ type Vulnerability {
""" """
title: String title: String
"""
Permissions for the current user on the resource
"""
userPermissions: VulnerabilityPermissions!
""" """
URL to the vulnerability's details page URL to the vulnerability's details page
""" """
...@@ -9575,6 +9621,51 @@ type VulnerabilityEdge { ...@@ -9575,6 +9621,51 @@ type VulnerabilityEdge {
node: Vulnerability node: Vulnerability
} }
"""
Check permissions for the current user on a vulnerability
"""
type VulnerabilityPermissions {
"""
Indicates the user can perform `admin_vulnerability` on this resource
"""
adminVulnerability: Boolean!
"""
Indicates the user can perform `admin_vulnerability_issue_link` on this resource
"""
adminVulnerabilityIssueLink: Boolean!
"""
Indicates the user can perform `create_vulnerability` on this resource
"""
createVulnerability: Boolean!
"""
Indicates the user can perform `create_vulnerability_export` on this resource
"""
createVulnerabilityExport: Boolean!
"""
Indicates the user can perform `create_vulnerability_feedback` on this resource
"""
createVulnerabilityFeedback: Boolean!
"""
Indicates the user can perform `destroy_vulnerability_feedback` on this resource
"""
destroyVulnerabilityFeedback: Boolean!
"""
Indicates the user can perform `read_vulnerability_feedback` on this resource
"""
readVulnerabilityFeedback: Boolean!
"""
Indicates the user can perform `update_vulnerability_feedback` on this resource
"""
updateVulnerabilityFeedback: Boolean!
}
""" """
The type of the security scan that found the vulnerability. The type of the security scan that found the vulnerability.
""" """
......
...@@ -317,6 +317,16 @@ Autogenerated return type of DestroySnippet ...@@ -317,6 +317,16 @@ Autogenerated return type of DestroySnippet
| `id` | ID! | ID of this discussion | | `id` | ID! | ID of this discussion |
| `replyId` | ID! | ID used to reply to this discussion | | `replyId` | ID! | ID used to reply to this discussion |
## DismissVulnerabilityPayload
Autogenerated return type of DismissVulnerability
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `vulnerability` | Vulnerability | The vulnerability after dismissal |
## Environment ## Environment
Describes where code is deployed for a project Describes where code is deployed for a project
...@@ -1495,8 +1505,24 @@ Represents a vulnerability. ...@@ -1495,8 +1505,24 @@ Represents a vulnerability.
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) | | `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, DISMISSED, RESOLVED, CONFIRMED) |
| `title` | String | Title of the vulnerability | | `title` | String | Title of the vulnerability |
| `userPermissions` | VulnerabilityPermissions! | Permissions for the current user on the resource |
| `vulnerabilityPath` | String | URL to the vulnerability's details page | | `vulnerabilityPath` | String | URL to the vulnerability's details page |
## VulnerabilityPermissions
Check permissions for the current user on a vulnerability
| Name | Type | Description |
| --- | ---- | ---------- |
| `adminVulnerability` | Boolean! | Indicates the user can perform `admin_vulnerability` on this resource |
| `adminVulnerabilityIssueLink` | Boolean! | Indicates the user can perform `admin_vulnerability_issue_link` on this resource |
| `createVulnerability` | Boolean! | Indicates the user can perform `create_vulnerability` on this resource |
| `createVulnerabilityExport` | Boolean! | Indicates the user can perform `create_vulnerability_export` on this resource |
| `createVulnerabilityFeedback` | Boolean! | Indicates the user can perform `create_vulnerability_feedback` on this resource |
| `destroyVulnerabilityFeedback` | Boolean! | Indicates the user can perform `destroy_vulnerability_feedback` on this resource |
| `readVulnerabilityFeedback` | Boolean! | Indicates the user can perform `read_vulnerability_feedback` on this resource |
| `updateVulnerabilityFeedback` | Boolean! | Indicates the user can perform `update_vulnerability_feedback` on this resource |
## VulnerabilitySeveritiesCount ## VulnerabilitySeveritiesCount
Represents vulnerability counts by severity Represents vulnerability counts by severity
......
...@@ -16,6 +16,7 @@ module EE ...@@ -16,6 +16,7 @@ module EE
mount_mutation ::Mutations::Epics::AddIssue mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::Requirements::Create mount_mutation ::Mutations::Requirements::Create
mount_mutation ::Mutations::Requirements::Update mount_mutation ::Mutations::Requirements::Update
mount_mutation ::Mutations::Vulnerabilities::Dismiss
end end
end end
end end
......
# frozen_string_literal: true
module Mutations
module Vulnerabilities
class Dismiss < BaseMutation
graphql_name 'DismissVulnerability'
authorize :admin_vulnerability
field :vulnerability, Types::VulnerabilityType,
null: true,
description: 'The vulnerability after dismissal'
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'ID of the vulnerability to be dismissed'
argument :comment,
GraphQL::STRING_TYPE,
required: false,
description: 'Reason why vulnerability should be dismissed'
def resolve(id:, comment: nil)
vulnerability = authorized_find!(id: id)
result = dismiss_vulnerability(vulnerability, comment)
{
vulnerability: result,
errors: result.errors.full_messages || []
}
end
private
def dismiss_vulnerability(vulnerability, comment)
::Vulnerabilities::DismissService.new(current_user, vulnerability, comment).execute
end
def find_object(id:)
GitlabSchema.object_from_id(id)
end
end
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class Vulnerability < BasePermissionType
graphql_name 'VulnerabilityPermissions'
description 'Check permissions for the current user on a vulnerability'
abilities :read_vulnerability_feedback, :create_vulnerability_feedback, :destroy_vulnerability_feedback,
:update_vulnerability_feedback, :create_vulnerability, :create_vulnerability_export,
:admin_vulnerability, :admin_vulnerability_issue_link
end
end
end
...@@ -7,6 +7,8 @@ module Types ...@@ -7,6 +7,8 @@ module Types
authorize :read_vulnerability authorize :read_vulnerability
expose_permissions Types::PermissionTypes::Vulnerability
field :id, GraphQL::ID_TYPE, null: false, field :id, GraphQL::ID_TYPE, null: false,
description: 'GraphQL ID of the vulnerability' description: 'GraphQL ID of the vulnerability'
......
...@@ -6,6 +6,11 @@ module Vulnerabilities ...@@ -6,6 +6,11 @@ module Vulnerabilities
FindingsDismissResult = Struct.new(:ok?, :finding, :message) FindingsDismissResult = Struct.new(:ok?, :finding, :message)
def initialize(current_user, vulnerability, comment = nil)
super(current_user, vulnerability)
@comment = comment
end
def execute def execute
raise Gitlab::Access::AccessDeniedError unless authorized? raise Gitlab::Access::AccessDeniedError unless authorized?
...@@ -33,7 +38,8 @@ module Vulnerabilities ...@@ -33,7 +38,8 @@ module Vulnerabilities
{ {
category: finding.report_type, category: finding.report_type,
feedback_type: 'dismissal', feedback_type: 'dismissal',
project_fingerprint: finding.project_fingerprint project_fingerprint: finding.project_fingerprint,
comment: @comment
} }
end end
......
---
title: Add mutation to Dismiss Vulnerability GraphQL API
merge_request: 29150
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Vulnerabilities::Dismiss do
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let_it_be(:vulnerability) { create(:vulnerability, :with_findings) }
let_it_be(:user) { create(:user) }
let(:comment) { 'Dismissal Feedbacl' }
let(:mutated_vulnerability) { subject[:vulnerability] }
subject { mutation.resolve(id: GitlabSchema.id_from_object(vulnerability).to_s, comment: comment) }
context 'when the user can dismiss the vulnerability' do
before do
stub_licensed_features(security_dashboard: true)
end
context 'when user doe not have access to the project' do
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user has access to the project' do
before do
vulnerability.project.add_developer(user)
end
it 'returns the dismissed vulnerability' do
expect(mutated_vulnerability).to eq(vulnerability)
expect(mutated_vulnerability).to be_dismissed
expect(subject[:errors]).to be_empty
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Types::PermissionTypes::Vulnerability do
it do
expected_permissions = %i[read_vulnerability_feedback create_vulnerability_feedback destroy_vulnerability_feedback
update_vulnerability_feedback create_vulnerability create_vulnerability_export
admin_vulnerability admin_vulnerability_issue_link]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
...@@ -8,7 +8,7 @@ describe GitlabSchema.types['Vulnerability'] do ...@@ -8,7 +8,7 @@ describe GitlabSchema.types['Vulnerability'] do
let_it_be(:vulnerability) { create(:vulnerability, project: project) } let_it_be(:vulnerability) { create(:vulnerability, project: project) }
let(:fields) do let(:fields) do
%i[id title description state severity report_type vulnerability_path location] %i[userPermissions id title description state severity report_type vulnerability_path location]
end end
before do before do
......
...@@ -31,6 +31,25 @@ describe Vulnerabilities::DismissService do ...@@ -31,6 +31,25 @@ describe Vulnerabilities::DismissService do
end end
end end
context 'when comment is added' do
let(:comment) { 'Dismissal Comment' }
let(:service) { described_class.new(user, vulnerability, comment) }
it 'dismisses a vulnerability and its associated findings with comment', :aggregate_failures do
Timecop.freeze do
dismiss_vulnerability
aggregate_failures do
expect(vulnerability.reload).to(
have_attributes(state: 'dismissed', dismissed_by: user, dismissed_at: be_like_time(Time.current)))
expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
expect(vulnerability.findings.map(&:dismissal_feedback)).to(
all(have_attributes(comment: comment, comment_author: user, comment_timestamp: be_like_time(Time.current))))
end
end
end
end
it 'creates note' do it 'creates note' do
expect(SystemNoteService).to receive(:change_vulnerability_state).with(vulnerability, user) expect(SystemNoteService).to receive(:change_vulnerability_state).with(vulnerability, user)
......
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