Skip to content
Snippets Groups Projects
Commit 7f06b149 authored by Gregory Havenga's avatar Gregory Havenga :two: Committed by Sashi Kumar Kumaresan
Browse files

Implement Resolve This Vulnerability service classes

No reason to create a distinct create service for the MR, can merge it
into the existing create from remediation service to save on duplicate
code and work.

Changelog: added
EE: true
parent 62b66abd
No related branches found
No related tags found
1 merge request!135826Implement Resolve This Vulnerability graphql Mutation
Showing
with 182 additions and 15 deletions
......@@ -1239,6 +1239,7 @@ Input type: `AiActionInput`
| <a id="mutationaiactiongeneratecommitmessage"></a>`generateCommitMessage` | [`AiGenerateCommitMessageInput`](#aigeneratecommitmessageinput) | Input for generate_commit_message AI action. |
| <a id="mutationaiactiongeneratedescription"></a>`generateDescription` | [`AiGenerateDescriptionInput`](#aigeneratedescriptioninput) | Input for generate_description AI action. |
| <a id="mutationaiactiongeneratetestfile"></a>`generateTestFile` | [`GenerateTestFileInput`](#generatetestfileinput) | Input for generate_test_file AI action. |
| <a id="mutationaiactionresolvevulnerability"></a>`resolveVulnerability` | [`AiResolveVulnerabilityInput`](#airesolvevulnerabilityinput) | Input for resolve_vulnerability AI action. |
| <a id="mutationaiactionsummarizecomments"></a>`summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. |
| <a id="mutationaiactionsummarizereview"></a>`summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. |
| <a id="mutationaiactiontanukibot"></a>`tanukiBot` | [`AiTanukiBotInput`](#aitanukibotinput) | Input for tanuki_bot AI action. |
......@@ -32267,6 +32268,14 @@ see the associated mutation type above.
| <a id="aigeneratedescriptioninputdescriptiontemplatename"></a>`descriptionTemplateName` | [`String`](#string) | Name of the description template to use to generate message off of. |
| <a id="aigeneratedescriptioninputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
 
### `AiResolveVulnerabilityInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="airesolvevulnerabilityinputresourceid"></a>`resourceId` | [`AiModelID!`](#aimodelid) | Global ID of the resource to mutate. |
### `AiSummarizeCommentsInput`
 
#### Arguments
# frozen_string_literal: true
module Types
module Ai
class ResolveVulnerabilityInputType < BaseMethodInputType
graphql_name 'AiResolveVulnerabilityInput'
end
end
end
......@@ -16,5 +16,13 @@ class VulnerabilityPolicy < BasePolicy
@subject&.finding.present?
end
condition(:can_resolve_vulnerability?, scope: :subject) do
::Feature.enabled?(:ai_global_switch, type: :ops) &&
::Feature.enabled?(:resolve_vulnerability_ai, @subject&.project) &&
::Gitlab::Llm::StageCheck.available?(@subject&.project, :resolve_vulnerability) &&
@subject&.finding.present?
end
rule { can_explain_vulnerability? & can?(:read_security_resource) }.enable(:explain_vulnerability)
rule { can_resolve_vulnerability? & can?(:read_security_resource) }.enable(:resolve_vulnerability)
end
......@@ -7,6 +7,7 @@ class ExecuteMethodService < BaseService
METHODS = {
analyze_ci_job_failure: Llm::AnalyzeCiJobFailureService,
explain_vulnerability: ::Llm::ExplainVulnerabilityService,
resolve_vulnerability: ::Llm::ResolveVulnerabilityService,
summarize_comments: Llm::GenerateSummaryService,
summarize_review: Llm::MergeRequests::SummarizeReviewService,
explain_code: Llm::ExplainCodeService,
......
# frozen_string_literal: true
module Llm
class ResolveVulnerabilityService < BaseService
def valid?
super && Ability.allowed?(user, :resolve_vulnerability, resource)
end
private
def ai_action
:resolve_vulnerability
end
def perform
schedule_completion_worker
end
end
end
---
name: resolve_vulnerability_ai
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135826
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430963
milestone: '16.6'
type: development
group: group::threat insights
default_enabled: false
......@@ -12,6 +12,7 @@ class StageCheck
:generate_test_file,
:summarize_diff,
:explain_vulnerability,
:resolve_vulnerability,
:generate_commit_message,
:chat,
:fill_in_merge_request_template,
......
......@@ -53,9 +53,9 @@
let(:options) { {} }
subject(:explain) { described_class.new(prompt_message, prompt_class, options) }
subject(:resolve) { described_class.new(prompt_message, prompt_class, options) }
def execute_explain(message_params = {}, options = {})
def execute_resolve(message_params = {}, options = {})
message = build(:ai_message, :resolve_vulnerability,
{ user: user, resource: vulnerability, request_id: 'uuid' }.merge(message_params))
......@@ -84,7 +84,7 @@ def execute_explain(message_params = {}, options = {})
end
it 'publishes the error to the graphql subscription' do
explain.execute
resolve.execute
expect(GraphqlTriggers).to have_received(:ai_completion_response)
.with(an_object_having_attributes(
......@@ -113,7 +113,7 @@ def execute_explain(message_params = {}, options = {})
end
it 'requests that a MR be created with the extracted patch' do
explain.execute
resolve.execute
expect(merge_request_service).to have_received(:new).with(
project,
......@@ -124,7 +124,7 @@ def execute_explain(message_params = {}, options = {})
end
it 'publishes the created merge request for the fix' do
explain.execute
resolve.execute
expect(GraphqlTriggers).to have_received(:ai_completion_response).with(
an_object_having_attributes(
......@@ -149,12 +149,12 @@ def execute_explain(message_params = {}, options = {})
end
it 'records the error' do
explain.execute
resolve.execute
expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(error)
end
it 'publishes a generic error to the graphql subscription' do
explain.execute
resolve.execute
expect(GraphqlTriggers).to have_received(:ai_completion_response).with(
an_object_having_attributes(
......@@ -178,12 +178,12 @@ def execute_explain(message_params = {}, options = {})
end
it 'records the error' do
explain.execute
resolve.execute
expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(error)
end
it 'publishes a generic error to the graphql subscription' do
explain.execute
resolve.execute
expect(GraphqlTriggers).to have_received(:ai_completion_response).with(
an_object_having_attributes(
......@@ -207,12 +207,12 @@ def execute_explain(message_params = {}, options = {})
it 'makes a fresh request for each user' do
# cache miss
execute_explain
execute_explain({ user: user2 })
execute_resolve
execute_resolve({ user: user2 })
# cache hit
execute_explain
execute_explain({ user: user2 })
execute_resolve
execute_resolve({ user: user2 })
expect(fake_client).to have_received(client_method).exactly(2).times
end
......
......@@ -27,7 +27,7 @@
it_behaves_like 'empty response error'
end
context 'when bo merge request reference is passed' do
context 'when no merge request reference is passed' do
let(:reference) { nil }
let(:ai_response) { { 'merge_request_reference' => reference }.to_json }
......
......@@ -20,6 +20,7 @@
it { is_expected.to be_disallowed(:read_vulnerability) }
it { is_expected.to be_disallowed(:create_note) }
it { is_expected.to be_disallowed(:explain_vulnerability) }
it { is_expected.to be_disallowed(:resolve_vulnerability) }
end
context "when the current user has developer access to the vulnerability's project" do
......@@ -42,12 +43,35 @@
namespace.update!(experiment_features_enabled: true)
end
it { is_expected.to be_allowed(:explain_vulnerability) }
context 'when explain_vulnerability is enabled' do
it { is_expected.to be_allowed(:explain_vulnerability) }
end
context 'when explain_vulnerability is disabled' do
before do
stub_feature_flags(explain_vulnerability: false)
end
it { is_expected.to be_disallowed(:explain_vulnerability) }
end
context 'when resolve_vulnerability_ai is enabled' do
it { is_expected.to be_allowed(:resolve_vulnerability) }
end
context 'when resolve_vulnerability_ai is disabled' do
before do
stub_feature_flags(resolve_vulnerability_ai: false)
end
it { is_expected.to be_disallowed(:resolve_vulnerability) }
end
context 'without finding' do
let(:vulnerability) { build(:vulnerability, project: project) }
it { is_expected.to be_disallowed(:explain_vulnerability) }
it { is_expected.to be_disallowed(:resolve_vulnerability) }
end
end
end
......
......@@ -22,6 +22,8 @@
:explain_code | build_stubbed(:project) | Llm::ExplainCodeService | {}
:explain_vulnerability | build_stubbed(:vulnerability,
:with_findings) | Llm::ExplainVulnerabilityService | { include_source_code: true }
:resolve_vulnerability | build_stubbed(:vulnerability,
:with_findings) | Llm::ResolveVulnerabilityService | {}
:categorize_question | user | Llm::Internal::CategorizeChatQuestionService | {}
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Llm::ResolveVulnerabilityService, :saas, feature_category: :vulnerability_management do
let(:user) { nil }
let_it_be(:no_access) { create(:user) }
let_it_be(:developer) { create(:user) }
let_it_be(:maintainer) { create(:user) }
let_it_be_with_reload(:namespace) { create(:group_with_plan, plan: :ultimate_plan) }
let_it_be(:project) { create(:project, namespace: namespace) }
let_it_be(:vulnerability) { create(:vulnerability, :with_finding, project: project) }
let_it_be(:options) { { include_source_code: true } }
before do
stub_feature_flags(ai_global_switch: true)
end
before_all do
namespace.add_developer(developer)
project.add_maintainer(maintainer)
end
subject { described_class.new(user, vulnerability, options) }
describe '#execute' do
before do
stub_application_setting(check_namespace_plan: true)
stub_feature_flags(resolve_vulnerability_ai: project, ai_global_switch: true)
stub_licensed_features(ai_features: true, security_dashboard: true)
namespace.namespace_settings.update!(
experiment_features_enabled: true,
third_party_ai_features_enabled: true
)
allow(Llm::CompletionWorker).to receive(:perform_async)
end
context 'when the user is permitted to view the vulnerability' do
let(:user) { developer }
let(:resource) { vulnerability }
let(:action_name) { :resolve_vulnerability }
let(:content) { 'Resolve vulnerability' }
it_behaves_like 'schedules completion worker'
context 'when feature flag is disabled' do
before do
stub_feature_flags(resolve_vulnerability_ai: false)
end
it 'returns an error' do
expect(subject.execute).to be_error
expect(Llm::CompletionWorker).not_to have_received(:perform_async)
end
end
end
context 'when the user is not permitted to view the vulnerability' do
let(:user) { no_access }
it 'returns an error' do
expect(subject.execute).to be_error
expect(Llm::CompletionWorker).not_to have_received(:perform_async)
end
end
context 'when experimental features are disabled' do
let(:user) { maintainer }
before do
namespace.update!(experiment_features_enabled: false)
end
it 'returns an error' do
result = subject.execute
expect(result).to be_error
expect(Llm::CompletionWorker).not_to have_received(:perform_async)
end
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment