diff --git a/doc/administration/audit_event_types.md b/doc/administration/audit_event_types.md
index 2a33cc2afff610699b8cdf94dfa88b0e4f5d137b..e3109eb836f7857f2b694d6fc1cbbf5959c4f11b 100644
--- a/doc/administration/audit_event_types.md
+++ b/doc/administration/audit_event_types.md
@@ -79,6 +79,7 @@ Audit event types belong to the following product categories.
| [`instance_google_cloud_logging_configuration_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131790) | Triggered when instance level Google Cloud Logging configuration is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/423039) | Instance |
| [`update_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74632) | Event triggered when an external audit event destination is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [14.6](https://gitlab.com/gitlab-org/gitlab/-/issues/344664) | Group |
| [`update_instance_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/125846) | Event triggered when an instance level external audit event destination is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.2](https://gitlab.com/gitlab-org/gitlab/-/issues/404730) | Instance |
+| [`updated_group_audit_event_streaming_destination`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148388) | Event triggered when an external audit event destination for a top-level group is updated.| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.11](https://gitlab.com/gitlab-org/gitlab/-/issues/436610) | Group |
### Build artifacts
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e9ee4d4e901a20f23f1c5e8b17c1b087e09a100b..51c5d5d2064143701fb37dfe5692022b4a022c6c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -4783,6 +4783,33 @@ Input type: `GroupAuditEventStreamingDestinationsDeleteInput`
| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+### `Mutation.groupAuditEventStreamingDestinationsUpdate`
+
+DETAILS:
+**Introduced** in GitLab 16.11.
+**Status**: Experiment.
+
+Input type: `GroupAuditEventStreamingDestinationsUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `category` | [`String`](#string) | Destination category. |
+| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| `config` | [`JSON`](#json) | Destination config. |
+| `id` | [`AuditEventsGroupExternalStreamingDestinationID!`](#auditeventsgroupexternalstreamingdestinationid) | ID of external audit event destination to update. |
+| `name` | [`String`](#string) | Destination name. |
+| `secretToken` | [`String`](#string) | Secret token. |
+
+#### 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. |
+| `externalAuditEventDestination` | [`GroupAuditEventStreamingDestination`](#groupauditeventstreamingdestination) | Updated destination. |
+
### `Mutation.groupMemberBulkUpdate`
Input type: `GroupMemberBulkUpdateInput`
diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb
index f7f9b13cce0e49c2d2b9bff4b82a5a3934190194..10705b65838b036af94fee4727e3338a984324c5 100644
--- a/ee/app/graphql/ee/types/mutation_type.rb
+++ b/ee/app/graphql/ee/types/mutation_type.rb
@@ -172,6 +172,8 @@ module MutationType
alpha: { milestone: '16.11' }
mount_mutation ::Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Delete,
alpha: { milestone: '16.11' }
+ mount_mutation ::Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update,
+ alpha: { milestone: '16.11' }
mount_mutation ::Mutations::AuditEvents::Instance::AuditEventStreamingDestinations::Create,
alpha: { milestone: '16.11' }
diff --git a/ee/app/graphql/mutations/audit_events/group/audit_event_streaming_destinations/update.rb b/ee/app/graphql/mutations/audit_events/group/audit_event_streaming_destinations/update.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f780d3ef25544805569a4f36a0339044d42e0939
--- /dev/null
+++ b/ee/app/graphql/mutations/audit_events/group/audit_event_streaming_destinations/update.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AuditEvents
+ module Group
+ module AuditEventStreamingDestinations
+ class Update < Base
+ graphql_name 'GroupAuditEventStreamingDestinationsUpdate'
+
+ include ::Audit::Changes
+
+ UPDATE_EVENT_NAME = 'updated_group_audit_event_streaming_destination'
+ AUDIT_EVENT_COLUMNS = [:config, :name, :category, :secret_token].freeze
+
+ argument :id, ::Types::GlobalIDType[::AuditEvents::Group::ExternalStreamingDestination],
+ required: true,
+ description: 'ID of external audit event destination to update.'
+
+ argument :config, GraphQL::Types::JSON, # rubocop:disable Graphql/JSONType -- Different type of destinations will have different configs
+ required: false,
+ description: 'Destination config.'
+
+ argument :name, GraphQL::Types::String,
+ required: false,
+ description: 'Destination name.'
+
+ argument :category, GraphQL::Types::String,
+ required: false,
+ description: 'Destination category.'
+
+ argument :secret_token, GraphQL::Types::String,
+ required: false,
+ description: 'Secret token.'
+
+ field :external_audit_event_destination, ::Types::AuditEvents::Group::StreamingDestinationType,
+ null: true,
+ description: 'Updated destination.'
+
+ def resolve(id:, config: nil, name: nil, category: nil, secret_token: nil)
+ destination = authorized_find!(id: id)
+
+ destination_attributes = {
+ config: config,
+ name: name,
+ category: category,
+ secret_token: secret_token
+ }.compact
+
+ if destination.update(destination_attributes)
+ audit_update(destination)
+ {
+ external_audit_event_destination: destination,
+ errors: []
+ }
+ else
+ { external_audit_event_destination: nil, errors: Array(destination.errors) }
+ end
+ end
+
+ private
+
+ def audit_update(destination)
+ AUDIT_EVENT_COLUMNS.each do |column|
+ audit_changes(
+ column,
+ as: column.to_s,
+ entity: destination.group,
+ model: destination,
+ event_type: UPDATE_EVENT_NAME
+ )
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/ee/config/audit_events/types/updated_group_audit_event_streaming_destination.yml b/ee/config/audit_events/types/updated_group_audit_event_streaming_destination.yml
new file mode 100644
index 0000000000000000000000000000000000000000..195cd914b95253c7bd3392606bae670cec37409b
--- /dev/null
+++ b/ee/config/audit_events/types/updated_group_audit_event_streaming_destination.yml
@@ -0,0 +1,9 @@
+name: updated_group_audit_event_streaming_destination
+description: Event triggered when an external audit event destination for a top-level group is updated.
+introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/436610
+introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/148388
+feature_category: audit_events
+milestone: "16.11"
+saved_to_database: true
+streamed: true
+scope: [Group]
diff --git a/ee/spec/requests/api/graphql/mutations/audit_events/group/audit_event_streaming_destinations/update_spec.rb b/ee/spec/requests/api/graphql/mutations/audit_events/group/audit_event_streaming_destinations/update_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f9a3d76a6668bcf210f8d078a9d90624f56306ce
--- /dev/null
+++ b/ee/spec/requests/api/graphql/mutations/audit_events/group/audit_event_streaming_destinations/update_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Update group level external audit event streaming destination', feature_category: :audit_events do
+ include GraphqlHelpers
+
+ let_it_be_with_reload(:destination) { create(:audit_events_group_external_streaming_destination) }
+ let_it_be(:group) { destination.group }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:updated_config) do
+ {
+ "accessKeyXid" => 'AKIA1234RANDOM5678',
+ "bucketName" => 'test-rspec-bucket',
+ "awsRegion" => 'us-east-2'
+ }
+ end
+
+ let_it_be(:updated_secret_token) { 'TEST/SECRET/XYZ/PQR' }
+ let_it_be(:updated_category) { 'aws' }
+ let_it_be(:updated_destination_name) { 'updated_destination_name' }
+ let_it_be(:destination_gid) { global_id_of(destination) }
+
+ let(:mutation) { graphql_mutation(:group_audit_event_streaming_destinations_update, input) }
+ let(:mutation_response) { graphql_mutation_response(:group_audit_event_streaming_destinations_update) }
+
+ let(:input) do
+ {
+ id: destination_gid,
+ config: updated_config,
+ name: updated_destination_name,
+ category: updated_category,
+ secret_token: updated_secret_token
+ }
+ end
+
+ subject(:mutate) { post_graphql_mutation(mutation, current_user: current_user) }
+
+ shared_examples 'a mutation that does not update the destination' do
+ it 'does not update the destination' do
+ expect { mutate }.not_to change { destination.reload.attributes }
+ end
+
+ it 'does not create audit event' do
+ expect { mutate }.not_to change { AuditEvent.count }
+ end
+ end
+
+ context 'when feature is licensed' do
+ before do
+ stub_licensed_features(external_audit_events: true)
+ end
+
+ context 'when current user is a group owner' do
+ before_all do
+ group.add_owner(current_user)
+ end
+
+ it 'updates the destination' do
+ mutate
+
+ destination.reload
+
+ expect(destination.config).to eq(updated_config)
+ expect(destination.name).to eq(updated_destination_name)
+ expect(destination.category).to eq(updated_category)
+ expect(destination.secret_token).to eq(updated_secret_token)
+ end
+
+ it 'audits the update' do
+ Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update::AUDIT_EVENT_COLUMNS.each do |column|
+ message = if column == :secret_token
+ "Changed #{column}"
+ else
+ "Changed #{column} from #{destination[column]} to #{input[column.to_s.camelize(:lower).to_sym]}"
+ end
+
+ expected_hash = {
+ name: Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update::UPDATE_EVENT_NAME,
+ author: current_user,
+ scope: group,
+ target: destination,
+ message: message
+ }
+
+ expect(Gitlab::Audit::Auditor).to receive(:audit).once.ordered.with(hash_including(expected_hash))
+ end
+
+ mutate
+ end
+
+ context 'when the fields are updated with existing values' do
+ let(:input) do
+ {
+ id: destination_gid,
+ config: destination.config,
+ name: destination.name
+ }
+ end
+
+ it 'does not audit the event' do
+ expect(Gitlab::Audit::Auditor).not_to receive(:audit)
+
+ mutate
+ end
+ end
+
+ context 'when no fields are provided for update' do
+ let(:input) do
+ {
+ id: destination_gid
+ }
+ end
+
+ it_behaves_like 'a mutation that does not update the destination'
+ end
+
+ context 'when there is error while updating' do
+ before do
+ allow_next_instance_of(Mutations::AuditEvents::Group::AuditEventStreamingDestinations::Update) do |mutation|
+ allow(mutation).to receive(:authorized_find!).with(id: destination_gid).and_return(destination)
+ end
+
+ allow(destination).to receive(:update).and_return(false)
+
+ errors = ActiveModel::Errors.new(destination).tap { |e| e.add(:base, 'error message') }
+ allow(destination).to receive(:errors).and_return(errors)
+ end
+
+ it 'does not update the destination and returns the error' do
+ mutate
+
+ expect(mutation_response).to include(
+ 'externalAuditEventDestination' => nil,
+ 'errors' => ['error message']
+ )
+ end
+ end
+ end
+
+ context 'when current user is a group maintainer' do
+ before_all do
+ group.add_maintainer(current_user)
+ end
+
+ it_behaves_like 'a mutation on an unauthorized resource'
+ it_behaves_like 'a mutation that does not update the destination'
+ end
+
+ context 'when current user is a group developer' do
+ before_all do
+ group.add_developer(current_user)
+ end
+
+ it_behaves_like 'a mutation on an unauthorized resource'
+ it_behaves_like 'a mutation that does not update the destination'
+ end
+
+ context 'when current user is a group guest' do
+ before_all do
+ group.add_guest(current_user)
+ end
+
+ it_behaves_like 'a mutation on an unauthorized resource'
+ it_behaves_like 'a mutation that does not update the destination'
+ end
+ end
+
+ context 'when feature is unlicensed' do
+ before do
+ stub_licensed_features(external_audit_events: false)
+ end
+
+ it_behaves_like 'a mutation on an unauthorized resource'
+ it_behaves_like 'a mutation that does not update the destination'
+ end
+end