diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md
index 05a118d40d1fd63602b54cc8955f2c5e659ba34c..de7924463fd389e5edb8304834f310581c8ab580 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -5255,6 +5255,29 @@ Input type: `DestroyComplianceRequirementInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.destroyComplianceRequirementsControl`
+
+{{< details >}}
+**Introduced** in GitLab 17.9.
+**Status**: Experiment.
+{{< /details >}}
+
+Input type: `DestroyComplianceRequirementsControlInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `id` | [`ComplianceManagementComplianceFrameworkComplianceRequirementsControlID!`](#compliancemanagementcomplianceframeworkcompliancerequirementscontrolid) | Global ID of the compliance requirement control 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`
diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md
index a0b9911c98549750460ca6c033ad7ef117e45af6..05ad559276c7606011caca847bb1b50e180c4cd6 100644
--- a/doc/user/compliance/audit_event_types.md
+++ b/doc/user/compliance/audit_event_types.md
@@ -159,6 +159,7 @@ Audit event types belong to the following product categories.
| [`delete_status_check`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/84624) | An external status check is deleted | {{< icon name="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) | A compliance framework is successfully deleted | {{< icon name="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) | A compliance framework requirement is destroyed | {{< icon name="check-circle" >}} Yes | GitLab [17.7](https://gitlab.com/gitlab-org/gitlab/-/issues/470695) | Group |
+| [`destroyed_compliance_requirement_control`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177878) | A compliance requirement control is destroyed. | {{< icon name="check-circle" >}} Yes | GitLab [17.9](https://gitlab.com/gitlab-org/gitlab/-/issues/512381) | Group |
| [`email_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114546) | An email is created | {{< icon name="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) | An email is destroyed | {{< icon name="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) | The name of an external status check is updated | {{< icon name="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 3b18eeae4faf50c5721b75a9e5b081a9c97fa258..1cdcb6b9ef56490e76aa18c1e3718c7c0be84508 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -276,6 +276,8 @@ def self.authorization_scopes
experiment: { milestone: '17.9' }
mount_mutation ::Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::Update,
experiment: { milestone: '17.9' }
+ mount_mutation ::Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::Destroy,
+ experiment: { milestone: '17.9' }
mount_mutation ::Mutations::Ai::DuoSettings::Update, experiment: { milestone: '17.9' }
mount_mutation ::Mutations::Ai::DeleteConversationThread, experiment: { milestone: '17.9' }
diff --git a/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy.rb b/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..09d87e10a4fe3c6a0b40dbc99ce747d088f5fbca
--- /dev/null
+++ b/ee/app/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ComplianceManagement
+ module ComplianceFramework
+ module ComplianceRequirementsControls
+ class Destroy < BaseMutation
+ graphql_name 'DestroyComplianceRequirementsControl'
+
+ authorize :admin_compliance_framework
+
+ argument :id, ::Types::GlobalIDType[
+ ::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl
+ ],
+ required: true,
+ description: 'Global ID of the compliance requirement control to destroy.'
+
+ def resolve(id:)
+ control = authorized_find!(id: id)
+
+ result = ::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::DestroyService.new(
+ control: control, current_user: current_user).execute
+
+ { errors: result.success? ? [] : Array.wrap(result.message) }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/destroy_service.rb b/ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/destroy_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c5e00fdf6c2a40f643adf138663a2d2d8491cc3
--- /dev/null
+++ b/ee/app/services/compliance_management/compliance_framework/compliance_requirements_controls/destroy_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module ComplianceManagement
+ module ComplianceFramework
+ module ComplianceRequirementsControls
+ class DestroyService < BaseService
+ attr_reader :current_user, :control
+
+ def initialize(control:, current_user:)
+ @control = control
+ @current_user = current_user
+ end
+
+ def execute
+ return ServiceResponse.error(message: _('Not permitted to destroy requirement control')) unless permitted?
+
+ control.destroy ? success : error
+ end
+
+ private
+
+ def permitted?
+ can? current_user, :admin_compliance_framework, control.compliance_requirement.framework
+ end
+
+ def success
+ audit_destroy
+
+ ServiceResponse.success(message: _('Compliance requirement control successfully deleted'))
+ end
+
+ def audit_destroy
+ audit_context = {
+ name: 'destroyed_compliance_requirement_control',
+ author: current_user,
+ scope: control.namespace,
+ target: control,
+ message: "Destroyed compliance requirement control #{control.name}"
+ }
+
+ ::Gitlab::Audit::Auditor.audit(audit_context)
+ end
+
+ def error
+ ServiceResponse.error(message: _('Failed to destroy compliance requirement control'), payload: control.errors)
+ end
+ end
+ end
+ end
+end
diff --git a/ee/config/audit_events/types/destroyed_compliance_requirement_control.yml b/ee/config/audit_events/types/destroyed_compliance_requirement_control.yml
new file mode 100644
index 0000000000000000000000000000000000000000..da2a413c87d34a46edafd633f2f8d70edd793641
--- /dev/null
+++ b/ee/config/audit_events/types/destroyed_compliance_requirement_control.yml
@@ -0,0 +1,10 @@
+---
+name: destroyed_compliance_requirement_control
+description: A compliance requirement control is destroyed.
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/512381
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/177878
+milestone: '17.9'
+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_controls/destroy_spec.rb b/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..952a3e03319a70a1c587f470d75e5e2dac2b1c55
--- /dev/null
+++ b/ee/spec/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::Destroy,
+ feature_category: :compliance_management do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:requirement) do
+ create(:compliance_requirement, framework: create(:compliance_framework, namespace: namespace))
+ end
+
+ let_it_be(:control) { create(:compliance_requirements_control, compliance_requirement: requirement) }
+
+ let_it_be(:current_user) { create(:user) }
+ let(:mutation) { described_class.new(object: nil, context: query_context, field: nil) }
+
+ subject(:mutate) { mutation.resolve(id: global_id_of(control)) }
+
+ before_all do
+ namespace.add_owner(current_user)
+ end
+
+ context 'when feature is unlicensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: false)
+ end
+
+ it 'raises an error' do
+ expect { mutate }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when feature is licensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: true)
+ end
+
+ it 'destroys a compliance requirement control' do
+ expect { mutate }.to change {
+ ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.exists?(id: control.id)
+ }.from(true).to(false)
+ end
+
+ it 'expects zero errors in the response' do
+ expect(mutate[:errors]).to be_empty
+ end
+ end
+end
diff --git a/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy_spec.rb b/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3989e90d370eb6836368778197024d44c6a3f302
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/compliance_management/compliance_framework/compliance_requirements_controls/destroy_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Destroy a Compliance Requirement Control', feature_category: :compliance_management do
+ include GraphqlHelpers
+
+ let_it_be(:namespace) { create(:group) }
+ let_it_be(:requirement) do
+ create(:compliance_requirement, framework: create(:compliance_framework, namespace: namespace))
+ end
+
+ let_it_be(:control) { create(:compliance_requirements_control, compliance_requirement: requirement) }
+
+ let_it_be(:current_user) { create(:user) }
+ let(:mutation) { graphql_mutation(:destroy_compliance_requirements_control, { id: global_id_of(control) }) }
+
+ subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ def mutation_response
+ graphql_mutation_response(:destroy_compliance_requirements_control)
+ end
+
+ context 'when feature is unlicensed' do
+ before do
+ stub_licensed_features(custom_compliance_frameworks: false)
+ end
+
+ it 'does not destroy a compliance requirement control' do
+ expect { mutate }.not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.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 control' do
+ expect { mutate }.to change {
+ ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.exists?(id: control.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 control' do
+ expect { mutate }
+ .not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.count }
+ end
+ end
+ end
+end
diff --git a/ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/destroy_service_spec.rb b/ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/destroy_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..18ccd241f189eea20962ddad3902b07c24edd49c
--- /dev/null
+++ b/ee/spec/services/compliance_management/compliance_framework/compliance_requirements_controls/destroy_service_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ComplianceManagement::ComplianceFramework::ComplianceRequirementsControls::DestroyService,
+ feature_category: :compliance_management do
+ let_it_be_with_refind(:namespace) { create(:group) }
+ let_it_be(:requirement) do
+ create(:compliance_requirement, framework: create(:compliance_framework, namespace: namespace))
+ end
+
+ let_it_be(:control) { create(:compliance_requirements_control, compliance_requirement: requirement) }
+ 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 control' do
+ expect { service.execute }
+ .not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.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(control: control, current_user: owner) }
+
+ it_behaves_like 'unsuccessful destruction', 'Not permitted to destroy requirement control'
+ end
+
+ context 'when current user is not the namespace owner' do
+ subject(:service) { described_class.new(control: control, current_user: non_owner) }
+
+ it_behaves_like 'unsuccessful destruction', 'Not permitted to destroy requirement control'
+ 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(control: control, current_user: owner) }
+
+ it 'destroys the compliance requirement control' do
+ expect { service.execute }.to change {
+ ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.exists?(id: control.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 control successfully deleted')
+ end
+
+ it 'audits the destruction' do
+ service.execute
+
+ expect(::Gitlab::Audit::Auditor).to have_received(:audit).with(
+ name: 'destroyed_compliance_requirement_control',
+ author: owner,
+ scope: control.namespace,
+ target: control,
+ message: "Destroyed compliance requirement control #{control.name}"
+ )
+ end
+
+ context 'when destruction fails' do
+ before do
+ allow(control).to receive(:destroy).and_return(false)
+ end
+
+ it 'is unsuccessful' do
+ result = service.execute
+
+ expect(result.success?).to be false
+ expect(result.message).to eq _('Failed to destroy compliance requirement control')
+ end
+ end
+ end
+
+ context 'when current user is not the namespace owner' do
+ subject(:service) { described_class.new(control: control, current_user: non_owner) }
+
+ it 'does not destroy the compliance requirement control' do
+ expect { service.execute }
+ .not_to change { ComplianceManagement::ComplianceFramework::ComplianceRequirementsControl.count }
+ end
+
+ it_behaves_like 'unsuccessful destruction', 'Not permitted to destroy requirement control'
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e34d594479ae1f19519a388e7d63bc7c3ebc135b..959bc33dfd99093b0fc33f82e10921d8d72caf91 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14613,6 +14613,9 @@ msgstr ""
msgid "Compliance frameworks"
msgstr ""
+msgid "Compliance requirement control successfully deleted"
+msgstr ""
+
msgid "Compliance requirement successfully deleted"
msgstr ""
@@ -24277,6 +24280,9 @@ msgstr ""
msgid "Failed to destroy compliance requirement"
msgstr ""
+msgid "Failed to destroy compliance requirement control"
+msgstr ""
+
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
@@ -38485,6 +38491,9 @@ msgstr ""
msgid "Not permitted to destroy requirement"
msgstr ""
+msgid "Not permitted to destroy requirement control"
+msgstr ""
+
msgid "Not permitted to reset user feed token"
msgstr ""