diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 6257492f7ea317ac0ae8b18248e427636fe986bf..73410628778d5f3a8afdca5e7292bb6706a0b0ac 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -561,6 +561,19 @@ Audit event types belong to the following product categories. | [`merge_request_merged_with_dismissed_security_policy`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/205857) | When a merge request violated a security policy in warn-mode that was dismissed and the MR was merged | {{< icon name="check-circle" >}} Yes | GitLab [18.5](https://gitlab.com/gitlab-org/gitlab/-/issues/569628) | Project | | [`security_policy_merge_request_bypass`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/205601) | A security policy is bypassed for a merge request | {{< icon name="check-circle" >}} Yes | GitLab [18.5](https://gitlab.com/gitlab-org/gitlab/-/issues/549797) | Project | +### Security risk management + +| Type name | Event triggered when | Saved to database | Introduced in | Scope | +|:----------|:---------------------|:------------------|:--------------|:------| +| [`security_attribute_attached_to_project`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security attribute is attached to a project | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Project | +| [`security_attribute_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security attribute is created | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Group | +| [`security_attribute_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security attribute is deleted | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Group | +| [`security_attribute_detached_from_project`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security attribute is detached from a project | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Project | +| [`security_attribute_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security attribute is updated | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Group | +| [`security_category_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security category is created | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Group | +| [`security_category_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security category is deleted | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Group | +| [`security_category_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118) | A security category is updated | {{< icon name="check-circle" >}} Yes | GitLab [18.6](https://gitlab.com/gitlab-org/gitlab/-/issues/568959) | Group | + ### Security testing configuration | Type name | Event triggered when | Saved to database | Introduced in | Scope | diff --git a/ee/app/services/security/attributes/create_service.rb b/ee/app/services/security/attributes/create_service.rb index 3e0db6699cf1743ff1e012f53913ac71e14750ee..fba355a2447c01fa294623df2efa60378074024a 100644 --- a/ee/app/services/security/attributes/create_service.rb +++ b/ee/app/services/security/attributes/create_service.rb @@ -3,6 +3,8 @@ module Security module Attributes class CreateService < BaseService + AUDIT_EVENT_NAME = 'security_attribute_created' + def initialize(category:, namespace:, params:, current_user:) @category = category @root_namespace = namespace&.root_ancestor @@ -31,7 +33,12 @@ def execute return error(attributes.select(&:invalid?)) if attributes.any?(&:invalid?) || !category.valid? - category.save ? success : error # Saving the category saves its attributes + if category.save # Saving the category saves its attributes + create_audit_events + success + else + error + end end attr_reader :category, :root_namespace, :params, :current_user, :attributes @@ -56,6 +63,40 @@ def error(failed_attributes = attributes) message += ": #{errors.join(', ')}" if errors.any? ServiceResponse.error(message: message, payload: errors) end + + def create_audit_events + return if attributes.empty? + + ::Gitlab::Audit::Auditor.audit(created_audit_context) do + attributes.each do |attribute| + event = AuditEvents::BuildService.new( + author: current_user, + scope: root_namespace, + target: attribute, + created_at: Time.current, + message: "Created security attribute #{attribute.name}", + additional_details: { + event_name: AUDIT_EVENT_NAME, + attribute_name: attribute.name, + attribute_description: attribute.description, + attribute_color: attribute.color.to_s, + category_name: category.name + } + ).execute + + ::Gitlab::Audit::EventQueue.push(event) + end + end + end + + def created_audit_context + { + author: current_user, + scope: root_namespace, + target: root_namespace, + name: AUDIT_EVENT_NAME + } + end end end end diff --git a/ee/app/services/security/attributes/destroy_service.rb b/ee/app/services/security/attributes/destroy_service.rb index c35bffa83769b3e72414fab0d569a990a9f62e44..0dcb7f1d793aa392e4a64c25b1167972f06e121e 100644 --- a/ee/app/services/security/attributes/destroy_service.rb +++ b/ee/app/services/security/attributes/destroy_service.rb @@ -3,6 +3,8 @@ module Security module Attributes class DestroyService < BaseService + AUDIT_EVENT_NAME = 'security_attribute_deleted' + def initialize(attribute:, current_user:) @attribute = attribute @current_user = current_user @@ -17,8 +19,12 @@ def execute deleted_attribute_gid = attribute.to_global_id - attribute.destroy ? success(deleted_attribute_gid) : deletion_failed_error("Failed to delete attribute") - + if attribute.destroy + create_audit_event + success(deleted_attribute_gid) + else + deletion_failed_error("Failed to delete attribute") + end rescue ActiveRecord::RecordNotDestroyed => e deletion_failed_error(e.message) end @@ -56,6 +62,25 @@ def not_editable_error def deletion_failed_error(error_message) error_response("Failed to delete attributes: #{error_message}") end + + def create_audit_event + root_namespace = attribute.security_category.namespace&.root_ancestor + + audit_context = { + name: AUDIT_EVENT_NAME, + author: current_user, + scope: root_namespace, + target: attribute, + message: "Deleted security attribute #{attribute.name}", + additional_details: { + attribute_name: attribute.name, + attribute_description: attribute.description, + category_name: attribute.security_category.name + } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/app/services/security/attributes/update_project_attributes_service.rb b/ee/app/services/security/attributes/update_project_attributes_service.rb index be504d2f5d1a63c174ae53987e115a9d9d4cbf6a..b00037055447d542c45dfaff03aa0e633e3dde1f 100644 --- a/ee/app/services/security/attributes/update_project_attributes_service.rb +++ b/ee/app/services/security/attributes/update_project_attributes_service.rb @@ -7,6 +7,8 @@ class UpdateProjectAttributesService < BaseService MAX_PROJECT_ATTRIBUTES = 20 MAX_ATTRIBUTES = MAX_PROJECT_ATTRIBUTES * 2 + ATTACHED_AUDIT_EVENT_NAME = 'security_attribute_attached_to_project' + DETACHED_AUDIT_EVENT_NAME = 'security_attribute_detached_from_project' def initialize(project:, current_user:, params:) @project = project @@ -28,6 +30,7 @@ def execute return error_response('Invalid attributes', errors: validation_errors) if validation_errors.present? apply_changes + create_audit_events ServiceResponse.success(payload: { project: project.reset, @@ -172,6 +175,80 @@ def validate_one_attribute_per_category(attribute) validation_errors << "Cannot add multiple attributes from the same category #{category_id}" end + + def create_audit_events + if associations_to_create.any? + ::Gitlab::Audit::Auditor.audit(attached_audit_context) do + associations_to_create.each do |association| + event = build_attached_audit_event(association) + ::Gitlab::Audit::EventQueue.push(event) + end + end + end + + return if associations_to_destroy.none? + + ::Gitlab::Audit::Auditor.audit(detached_audit_context) do + associations_to_destroy.each do |association| + event = build_detached_audit_event(association) + ::Gitlab::Audit::EventQueue.push(event) + end + end + end + + def attached_audit_context + { + author: current_user, + scope: project, + target: project, + name: ATTACHED_AUDIT_EVENT_NAME + } + end + + def detached_audit_context + { + author: current_user, + scope: project, + target: project, + name: DETACHED_AUDIT_EVENT_NAME + } + end + + def build_attached_audit_event(association) + attribute = association.security_attribute + AuditEvents::BuildService.new( + author: current_user, + scope: project, + target: attribute, + created_at: now, + message: "Attached security attribute #{attribute.name} to project #{project.name}", + additional_details: { + event_name: ATTACHED_AUDIT_EVENT_NAME, + attribute_name: attribute.name, + category_name: attribute.security_category.name, + project_name: project.name, + project_path: project.full_path + } + ).execute + end + + def build_detached_audit_event(association) + attribute = association.security_attribute + AuditEvents::BuildService.new( + author: current_user, + scope: project, + target: attribute, + created_at: now, + message: "Detached security attribute #{attribute.name} from project #{project.name}", + additional_details: { + event_name: DETACHED_AUDIT_EVENT_NAME, + attribute_name: attribute.name, + category_name: attribute.security_category.name, + project_name: project.name, + project_path: project.full_path + } + ).execute + end end end end diff --git a/ee/app/services/security/attributes/update_service.rb b/ee/app/services/security/attributes/update_service.rb index 053f4aff3c3df3deb0cb9f3836f591e2fdc6fbbd..3cf0ec177f9222f20af096ad0122418db2210164 100644 --- a/ee/app/services/security/attributes/update_service.rb +++ b/ee/app/services/security/attributes/update_service.rb @@ -3,6 +3,8 @@ module Security module Attributes class UpdateService < BaseService + AUDIT_EVENT_NAME = 'security_attribute_updated' + def initialize(attribute:, params:, current_user:) @attribute = attribute @root_namespace = attribute.namespace&.root_ancestor @@ -19,7 +21,13 @@ def execute return ServiceResponse.error(message: 'Cannot update non editable attribute') unless attribute.editable? attribute.assign_attributes(params.slice(:name, :description, :color)) - attribute.save ? success : error + + if attribute.save + create_audit_event + success + else + error + end end attr_reader :attribute, :root_namespace, :params, :current_user @@ -40,6 +48,29 @@ def error message += ": #{errors.join(', ')}" if errors.any? ServiceResponse.error(message: message, payload: errors) end + + def create_audit_event + audit_context = { + name: AUDIT_EVENT_NAME, + author: current_user, + scope: root_namespace, + target: attribute, + message: "Updated security attribute #{attribute.name}", + additional_details: { + attribute_name: attribute.name, + attribute_description: attribute.description, + attribute_color: attribute.color.to_s, + category_name: attribute.security_category.name, + previous_values: { + name: attribute.name_previously_was, + description: attribute.description_previously_was, + color: attribute.color_previously_was.to_s + } + } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/app/services/security/categories/create_service.rb b/ee/app/services/security/categories/create_service.rb index 019bc1d93cbf54cefb79b572cacf67bd7b58f1a3..7e691f4a8a8b7b60671d0c20aeaeb232e1a30705 100644 --- a/ee/app/services/security/categories/create_service.rb +++ b/ee/app/services/security/categories/create_service.rb @@ -30,6 +30,7 @@ def execute ) return error unless category.save + create_audit_event success end @@ -48,6 +49,24 @@ def success def error ServiceResponse.error(message: _('Failed to create security category'), payload: category.errors) end + + def create_audit_event + audit_context = { + name: 'security_category_created', + author: current_user, + scope: root_namespace, + target: category, + message: "Created security category #{category.name}", + additional_details: { + category_name: category.name, + category_description: category.description, + multiple_selection: category.multiple_selection, + template_type: category.template_type + } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/app/services/security/categories/destroy_service.rb b/ee/app/services/security/categories/destroy_service.rb index a87526448c4dc78cc185f0b73220c0c1c95d94d6..b026999f47729e262ab32fb733c1c9f61ede2526 100644 --- a/ee/app/services/security/categories/destroy_service.rb +++ b/ee/app/services/security/categories/destroy_service.rb @@ -19,6 +19,8 @@ def execute [attribute.id, attribute.to_global_id] end + create_audit_event + Security::Category.transaction do category.security_attributes.destroy_all # rubocop:disable Cop/DestroyAll -- Need destroy callbacks to trigger worker for project association cleanup category.destroy @@ -66,6 +68,23 @@ def not_editable_error def deletion_failed_error(error_message) error_response("Failed to delete category: #{error_message}") end + + def create_audit_event + audit_context = { + name: 'security_category_deleted', + author: current_user, + scope: category.namespace, + target: category, + message: "Deleted security category #{category.name}", + additional_details: { + category_name: category.name, + category_description: category.description, + attributes_count: category.security_attributes.count + } + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/app/services/security/categories/update_service.rb b/ee/app/services/security/categories/update_service.rb index bfc1281d294ae47a6f82ccde48e3093908a87263..f560ea0d5dd1311c17993e1ed1daecec3a653788 100644 --- a/ee/app/services/security/categories/update_service.rb +++ b/ee/app/services/security/categories/update_service.rb @@ -20,6 +20,7 @@ def execute update_params = params.except(:namespace, :editable_state, :template_type, :multiple_selection) return error unless update_params.present? && category.update(update_params) + create_audit_event(update_params) success end @@ -44,6 +45,22 @@ def error message += ": #{category.errors.full_messages.join(', ')}" if category.errors.present? ServiceResponse.error(message: message, payload: category.errors) end + + def create_audit_event(update_params) + audit_context = { + name: 'security_category_updated', + author: current_user, + scope: category.namespace, + target: category, + message: "Updated security category #{category.name}", + additional_details: { + category_name: category.name, + updated_fields: update_params.keys + }.merge(update_params) + } + + ::Gitlab::Audit::Auditor.audit(audit_context) + end end end end diff --git a/ee/config/audit_events/types/security_attribute_attached_to_project.yml b/ee/config/audit_events/types/security_attribute_attached_to_project.yml new file mode 100644 index 0000000000000000000000000000000000000000..28c4f8a8a5513566afa7404d4b0c3a865fd7ae6c --- /dev/null +++ b/ee/config/audit_events/types/security_attribute_attached_to_project.yml @@ -0,0 +1,10 @@ +--- +name: security_attribute_attached_to_project +description: A security attribute is attached to a project +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Project] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_attribute_created.yml b/ee/config/audit_events/types/security_attribute_created.yml new file mode 100644 index 0000000000000000000000000000000000000000..23be286dfbf87c4f804e3ce4a34f16b44ec22853 --- /dev/null +++ b/ee/config/audit_events/types/security_attribute_created.yml @@ -0,0 +1,10 @@ +--- +name: security_attribute_created +description: A security attribute is created +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_attribute_deleted.yml b/ee/config/audit_events/types/security_attribute_deleted.yml new file mode 100644 index 0000000000000000000000000000000000000000..29b13349a4018cca6e60586b42c9eaa0f5d6ce51 --- /dev/null +++ b/ee/config/audit_events/types/security_attribute_deleted.yml @@ -0,0 +1,10 @@ +--- +name: security_attribute_deleted +description: A security attribute is deleted +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_attribute_detached_from_project.yml b/ee/config/audit_events/types/security_attribute_detached_from_project.yml new file mode 100644 index 0000000000000000000000000000000000000000..14d96841033b8586e6e50361a1621627b2675fda --- /dev/null +++ b/ee/config/audit_events/types/security_attribute_detached_from_project.yml @@ -0,0 +1,10 @@ +--- +name: security_attribute_detached_from_project +description: A security attribute is detached from a project +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Project] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_attribute_updated.yml b/ee/config/audit_events/types/security_attribute_updated.yml new file mode 100644 index 0000000000000000000000000000000000000000..3bd15fb3bd6a0aac64a5a9a93193e8cb3df88c1c --- /dev/null +++ b/ee/config/audit_events/types/security_attribute_updated.yml @@ -0,0 +1,10 @@ +--- +name: security_attribute_updated +description: A security attribute is updated +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_category_created.yml b/ee/config/audit_events/types/security_category_created.yml new file mode 100644 index 0000000000000000000000000000000000000000..feb78b8b9f34a58f41406b34ce92f159f69649af --- /dev/null +++ b/ee/config/audit_events/types/security_category_created.yml @@ -0,0 +1,10 @@ +--- +name: security_category_created +description: A security category is created +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_category_deleted.yml b/ee/config/audit_events/types/security_category_deleted.yml new file mode 100644 index 0000000000000000000000000000000000000000..1a5755f8f06556c3345c49a9d021fab841f21118 --- /dev/null +++ b/ee/config/audit_events/types/security_category_deleted.yml @@ -0,0 +1,10 @@ +--- +name: security_category_deleted +description: A security category is deleted +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/ee/config/audit_events/types/security_category_updated.yml b/ee/config/audit_events/types/security_category_updated.yml new file mode 100644 index 0000000000000000000000000000000000000000..4905690c75ff74f9dc8dacadceae757cb1dd0d51 --- /dev/null +++ b/ee/config/audit_events/types/security_category_updated.yml @@ -0,0 +1,10 @@ +--- +name: security_category_updated +description: A security category is updated +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/568959 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/209118 +feature_category: security_risk_management +milestone: '18.6' +saved_to_database: true +streamed: true +scope: [Group] \ No newline at end of file diff --git a/ee/spec/services/security/attributes/create_service_spec.rb b/ee/spec/services/security/attributes/create_service_spec.rb index 63d4f399ec6a4e8cbce67a8630ce9b32b8b39538..91a91f83d90d547dbb5eba9e0a75e5f66fcaa2f3 100644 --- a/ee/spec/services/security/attributes/create_service_spec.rb +++ b/ee/spec/services/security/attributes/create_service_spec.rb @@ -82,6 +82,44 @@ expect(execute).to be_success end + it 'creates audit events for each attribute', :request_store do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with({ + author: user, + scope: namespace, + target: namespace, + name: 'security_attribute_created' + }).and_call_original + + expect(::Gitlab::Audit::EventQueue).to receive(:push).twice.with( + satisfy { |event| security_attribute_created_event?(event) } + ).and_call_original + + expect { execute }.to change { AuditEvent.count }.by(2) + + audit_events = AuditEvent.last(2) + audit_events.each do |audit_event| + expect(audit_event.details).to include( + event_name: 'security_attribute_created', + author_name: user.name, + category_name: category.name + ) + end + + expect(audit_events.first.details).to include( + custom_message: 'Created security attribute Critical', + attribute_name: 'Critical', + attribute_description: 'Critical security level', + attribute_color: '#FF0000' + ) + + expect(audit_events.second.details).to include( + custom_message: 'Created security attribute High', + attribute_name: 'High', + attribute_description: 'High security level', + attribute_color: '#FF8C00' + ) + end + context 'when attribute validation fails' do let(:params) { { attributes: [{ name: '', description: 'Test description', color: '#FF0000' }] } } @@ -172,7 +210,50 @@ expect(execute.message).to include("Description can't be blank") end end + + context 'when category is not editable' do + before do + category.update!(editable_state: :locked) + end + + it 'returns non-editable error' do + expect(execute).to be_error + expect(execute.message).to eq("You can not edit this category's attributes.") + end + end + + context 'when category save fails' do + before do + # Mock the category to fail validation but allow other methods + allow(category).to receive_messages(valid?: true, save: false) + allow(category.errors).to receive(:full_messages).and_return(['Category validation failed']) + end + + it 'returns error with category errors' do + expect(execute).to be_error + expect(execute.message).to include('Failed to create security attributes') + expect(execute.message).to include('Category validation failed') + end + end + + context 'when no attributes are provided' do + let(:params) { { attributes: [] } } + + it 'succeeds with empty attributes' do + expect(execute).to be_success + expect(execute.payload[:attributes]).to be_empty + end + + it 'does not create audit events when no attributes' do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + expect(execute).to be_success + end + end end end end + + def security_attribute_created_event?(event) + event.is_a?(AuditEvent) && event.details[:event_name] == 'security_attribute_created' + end end diff --git a/ee/spec/services/security/attributes/destroy_service_spec.rb b/ee/spec/services/security/attributes/destroy_service_spec.rb index 3780187f0349fce3834ba6d47538cc81914e6a4d..7a22268004f1fcbb58e776e742d0922f302c2aef 100644 --- a/ee/spec/services/security/attributes/destroy_service_spec.rb +++ b/ee/spec/services/security/attributes/destroy_service_spec.rb @@ -81,6 +81,25 @@ execute end + it 'creates an audit event' do + expect { execute }.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.last + expect(audit_event.details).to include( + event_name: 'security_attribute_deleted', + author_name: user.name, + custom_message: "Deleted security attribute #{attribute.name}", + attribute_name: attribute.name, + attribute_description: attribute.description, + category_name: category.name + ) + end + + it 'does not create an audit event when deletion fails' do + allow(attribute).to receive(:destroy).and_return(false) + expect { execute }.not_to change { AuditEvent.count } + end + it 'does not enqueue worker when deletion fails' do allow(attribute).to receive(:destroy).and_return(false) diff --git a/ee/spec/services/security/attributes/update_project_attributes_service_spec.rb b/ee/spec/services/security/attributes/update_project_attributes_service_spec.rb index 5d8a8290f11fed5fa4ef22ba1681d7120aa15a4d..e69fc166f7ec77f0ea49c7ccb9fbd59e8ec08bb4 100644 --- a/ee/spec/services/security/attributes/update_project_attributes_service_spec.rb +++ b/ee/spec/services/security/attributes/update_project_attributes_service_spec.rb @@ -176,6 +176,43 @@ def expect_error_with_payload_errors(*errors) expect(execute.payload[:added_count]).to eq(2) expect(execute.payload[:removed_count]).to eq(0) end + + it 'creates audit events for attached attributes', :request_store do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with({ + author: user, + scope: project, + target: project, + name: 'security_attribute_attached_to_project' + }).and_call_original + + expect(::Gitlab::Audit::EventQueue).to receive(:push).twice.with( + satisfy { |event| security_attribute_attached_event?(event) } + ).and_call_original + + expect { execute }.to change { AuditEvent.count }.by(2) + + audit_events = AuditEvent.last(2) + audit_events.each do |audit_event| + expect(audit_event.details).to include( + event_name: 'security_attribute_attached_to_project', + author_name: user.name, + project_name: project.name, + project_path: project.full_path + ) + end + + expect(audit_events.first.details).to include( + custom_message: "Attached security attribute #{attribute1.name} to project #{project.name}", + attribute_name: attribute1.name, + category_name: single_selection_category.name + ) + + expect(audit_events.second.details).to include( + custom_message: "Attached security attribute #{other_attribute.name} to project #{project.name}", + attribute_name: other_attribute.name, + category_name: other_category.name + ) + end end context 'when removing attributes' do @@ -196,6 +233,32 @@ def expect_error_with_payload_errors(*errors) expect(execute.payload[:added_count]).to eq(0) expect(execute.payload[:removed_count]).to eq(1) end + + it 'creates audit event for detached attribute', :request_store do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with({ + author: user, + scope: project, + target: project, + name: 'security_attribute_detached_from_project' + }).and_call_original + + expect(::Gitlab::Audit::EventQueue).to receive(:push).once.with( + satisfy { |event| security_attribute_detached_event?(event) } + ).and_call_original + + expect { execute }.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.last + expect(audit_event.details).to include( + event_name: 'security_attribute_detached_from_project', + author_name: user.name, + custom_message: "Detached security attribute #{other_attribute.name} from project #{project.name}", + attribute_name: other_attribute.name, + category_name: other_category.name, + project_name: project.name, + project_path: project.full_path + ) + end end context 'when attribute does not exist' do @@ -390,4 +453,12 @@ def expect_error_with_payload_errors(*errors) it_behaves_like 'does not change project security attributes count' end end + + def security_attribute_attached_event?(event) + event.is_a?(AuditEvent) && event.details[:event_name] == 'security_attribute_attached_to_project' + end + + def security_attribute_detached_event?(event) + event.is_a?(AuditEvent) && event.details[:event_name] == 'security_attribute_detached_from_project' + end end diff --git a/ee/spec/services/security/attributes/update_service_spec.rb b/ee/spec/services/security/attributes/update_service_spec.rb index 840370414342e8fdb319a16f020b4f072f8f507d..34cfdbadf32d73081cf8913a7e5bd26444269aad 100644 --- a/ee/spec/services/security/attributes/update_service_spec.rb +++ b/ee/spec/services/security/attributes/update_service_spec.rb @@ -66,6 +66,26 @@ expect(attribute.color).to eq(::Gitlab::Color.of('#00FF00')) end + it 'creates an audit event' do + expect { execute }.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.last + expect(audit_event.details).to include( + event_name: 'security_attribute_updated', + author_name: user.name, + custom_message: "Updated security attribute Updated Name", + attribute_name: 'Updated Name', + attribute_description: 'Updated Description', + attribute_color: '#00FF00', + category_name: category.name, + previous_values: { + name: 'Original Name', + description: 'Original Description', + color: '#FF0000' + } + ) + end + context 'when attribute is not editable' do let(:attribute) do create(:security_attribute, security_category: category, namespace: namespace, editable_state: :locked) diff --git a/ee/spec/services/security/categories/create_service_spec.rb b/ee/spec/services/security/categories/create_service_spec.rb index 25e970b19531b9254b74a92a19ab951bc700a227..964c6b2d961512c3022dd3547e93b88d69a20124 100644 --- a/ee/spec/services/security/categories/create_service_spec.rb +++ b/ee/spec/services/security/categories/create_service_spec.rb @@ -174,6 +174,20 @@ execute end + + it 'creates an audit event' do + expect { execute }.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.last + expect(audit_event.details).to include( + event_name: 'security_category_created', + author_name: current_user.name, + custom_message: "Created security category #{params[:name]}", + category_name: params[:name], + category_description: params[:description], + multiple_selection: params[:multiple_selection] + ) + end end context 'when name already exists in the same namespace' do diff --git a/ee/spec/services/security/categories/destroy_service_spec.rb b/ee/spec/services/security/categories/destroy_service_spec.rb index 331cb1b686a1cf58e5a6e5c9273bfcfb4bfc37b3..5963ff8cbb4543344b63f71630dca43da0036106 100644 --- a/ee/spec/services/security/categories/destroy_service_spec.rb +++ b/ee/spec/services/security/categories/destroy_service_spec.rb @@ -107,6 +107,21 @@ let(:test_category) { category } include_examples 'successful category deletion' + + it 'creates an audit event' do + expect { execute }.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.last + + expect(audit_event.details).to include( + event_name: 'security_category_deleted', + author_name: user.name, + custom_message: "Deleted security category #{category.name}", + category_name: category.name, + category_description: category.description, + attributes_count: 1 + ) + end end context 'when deletion fails due to database error' do diff --git a/ee/spec/services/security/categories/update_service_spec.rb b/ee/spec/services/security/categories/update_service_spec.rb index dae1c508ce63dcefceefc41196ce83db0d4a6f99..d02727b3c0aef41b2a4f0c4ef58be0dda4bba364 100644 --- a/ee/spec/services/security/categories/update_service_spec.rb +++ b/ee/spec/services/security/categories/update_service_spec.rb @@ -126,6 +126,22 @@ result = execute expect(result.payload[:category]).to eq(category) end + + it 'creates an audit event' do + expect { execute }.to change { AuditEvent.count }.by(1) + + audit_event = AuditEvent.last + + expect(audit_event.details).to include( + event_name: 'security_category_updated', + author_name: current_user.name, + custom_message: "Updated security category #{params[:name]}", + category_name: params[:name], + updated_fields: %i[name description], + name: params[:name], + description: params[:description] + ) + end end context 'when updating only specific fields' do