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 {
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 {
"""
Flat path of the entry
......@@ -5413,6 +5453,7 @@ type Mutation {
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
dismissVulnerability(input: DismissVulnerabilityInput!): DismissVulnerabilityPayload
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
......@@ -9534,6 +9575,11 @@ type Vulnerability {
"""
title: String
"""
Permissions for the current user on the resource
"""
userPermissions: VulnerabilityPermissions!
"""
URL to the vulnerability's details page
"""
......@@ -9575,6 +9621,51 @@ type VulnerabilityEdge {
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.
"""
......
......@@ -5393,6 +5393,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DismissVulnerabilityInput",
"description": "Autogenerated input type of DismissVulnerability",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "ID of the vulnerability to be dismissed",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "comment",
"description": "Reason why vulnerability should be dismissed",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DismissVulnerabilityPayload",
"description": "Autogenerated return type of DismissVulnerability",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerability",
"description": "The vulnerability after dismissal",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Vulnerability",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "Entry",
......@@ -15821,6 +15933,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dismissVulnerability",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DismissVulnerabilityInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DismissVulnerabilityPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "epicAddIssue",
"description": null,
......@@ -28762,6 +28901,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userPermissions",
"description": "Permissions for the current user on the resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilityPermissions",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityPath",
"description": "URL to the vulnerability's details page",
......@@ -28896,6 +29053,163 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityPermissions",
"description": "Check permissions for the current user on a vulnerability",
"fields": [
{
"name": "adminVulnerability",
"description": "Indicates the user can perform `admin_vulnerability` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "adminVulnerabilityIssueLink",
"description": "Indicates the user can perform `admin_vulnerability_issue_link` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createVulnerability",
"description": "Indicates the user can perform `create_vulnerability` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createVulnerabilityExport",
"description": "Indicates the user can perform `create_vulnerability_export` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createVulnerabilityFeedback",
"description": "Indicates the user can perform `create_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyVulnerabilityFeedback",
"description": "Indicates the user can perform `destroy_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "readVulnerabilityFeedback",
"description": "Indicates the user can perform `read_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateVulnerabilityFeedback",
"description": "Indicates the user can perform `update_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "VulnerabilityReportType",
......
......@@ -317,6 +317,16 @@ Autogenerated return type of DestroySnippet
| `id` | ID! | ID of 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
Describes where code is deployed for a project
......@@ -1495,8 +1505,24 @@ Represents a vulnerability.
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
| `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 |
## 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
Represents vulnerability counts by severity
......
......@@ -16,6 +16,7 @@ module EE
mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::Requirements::Create
mount_mutation ::Mutations::Requirements::Update
mount_mutation ::Mutations::Vulnerabilities::Dismiss
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
authorize :read_vulnerability
expose_permissions Types::PermissionTypes::Vulnerability
field :id, GraphQL::ID_TYPE, null: false,
description: 'GraphQL ID of the vulnerability'
......
......@@ -6,6 +6,11 @@ module Vulnerabilities
FindingsDismissResult = Struct.new(:ok?, :finding, :message)
def initialize(current_user, vulnerability, comment = nil)
super(current_user, vulnerability)
@comment = comment
end
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
......@@ -33,7 +38,8 @@ module Vulnerabilities
{
category: finding.report_type,
feedback_type: 'dismissal',
project_fingerprint: finding.project_fingerprint
project_fingerprint: finding.project_fingerprint,
comment: @comment
}
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
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
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
before do
......
......@@ -31,6 +31,25 @@ describe Vulnerabilities::DismissService do
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
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