From 07e6c2ebee1c9d02e5bb9c5396b8dc65306f1bb4 Mon Sep 17 00:00:00 2001 From: atiwari71 Date: Thu, 7 Oct 2021 18:43:05 +0530 Subject: [PATCH] Add corpus create mutation and service Add ability to create corpus for a package associated to a project Changelog: added EE: true --- doc/api/graphql/reference/index.md | 21 +++++ ee/app/graphql/ee/types/mutation_type.rb | 1 + .../app_sec/fuzzing/coverage/corpus/create.rb | 54 +++++++++++ .../models/app_sec/fuzzing/coverage/corpus.rb | 14 +++ ee/app/policies/ee/project_policy.rb | 1 + .../coverage/corpuses/create_service.rb | 51 +++++++++++ .../app_sec/fuzzing/coverage/corpuses.rb | 2 +- .../fuzzing/coverage/corpus/create_spec.rb | 52 +++++++++++ .../app_sec/fuzzing/coverage/corpus_spec.rb | 12 +++ .../coverage/corpuses/create_service_spec.rb | 90 +++++++++++++++++++ 10 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 ee/app/graphql/mutations/app_sec/fuzzing/coverage/corpus/create.rb create mode 100644 ee/app/services/app_sec/fuzzing/coverage/corpuses/create_service.rb create mode 100644 ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb create mode 100644 ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index b62d46b4a039ce..ea40b5b1d50e56 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1034,6 +1034,27 @@ Input type: `ConfigureSecretDetectionInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `successPath` | [`String`](#string) | Redirect path to use when the response is successful. | +### `Mutation.corpusCreate` + +Available only when feature flag `corpus_management` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice. + +Input type: `CorpusCreateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `fullPath` | [`ID!`](#id) | Project the corpus belongs to. | +| `packageId` | [`PackagesPackageID!`](#packagespackageid) | ID of the corpus package. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | + ### `Mutation.createAlertIssue` Input type: `CreateAlertIssueInput` diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index aa875576b7c4e1..de99f6289cde95 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -78,6 +78,7 @@ module MutationType mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Update mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Destroy mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create + mount_mutation ::Mutations::AppSec::Fuzzing::Coverage::Corpus::Create, feature_flag: :corpus_management mount_mutation ::Mutations::Projects::SetComplianceFramework mount_mutation ::Mutations::SecurityPolicy::CommitScanExecutionPolicy mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject diff --git a/ee/app/graphql/mutations/app_sec/fuzzing/coverage/corpus/create.rb b/ee/app/graphql/mutations/app_sec/fuzzing/coverage/corpus/create.rb new file mode 100644 index 00000000000000..ad5c8229c6d7b7 --- /dev/null +++ b/ee/app/graphql/mutations/app_sec/fuzzing/coverage/corpus/create.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module Mutations + module AppSec::Fuzzing::Coverage + module Corpus + class Create < BaseMutation + include FindsProject + + graphql_name 'CorpusCreate' + + authorize :create_coverage_fuzzing_corpus + + argument :package_id, Types::GlobalIDType[::Packages::Package], + required: true, + description: 'ID of the corpus package.' + + argument :full_path, GraphQL::Types::ID, + required: true, + description: 'Project the corpus belongs to.' + + def resolve(full_path:, package_id:) + project = authorized_find!(full_path) + + raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project) + + response = ::AppSec::Fuzzing::Coverage::Corpuses::CreateService.new( + project: project, + current_user: current_user, + params: { + package_id: Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(package_id))&.id + } + ).execute + + return { errors: response.errors } if response.error? + + build_response(response.payload) + end + + private + + def allowed?(project) + Feature.enabled?(:corpus_management, project, default_enabled: :yaml) + end + + def build_response(payload) + { + errors: [], + corpus: payload.fetch(:corpus) + } + end + end + end + end +end diff --git a/ee/app/models/app_sec/fuzzing/coverage/corpus.rb b/ee/app/models/app_sec/fuzzing/coverage/corpus.rb index bea6ca907cadce..04cdf4cb2d553b 100644 --- a/ee/app/models/app_sec/fuzzing/coverage/corpus.rb +++ b/ee/app/models/app_sec/fuzzing/coverage/corpus.rb @@ -9,6 +9,20 @@ class Corpus < ApplicationRecord belongs_to :package, class_name: 'Packages::Package' belongs_to :user, optional: true belongs_to :project + + validate :project_same_as_package_project + + def audit_details + user&.name + end + + private + + def project_same_as_package_project + if package && package.project_id != project_id + errors.add(:package_id, 'should belong to the associated project') + end + end end end end diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 8856a545351f09..6f0a10c3bedcfe 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -210,6 +210,7 @@ module ProjectPolicy rule { coverage_fuzzing_enabled & can?(:developer_access) }.policy do enable :read_coverage_fuzzing + enable :create_coverage_fuzzing_corpus end rule { on_demand_scans_enabled & can?(:developer_access) }.policy do diff --git a/ee/app/services/app_sec/fuzzing/coverage/corpuses/create_service.rb b/ee/app/services/app_sec/fuzzing/coverage/corpuses/create_service.rb new file mode 100644 index 00000000000000..a29f7ef61458d7 --- /dev/null +++ b/ee/app/services/app_sec/fuzzing/coverage/corpuses/create_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module AppSec + module Fuzzing + module Coverage + module Corpuses + class CreateService < BaseProjectService + def execute + return ServiceResponse.error(message: 'Insufficient permissions') unless allowed? + + corpus = AppSec::Fuzzing::Coverage::Corpus.new( + project: project, + user: current_user, + package_id: params.fetch(:package_id) + ) + + if corpus.save + create_audit_event(corpus) + + return ServiceResponse.success( + payload: { + corpus: corpus + } + ) + end + + ServiceResponse.error(message: corpus.errors.full_messages) + rescue KeyError => err + ServiceResponse.error(message: err.message.capitalize) + end + + private + + def allowed? + project.licensed_feature_available?(:coverage_fuzzing) + end + + def create_audit_event(corpus) + ::Gitlab::Audit::Auditor.audit( + name: 'coverage_fuzzing_corpus_create', + author: current_user, + scope: project, + target: corpus, + message: 'Added Coverage Fuzzing Corpus' + ) + end + end + end + end + end +end diff --git a/ee/spec/factories/app_sec/fuzzing/coverage/corpuses.rb b/ee/spec/factories/app_sec/fuzzing/coverage/corpuses.rb index 9fad8aa946fd52..90bb819ff11740 100644 --- a/ee/spec/factories/app_sec/fuzzing/coverage/corpuses.rb +++ b/ee/spec/factories/app_sec/fuzzing/coverage/corpuses.rb @@ -3,7 +3,7 @@ FactoryBot.define do factory :corpus, class: 'AppSec::Fuzzing::Coverage::Corpus' do user - package project + package { association :package, project: project } end end diff --git a/ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb b/ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb new file mode 100644 index 00000000000000..56baedc84a3fb2 --- /dev/null +++ b/ee/spec/graphql/mutations/app_sec/fuzzing/coverage/corpus/create_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::AppSec::Fuzzing::Coverage::Corpus::Create do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user, developer_projects: [project] ) } + let_it_be(:package) { create(:package, project: project, creator: developer) } + + let(:corpus) { AppSec::Fuzzing::Coverage::Corpus.find_by(user: developer, project: project) } + + let(:mutation) { described_class.new(object: nil, context: { current_user: developer }, field: nil) } + + before do + stub_licensed_features(coverage_fuzzing: true) + end + + specify { expect(described_class).to require_graphql_authorizations(:create_coverage_fuzzing_corpus) } + + describe '#resolve' do + subject(:resolve) do + mutation.resolve( + full_path: project.full_path, + package_id: package.to_global_id + ) + end + + context 'when the feature is licensed' do + context 'when the user can create a corpus' do + context 'when corpus_management feature is enabled' do + before do + stub_feature_flags(corpus_management: true) + end + + it 'returns the corpus' do + expect(resolve[:corpus]).to eq(corpus) + end + end + + context 'when corpus_management feature is disabled' do + before do + stub_feature_flags(corpus_management: false) + end + + it 'raises the resource not available error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + end + end + end +end diff --git a/ee/spec/models/app_sec/fuzzing/coverage/corpus_spec.rb b/ee/spec/models/app_sec/fuzzing/coverage/corpus_spec.rb index 79b7e34c489b96..512e124aa84f83 100644 --- a/ee/spec/models/app_sec/fuzzing/coverage/corpus_spec.rb +++ b/ee/spec/models/app_sec/fuzzing/coverage/corpus_spec.rb @@ -12,4 +12,16 @@ it { is_expected.to belong_to(:user).optional } it { is_expected.to belong_to(:project) } end + + describe 'validate' do + describe 'project_same_as_package_project' do + let(:package_2) { create(:package) } + + subject(:corpus) { build(:corpus, package: package_2) } + + it 'raises the error on adding the package of a different project' do + expect { corpus.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package should belong to the associated project') + end + end + end end diff --git a/ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb b/ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb new file mode 100644 index 00000000000000..f4f8f9d833f1c3 --- /dev/null +++ b/ee/spec/services/app_sec/fuzzing/coverage/corpuses/create_service_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe AppSec::Fuzzing::Coverage::Corpuses::CreateService do + let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user, developer_projects: [project] ) } + let_it_be(:package) { create(:package, project: project, creator: developer) } + + let_it_be(:default_params) do + { + package_id: package.id + } + end + + let(:params) { default_params } + + subject(:service_result) { described_class.new(project: project, current_user: developer, params: params).execute } + + describe 'execute' do + before do + stub_licensed_features(coverage_fuzzing: coverage_fuzzing_enabled?) + end + + context 'when the feature coverage_fuzzing is not available' do + let(:coverage_fuzzing_enabled?) { false } + + it 'communicates failure', :aggregate_failures do + expect(service_result.status).to eq(:error) + expect(service_result.message).to eq('Insufficient permissions') + end + end + + context 'when the feature coverage_fuzzing is enabled' do + let(:coverage_fuzzing_enabled?) { true } + + it 'communicates success' do + expect(service_result.status).to eq(:success) + end + + it 'creates a corpus' do + expect { service_result }.to change { AppSec::Fuzzing::Coverage::Corpus.count }.by(1) + end + + it 'audits the creation', :aggregate_failures do + corpus = service_result.payload[:corpus] + + audit_event = AuditEvent.find_by(target_id: corpus.id) + + expect(audit_event.author).to eq(developer) + expect(audit_event.entity).to eq(project) + expect(audit_event.target_id).to eq(corpus.id) + expect(audit_event.target_type).to eq('AppSec::Fuzzing::Coverage::Corpus') + expect(audit_event.target_details).to eq(developer.name) + expect(audit_event.details).to eq({ + author_name: developer.name, + custom_message: 'Added Coverage Fuzzing Corpus', + target_id: corpus.id, + target_type: 'AppSec::Fuzzing::Coverage::Corpus', + target_details: developer.name + }) + end + + context 'when a param is missing' do + let(:params) { default_params.except(:package_id) } + + it 'communicates failure', :aggregate_failures do + expect(service_result.status).to eq(:error) + expect(service_result.message).to eq('Key not found: :package_id') + end + end + + context 'when a param is incorrect' do + let(:package_2) { create(:package) } + let(:params) { { package_id: package_2.id } } + + it 'communicates failure', :aggregate_failures do + allow_next_instance_of(AppSec::Fuzzing::Coverage::Corpus) do |service| + allow(service).to receive(:save).and_return(false) + allow(service).to receive_message_chain(:errors, :full_messages) + .and_return(['error message']) + end + + expect(service_result.status).to eq(:error) + expect(service_result.message).to match_array(['error message']) + end + end + end + end +end -- GitLab