diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 69fbd45ac535cd977a2e1911857c9a633728acc6..80120964dffad12bae4298a1f2fb8ea3856504e2 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4689,6 +4689,28 @@ Input type: `DestroyComplianceFrameworkInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.destroyComplianceRequirement`
+
+DETAILS:
+**Introduced** in GitLab 17.7.
+**Status**: Experiment.
+
+Input type: `DestroyComplianceRequirementInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `id` | [`ComplianceManagementComplianceFrameworkComplianceRequirementID!`](#compliancemanagementcomplianceframeworkcompliancerequirementid) | Global ID of the compliance requirement to destroy. |
+
+#### 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.destroyContainerRepository`
Input type: `DestroyContainerRepositoryInput`
@@ -41232,6 +41254,12 @@ Color represented as a hex code or named color.
For example: "#fefefe".
+### `ComplianceManagementComplianceFrameworkComplianceRequirementID`
+
+A `ComplianceManagementComplianceFrameworkComplianceRequirementID` is a global ID. It is encoded as a string.
+
+An example `ComplianceManagementComplianceFrameworkComplianceRequirementID` is: `"gid://gitlab/ComplianceManagement::ComplianceFramework::ComplianceRequirement/1"`.
+
### `ComplianceManagementFrameworkID`
A `ComplianceManagementFrameworkID` is a global ID. It is encoded as a string.
diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md
index 41be7a311633606676c54c5c1ce0154678e0c70f..6bb9ad7f500e99c0d1b1e5587914d267a378858c 100644
--- a/doc/user/compliance/audit_event_types.md
+++ b/doc/user/compliance/audit_event_types.md
@@ -149,6 +149,7 @@ Audit event types belong to the following product categories.
| [`created_compliance_requirement`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169485) | Triggered when a requirement is added to a compliance framework | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.6](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group |
| [`delete_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | Triggered when an external status check is deleted | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.9](https://gitlab.com/gitlab-org/gitlab/-/issues/355805) | Project |
| [`destroy_compliance_framework`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74292) | Triggered when a compliance framework is successfully deleted | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/340649) | Group |
+| [`destroyed_compliance_requirement`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170380) | Triggered when a compliance framework requirement is destroyed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group |
| [`email_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114546) | Triggered when an email is created | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.11](https://gitlab.com/gitlab-org/gitlab/-/issues/374107) | User |
| [`email_destroyed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114546) | Triggered when an email is destroyed | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.11](https://gitlab.com/gitlab-org/gitlab/-/issues/374107) | User |
| [`external_status_check_name_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106095) | Triggered when the name of an external status check is updated | **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.7](https://gitlab.com/gitlab-org/gitlab/-/issues/369333) | Project |
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index efe930e5b7145fc49ed10efe9b611381ca5c29b1..f187eba8a6274523c57a177619a1f87d0e605a6b 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -255,6 +255,8 @@ def self.authorization_scopes
mount_mutation ::Mutations::Projects::TargetBranchRules::Destroy
mount_mutation ::Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirements::Create,
experiment: { milestone: '17.6' }
+ mount_mutation ::Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirements::Destroy,
+ experiment: { milestone: '17.7' }
prepend(Types::DeprecatedMutations)
end
diff --git a/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy.rb b/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e6f5aed25a1989736b6a83216688b83226d3937f
--- /dev/null
+++ b/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ComplianceManagement
+ module ComplianceFramework
+ module ComplianceRequirements
+ class Destroy < BaseMutation
+ graphql_name 'DestroyComplianceRequirement'
+ authorize :admin_compliance_framework
+
+ argument :id, ::Types::GlobalIDType[::ComplianceManagement::ComplianceFramework::ComplianceRequirement],
+ required: true,
+ description: 'Global ID of the compliance requirement to destroy.'
+
+ def resolve(id:)
+ requirement = authorized_find!(id: id)
+
+ result = ::ComplianceManagement::ComplianceFramework::ComplianceRequirements::DestroyService.new(
+ requirement: requirement, current_user: current_user).execute
+
+ { errors: result.success? ? [] : Array.wrap(result.message) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/app/policies/compliance_management/compliance_framework/compliance_requirement_policy.rb b/ee/app/policies/compliance_management/compliance_framework/compliance_requirement_policy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..278f97a87c616c8eacad4e48aec467c6366554c8
--- /dev/null
+++ b/ee/app/policies/compliance_management/compliance_framework/compliance_requirement_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module ComplianceManagement
+ module ComplianceFramework
+ class ComplianceRequirementPolicy < BasePolicy
+ delegate { @subject.framework }
+ end
+ end
+end
diff --git a/ee/app/services/compliance_management/compliance_framework/compliance_requirements/destroy_service.rb b/ee/app/services/compliance_management/compliance_framework/compliance_requirements/destroy_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6c77494eca26745a5b9b71525f35e19b0695776
--- /dev/null
+++ b/ee/app/services/compliance_management/compliance_framework/compliance_requirements/destroy_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ComplianceManagement
+ module ComplianceFramework
+ module ComplianceRequirements
+ class DestroyService < BaseService
+ attr_reader :current_user, :requirement
+
+ def initialize(requirement:, current_user:)
+ @requirement = requirement
+ @current_user = current_user
+ end
+
+ def execute
+ return ServiceResponse.error(message: _('Not permitted to destroy requirement')) unless permitted?
+
+ requirement.destroy ? success : error
+ end
+
+ private
+
+ def permitted?
+ can? current_user, :admin_compliance_framework, requirement.framework
+ end
+
+ def success
+ audit_destroy
+
+ ServiceResponse.success(message: _('Compliance requirement successfully deleted'))
+ end
+
+ def audit_destroy
+ audit_context = {
+ name: 'destroyed_compliance_requirement',
+ author: current_user,
+ scope: requirement.framework.namespace,
+ target: requirement,
+ message: "Destroyed compliance requirement #{requirement.name}"
+ }
+
+ ::Gitlab::Audit::Auditor.audit(audit_context)
+ end
+
+ def error
+ ServiceResponse.error(message: _('Failed to destroy compliance requirement'), payload: requirement.errors)
+ end
+ end
+ end
+ end
+end
diff --git a/ee/config/audit_events/types/destroyed_compliance_requirement.yml b/ee/config/audit_events/types/destroyed_compliance_requirement.yml
new file mode 100644
index 0000000000000000000000000000000000000000..82ee7bb03d85c5825318953200bd591f1caa0208
--- /dev/null
+++ b/ee/config/audit_events/types/destroyed_compliance_requirement.yml
@@ -0,0 +1,10 @@
+---
+name: destroyed_compliance_requirement
+description: Triggered when a compliance framework requirement is destroyed
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/470695
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170380
+milestone: '17.7'
+feature_category: compliance_management
+saved_to_database: true
+streamed: true
+scope: [Group]
diff --git a/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy_spec.rb b/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..34c70781db8ce9859a16e072491780a5b4cf402a
--- /dev/null
+++ b/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirements::Destroy,
+ feature_category: :compliance_management do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:framework) { create(:compliance_framework, namespace: namespace) }
+ let_it_be(:requirement) { create(:compliance_requirement, framework: framework) }
+
+ let_it_be(:current_user) { create(:user) }
+ let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
+
+ subject { mutation.resolve(id: global_id_of(requirement)) }
+
+ before_all do
+ namespace.add_owner(current_user)
+ end
+
+ shared_examples 'a compliance requirement that cannot be found' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ shared_examples 'one compliance requirement was destroyed' do
+ it 'destroys a compliance requirement' do
+ expect { subject }.to change {
+ ComplianceManagement::ComplianceFramework::ComplianceRequirement.exists?(id: requirement.id)
+ }.from(true).to(false)
+ end
+
+ it 'expects zero errors in the response' do
+ expect(subject[:errors]).to be_empty
+ end
+ end
+
+ context 'when feature is unlicensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: false)
+ end
+
+ it_behaves_like 'a compliance requirement that cannot be found'
+ end
+
+ context 'when feature is licensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: true)
+ end
+
+ context 'when current_user is namespace owner' do
+ it_behaves_like 'one compliance requirement was destroyed'
+ end
+
+ context 'when current_user is group owner' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:framework) { create(:compliance_framework, namespace: group) }
+ let_it_be(:requirement) { create(:compliance_requirement, framework: framework) }
+
+ before_all do
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'one compliance requirement was destroyed'
+ end
+ end
+end
diff --git a/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy_spec.rb b/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..985d5e93ed5577dff16b2c7092a286bc551e7b5f
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements/destroy_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroy a Compliance Requirement', feature_category: :compliance_management do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:framework) { create(:compliance_framework, namespace: namespace) }
+ let_it_be(:requirement) { create(:compliance_requirement, framework: framework) }
+
+ let_it_be(:current_user) { create(:user) }
+ let(:mutation) { graphql_mutation(:destroy_compliance_requirement, { id: global_id_of(requirement) }) }
+
+ subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:destroy_compliance_requirement)
+ end
+
+ context 'when feature is unlicensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: false)
+ end
+
+ it 'does not destroy a compliance requirement' do
+ expect { mutate }.not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirement.count }
+ end
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+
+ context 'when licensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: true)
+ end
+
+ context 'when current_user is namespace owner' do
+ before_all do
+ namespace.add_owner(current_user)
+ end
+
+ it 'has no errors' do
+ mutate
+
+ expect(mutation_response['errors']).to be_empty
+ end
+
+ it 'destroys a compliance requirement' do
+ expect { mutate }.to change {
+ ComplianceManagement::ComplianceFramework::ComplianceRequirement.exists?(id: requirement.id)
+ }.from(true).to(false)
+ end
+ end
+
+ context 'when current_user is not namespace owner' do
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+
+ it 'does not destroy a compliance requirement' do
+ expect { mutate }.not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirement.count }
+ end
+ end
+ end
+end
diff --git a/ee/spec/services/compliance_management/compliance_framework/compliance_requirements/destroy_service_spec.rb b/ee/spec/services/compliance_management/compliance_framework/compliance_requirements/destroy_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a0d80907315f3427ac4f0adf95f9e464c01d155f
--- /dev/null
+++ b/ee/spec/services/compliance_management/compliance_framework/compliance_requirements/destroy_service_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ComplianceManagement::ComplianceFramework::ComplianceRequirements::DestroyService,
+ feature_category: :compliance_management do
+ let_it_be_with_refind(:namespace) { create(:group) }
+ let_it_be_with_refind(:framework) { create(:compliance_framework, namespace: namespace) }
+ let_it_be_with_refind(:requirement) { create(:compliance_requirement, framework: framework) }
+ let_it_be(:owner) { create(:user, owner_of: namespace) }
+ let_it_be(:non_owner) { create(:user) }
+
+ shared_examples 'unsuccessful destruction' do |error_message|
+ it 'does not destroy the compliance requirement' do
+ expect { service.execute }
+ .not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirement.count }
+ end
+
+ it 'is unsuccessful' do
+ result = service.execute
+
+ expect(result.success?).to be false
+ expect(result.message).to eq _(error_message)
+ end
+
+ it 'does not audit the destruction' do
+ service.execute
+
+ expect(::Gitlab::Audit::Auditor).not_to have_received(:audit)
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: false)
+ allow(::Gitlab::Audit::Auditor).to receive(:audit)
+ end
+
+ context 'when current user is namespace owner' do
+ subject(:service) { described_class.new(requirement: requirement, current_user: owner) }
+
+ it_behaves_like 'unsuccessful destruction', 'Not permitted to destroy requirement'
+ end
+
+ context 'when current user is not the namespace owner' do
+ subject(:service) { described_class.new(requirement: requirement, current_user: non_owner) }
+
+ it_behaves_like 'unsuccessful destruction', 'Not permitted to destroy requirement'
+ end
+ end
+
+ context 'when feature is enabled' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: true)
+ allow(::Gitlab::Audit::Auditor).to receive(:audit)
+ end
+
+ context 'when current user is namespace owner' do
+ subject(:service) { described_class.new(requirement: requirement, current_user: owner) }
+
+ it 'destroys the compliance requirement' do
+ expect { service.execute }.to change {
+ ComplianceManagement::ComplianceFramework::ComplianceRequirement.exists?(id: requirement.id)
+ }.from(true).to(false)
+ end
+
+ it 'is successful' do
+ result = service.execute
+
+ expect(result.success?).to be true
+ expect(result.message).to eq _('Compliance requirement successfully deleted')
+ end
+
+ it 'audits the destruction' do
+ service.execute
+
+ expect(::Gitlab::Audit::Auditor).to have_received(:audit).with(
+ name: 'destroyed_compliance_requirement',
+ author: owner,
+ scope: requirement.framework.namespace,
+ target: requirement,
+ message: "Destroyed compliance requirement #{requirement.name}"
+ )
+ end
+
+ context 'when destruction fails' do
+ before do
+ allow_next_found_instance_of(ComplianceManagement::ComplianceFramework::ComplianceRequirement) do |instance|
+ allow(instance).to receive(:destroy).and_return(false)
+ end
+ end
+
+ it 'is unsuccessful' do
+ result = service.execute
+
+ expect(result.success?).to be false
+ expect(result.message).to eq _('Failed to destroy compliance requirement')
+ end
+ end
+ end
+
+ context 'when current user is not the namespace owner' do
+ subject(:service) { described_class.new(requirement: requirement, current_user: non_owner) }
+
+ it 'does not destroy the compliance requirement' do
+ expect { service.execute }
+ .not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirement.count }
+ end
+
+ it_behaves_like 'unsuccessful destruction', 'Not permitted to destroy requirement'
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c796bf5f4bdfcae223ee9b7c4165dfa5275bff79..ecd997be206e0d3097ab4d1af017b81fcb1daedf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14157,6 +14157,9 @@ msgstr ""
msgid "Compliance frameworks"
msgstr ""
+msgid "Compliance requirement successfully deleted"
+msgstr ""
+
msgid "ComplianceChainOfCustody| Chain of custody export"
msgstr ""
@@ -23131,6 +23134,9 @@ msgstr ""
msgid "Failed to deploy to"
msgstr ""
+msgid "Failed to destroy compliance requirement"
+msgstr ""
+
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
@@ -36797,6 +36803,9 @@ msgstr ""
msgid "Not permitted to destroy framework"
msgstr ""
+msgid "Not permitted to destroy requirement"
+msgstr ""
+
msgid "Not permitted to reset user feed token"
msgstr ""