Commit 0b523a7a authored by Mehmet Emin INAC's avatar Mehmet Emin INAC

Add `dismissal_reason` argument to the `VulnerabilityDismiss` mutation

This argument will provide better structured dismissal reason tracking.
parent c0273365
...@@ -7742,10 +7742,15 @@ input DismissVulnerabilityInput { ...@@ -7742,10 +7742,15 @@ input DismissVulnerabilityInput {
clientMutationId: String clientMutationId: String
""" """
Reason why vulnerability should be dismissed Comment why vulnerability should be dismissed
""" """
comment: String comment: String
"""
Reason why vulnerability should be dismissed
"""
dismissalReason: VulnerabilityDismissalReason
""" """
ID of the vulnerability to be dismissed ID of the vulnerability to be dismissed
""" """
...@@ -25997,10 +26002,15 @@ input VulnerabilityDismissInput { ...@@ -25997,10 +26002,15 @@ input VulnerabilityDismissInput {
clientMutationId: String clientMutationId: String
""" """
Reason why vulnerability should be dismissed Comment why vulnerability should be dismissed
""" """
comment: String comment: String
"""
Reason why vulnerability should be dismissed
"""
dismissalReason: VulnerabilityDismissalReason
""" """
ID of the vulnerability to be dismissed ID of the vulnerability to be dismissed
""" """
...@@ -26027,6 +26037,17 @@ type VulnerabilityDismissPayload { ...@@ -26027,6 +26037,17 @@ type VulnerabilityDismissPayload {
vulnerability: Vulnerability vulnerability: Vulnerability
} }
"""
The dismissal reason of the Vulnerability
"""
enum VulnerabilityDismissalReason {
ACCEPTABLE_RISK
FALSE_POSITIVE
MITIGATING_CONTROL
NOT_APPLICABLE
USED_IN_TESTS
}
""" """
An edge in a connection. An edge in a connection.
""" """
......
...@@ -21435,7 +21435,7 @@ ...@@ -21435,7 +21435,7 @@
}, },
{ {
"name": "comment", "name": "comment",
"description": "Reason why vulnerability should be dismissed", "description": "Comment why vulnerability should be dismissed",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -21443,6 +21443,16 @@ ...@@ -21443,6 +21443,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "dismissalReason",
"description": "Reason why vulnerability should be dismissed",
"type": {
"kind": "ENUM",
"name": "VulnerabilityDismissalReason",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "clientMutationId", "name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.", "description": "A unique identifier for the client performing the mutation.",
...@@ -75380,7 +75390,7 @@ ...@@ -75380,7 +75390,7 @@
}, },
{ {
"name": "comment", "name": "comment",
"description": "Reason why vulnerability should be dismissed", "description": "Comment why vulnerability should be dismissed",
"type": { "type": {
"kind": "SCALAR", "kind": "SCALAR",
"name": "String", "name": "String",
...@@ -75388,6 +75398,16 @@ ...@@ -75388,6 +75398,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "dismissalReason",
"description": "Reason why vulnerability should be dismissed",
"type": {
"kind": "ENUM",
"name": "VulnerabilityDismissalReason",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "clientMutationId", "name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.", "description": "A unique identifier for the client performing the mutation.",
...@@ -75470,6 +75490,47 @@ ...@@ -75470,6 +75490,47 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "ENUM",
"name": "VulnerabilityDismissalReason",
"description": "The dismissal reason of the Vulnerability",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "ACCEPTABLE_RISK",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "FALSE_POSITIVE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "MITIGATING_CONTROL",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "USED_IN_TESTS",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "NOT_APPLICABLE",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "VulnerabilityEdge", "name": "VulnerabilityEdge",
...@@ -4935,6 +4935,18 @@ Possible states of a user. ...@@ -4935,6 +4935,18 @@ Possible states of a user.
| `private` | | | `private` | |
| `public` | | | `public` | |
### VulnerabilityDismissalReason
The dismissal reason of the Vulnerability.
| Value | Description |
| ----- | ----------- |
| `ACCEPTABLE_RISK` | |
| `FALSE_POSITIVE` | |
| `MITIGATING_CONTROL` | |
| `NOT_APPLICABLE` | |
| `USED_IN_TESTS` | |
### VulnerabilityExternalIssueLinkExternalTracker ### VulnerabilityExternalIssueLinkExternalTracker
The external tracker of the external issue link related to a vulnerability. The external tracker of the external issue link related to a vulnerability.
......
...@@ -19,11 +19,16 @@ module Mutations ...@@ -19,11 +19,16 @@ module Mutations
argument :comment, argument :comment,
GraphQL::STRING_TYPE, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Comment why vulnerability should be dismissed'
argument :dismissal_reason,
Types::Vulnerabilities::DismissalReasonEnum,
required: false,
description: 'Reason why vulnerability should be dismissed' description: 'Reason why vulnerability should be dismissed'
def resolve(id:, comment: nil) def resolve(id:, comment: nil, dismissal_reason: nil)
vulnerability = authorized_find!(id: id) vulnerability = authorized_find!(id: id)
result = dismiss_vulnerability(vulnerability, comment) result = dismiss_vulnerability(vulnerability, comment, dismissal_reason)
{ {
vulnerability: result, vulnerability: result,
...@@ -33,8 +38,8 @@ module Mutations ...@@ -33,8 +38,8 @@ module Mutations
private private
def dismiss_vulnerability(vulnerability, comment) def dismiss_vulnerability(vulnerability, comment, dismissal_reason)
::Vulnerabilities::DismissService.new(current_user, vulnerability, comment).execute ::Vulnerabilities::DismissService.new(current_user, vulnerability, comment, dismissal_reason).execute
end end
def find_object(id:) def find_object(id:)
......
# frozen_string_literal: true
module Types
module Vulnerabilities
class DismissalReasonEnum < BaseEnum
graphql_name 'VulnerabilityDismissalReason'
description 'The dismissal reason of the Vulnerability'
::Vulnerabilities::Feedback.dismissal_reasons.keys.each do |dismissal_reason|
value dismissal_reason.to_s.upcase, value: dismissal_reason.to_s
end
end
end
end
...@@ -6,9 +6,10 @@ module Vulnerabilities ...@@ -6,9 +6,10 @@ module Vulnerabilities
class DismissService < BaseService class DismissService < BaseService
FindingsDismissResult = Struct.new(:ok?, :finding, :message) FindingsDismissResult = Struct.new(:ok?, :finding, :message)
def initialize(current_user, vulnerability, comment = nil, dismiss_findings: true) def initialize(current_user, vulnerability, comment = nil, dismissal_reason = nil, dismiss_findings: true)
super(current_user, vulnerability) super(current_user, vulnerability)
@comment = comment @comment = comment
@dismissal_reason = dismissal_reason
@dismiss_findings = dismiss_findings @dismiss_findings = dismiss_findings
end end
...@@ -45,6 +46,7 @@ module Vulnerabilities ...@@ -45,6 +46,7 @@ module Vulnerabilities
feedback_type: 'dismissal', feedback_type: 'dismissal',
project_fingerprint: finding.project_fingerprint, project_fingerprint: finding.project_fingerprint,
comment: @comment, comment: @comment,
dismissal_reason: @dismissal_reason,
pipeline: @project.latest_pipeline_with_security_reports(only_successful: true), pipeline: @project.latest_pipeline_with_security_reports(only_successful: true),
finding_uuid: finding.uuid_v5, finding_uuid: finding.uuid_v5,
dismiss_vulnerability: false dismiss_vulnerability: false
......
...@@ -11,17 +11,17 @@ RSpec.describe Mutations::Vulnerabilities::Dismiss do ...@@ -11,17 +11,17 @@ RSpec.describe Mutations::Vulnerabilities::Dismiss do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:vulnerability_id) { GitlabSchema.id_from_object(vulnerability).to_s } let_it_be(:vulnerability_id) { GitlabSchema.id_from_object(vulnerability).to_s }
let(:comment) { 'Dismissal Feedbacl' } let(:comment) { 'Dismissal Feedback' }
let(:mutated_vulnerability) { subject[:vulnerability] } let(:mutated_vulnerability) { subject[:vulnerability] }
subject { mutation.resolve(id: vulnerability_id, comment: comment) } subject { mutation.resolve(id: vulnerability_id, comment: comment, dismissal_reason: 'used_in_tests') }
context 'when the user can dismiss the vulnerability' do context 'when the user can dismiss the vulnerability' do
before do before do
stub_licensed_features(security_dashboard: true) stub_licensed_features(security_dashboard: true)
end end
context 'when user doe not have access to the project' do context 'when user does not have access to the project' do
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end end
......
...@@ -72,6 +72,19 @@ RSpec.describe Vulnerabilities::DismissService do ...@@ -72,6 +72,19 @@ RSpec.describe Vulnerabilities::DismissService do
end end
end end
context 'when the dismissal_reason is added' do
let(:dismissal_reason) { 'used_in_tests' }
let(:service) { described_class.new(user, vulnerability, nil, dismissal_reason) }
it 'dismisses a vulnerability and its associated findings with comment', :aggregate_failures do
dismiss_vulnerability
expect(vulnerability.reload).to have_attributes(state: 'dismissed', dismissed_by: user)
expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
expect(vulnerability.findings.map(&:dismissal_feedback)).to all(have_attributes(dismissal_reason: dismissal_reason))
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