diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 981416224f204ab9a413bf8737290b1b933eff4f..f61d6ad4c5dfb6768fec08fa56462668081d4e9f 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -959,6 +959,8 @@ - 1 - - security_pipeline_execution_policies_run_schedule - 1 +- - security_policies_failed_pipelines_audit + - 1 - - security_policies_project_transfer - 1 - - security_process_scan_result_policy diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 78db3be8cffe18c644b17296d3ce560ab2f230ff..6209c0579dda4a14db0b38b4791b66c39539353a 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -534,6 +534,7 @@ Audit event types belong to the following product categories. | Type name | Event triggered when | Saved to database | Introduced in | Scope | |:----------|:---------------------|:------------------|:--------------|:------| +| [`policy_pipeline_failed`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196628) | A pipeline with security policy jobs failed | {{< icon name="dotted-circle" >}} No | GitLab [18.2](https://gitlab.com/gitlab-org/gitlab/-/issues/539232) | Project | | [`policy_project_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102154) | The security policy project is updated for a project | {{< icon name="check-circle" >}} Yes | GitLab [15.6](https://gitlab.com/gitlab-org/gitlab/-/issues/377877) | Group, Project | | [`security_policy_create`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192797) | A security policy is created | {{< icon name="check-circle" >}} Yes | GitLab [18.1](https://gitlab.com/gitlab-org/gitlab/-/issues/539230) | Project | | [`security_policy_delete`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192797) | A security policy is deleted | {{< icon name="check-circle" >}} Yes | GitLab [18.1](https://gitlab.com/gitlab-org/gitlab/-/issues/539230) | Project | diff --git a/ee/app/models/concerns/security/pipeline_execution_policy.rb b/ee/app/models/concerns/security/pipeline_execution_policy.rb index ca411c2ee8fdea6352a85a46044529d71077f01a..8e99c10ac1168e083abb8c3a867765400d35452b 100644 --- a/ee/app/models/concerns/security/pipeline_execution_policy.rb +++ b/ee/app/models/concerns/security/pipeline_execution_policy.rb @@ -6,6 +6,10 @@ def active_pipeline_execution_policies pipeline_execution_policy.select { |config| config[:enabled] }.first(policy_limit) end + def active_pipeline_execution_policy_names + active_pipeline_execution_policies.pluck(:name) # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- not an ActiveRecord model and active_pipeline_execution_policies has limit + end + def pipeline_execution_policy policy_by_type(:pipeline_execution_policy) end diff --git a/ee/app/models/concerns/security/scan_execution_policy.rb b/ee/app/models/concerns/security/scan_execution_policy.rb index c9b51b8661e5245787251dc6b112ee4b847d932c..a8f58ff13baf863e72ef6dc1f89fbe114acac9f8 100644 --- a/ee/app/models/concerns/security/scan_execution_policy.rb +++ b/ee/app/models/concerns/security/scan_execution_policy.rb @@ -63,6 +63,10 @@ def active_policies_for_project(ref, project, pipeline_source = nil) .select { |policy| applicable_for_pipeline_source?(block_given? ? yield(policy[:rules]) : policy[:rules], pipeline_source) } end + def active_scan_execution_policy_names(ref, project) + active_policies_for_project(ref, project).pluck(:name) # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- not an ActiveRecord model and active_scan_execution_policies has limit + end + def active_pipeline_policies_for_project(ref, project, pipeline_source = nil) active_policies_for_project(ref, project, pipeline_source) do |policy_rules| policy_rules.select { |rule| rule[:type] == RULE_TYPES[:pipeline] } diff --git a/ee/app/models/ee/ci/pipeline.rb b/ee/app/models/ee/ci/pipeline.rb index f5877c7e4f4524c34aace4f4ebc1daabb98e8e85..78b9252f1cacc86fc1f2c24e22ebd5c2abc3577b 100644 --- a/ee/app/models/ee/ci/pipeline.rb +++ b/ee/app/models/ee/ci/pipeline.rb @@ -57,6 +57,9 @@ module Pipeline cyclonedx: %i[dependency_scanning container_scanning] }.freeze + PIPELINE_EXECUTION_POLICIES_BUILD_SOURCES = %w[pipeline_execution_policy scan_execution_policy].freeze + SECURITY_POLICIES_PIPELINE_SOURCES = %w[security_orchestration_policy security_policies_default_source pipeline_execution_policy_forced pipeline_execution_policy_schedule].freeze + def self.latest_limited_pipeline_ids_per_source(pipelines, sha) pipelines_for_sha = pipelines.complete_or_manual.for_sha(sha).order(id: :desc).limit(LATEST_PIPELINES_LIMIT) @@ -127,6 +130,14 @@ def self.latest_limited_pipeline_ids_per_source(pipelines, sha) Security::PipelineAnalyzersStatusUpdateWorker.perform_async(pipeline.id) if pipeline.default_branch? end end + + after_transition any => :failed do |pipeline| + pipeline.run_after_commit do + if should_audit_security_policy_pipeline_failure?(pipeline) + Security::Policies::FailedPipelinesAuditWorker.perform_async(pipeline.id) + end + end + end end end @@ -345,6 +356,19 @@ def security_builds def sbom_report_ingestion_errors_redis_key "sbom_report_ingestion_errors/#{id}" end + + def should_audit_security_policy_pipeline_failure?(pipeline) + ::Feature.enabled?(:collect_security_policy_failed_pipelines_audit_events, pipeline.project) && + (pipeline_created_by_security_policies?(pipeline.source) || pipeline_with_security_policy_jobs?) + end + + def pipeline_created_by_security_policies?(source) + SECURITY_POLICIES_PIPELINE_SOURCES.include?(source) + end + + def pipeline_with_security_policy_jobs? + builds.any? { |build| PIPELINE_EXECUTION_POLICIES_BUILD_SOURCES.include?(build.source) } + end end end end diff --git a/ee/app/workers/all_queues.yml b/ee/app/workers/all_queues.yml index fd039a95e7b8875f2ed225fd1eb31bac36030ddc..43efeef690d500c21c03ec591dad163e7c058fb4 100644 --- a/ee/app/workers/all_queues.yml +++ b/ee/app/workers/all_queues.yml @@ -3584,6 +3584,16 @@ :idempotent: true :tags: [] :queue_namespace: +- :name: security_policies_failed_pipelines_audit + :worker_name: Security::Policies::FailedPipelinesAuditWorker + :feature_category: :security_policy_management + :has_external_dependencies: true + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] + :queue_namespace: - :name: security_policies_project_transfer :worker_name: Security::Policies::ProjectTransferWorker :feature_category: :security_policy_management diff --git a/ee/app/workers/security/policies/failed_pipelines_audit_worker.rb b/ee/app/workers/security/policies/failed_pipelines_audit_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..11ccb1c03c89505a605e2e3822b94f37e4de3f83 --- /dev/null +++ b/ee/app/workers/security/policies/failed_pipelines_audit_worker.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Security + module Policies + class FailedPipelinesAuditWorker + include ApplicationWorker + + data_consistency :sticky + + feature_category :security_policy_management + urgency :low + idempotent! + deduplicate :until_executed + defer_on_database_health_signal :gitlab_main, [:project_audit_events], 1.minute + + # Audit stream to external destination with HTTP request if configured + worker_has_external_dependencies! + + def perform(pipeline_id) + pipeline = Ci::Pipeline.find_by_id(pipeline_id) + return unless pipeline + return unless pipeline.project.licensed_feature_available?(:security_orchestration_policies) + + Security::SecurityOrchestrationPolicies::PipelineFailedAuditor.new(pipeline: pipeline).audit + end + end + end +end diff --git a/ee/config/audit_events/types/policy_pipeline_failed.yml b/ee/config/audit_events/types/policy_pipeline_failed.yml new file mode 100644 index 0000000000000000000000000000000000000000..bd4c8fa9539a8d31f85efd522bf4c9a096ae6866 --- /dev/null +++ b/ee/config/audit_events/types/policy_pipeline_failed.yml @@ -0,0 +1,10 @@ +--- +name: policy_pipeline_failed +description: A pipeline with security policy jobs failed +introduced_by_issue: https://gitlab.com/gitlab-org/gitlab/-/issues/539232 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196628 +feature_category: security_policy_management +milestone: '18.2' +saved_to_database: false +streamed: true +scope: [Project] diff --git a/ee/config/feature_flags/gitlab_com_derisk/collect_security_policy_failed_pipelines_audit_events.yml b/ee/config/feature_flags/gitlab_com_derisk/collect_security_policy_failed_pipelines_audit_events.yml new file mode 100644 index 0000000000000000000000000000000000000000..b242b0c4e5344ededb8782a756c76e91b241584d --- /dev/null +++ b/ee/config/feature_flags/gitlab_com_derisk/collect_security_policy_failed_pipelines_audit_events.yml @@ -0,0 +1,10 @@ +--- +name: collect_security_policy_failed_pipelines_audit_events +description: Collects audit events for failed pipelines with security policy jobs or pipelines created by security policies. +feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/539232 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196628 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/554064 +milestone: '18.2' +group: group::security policies +type: gitlab_com_derisk +default_enabled: false diff --git a/ee/lib/security/security_orchestration_policies/pipeline_auditor.rb b/ee/lib/security/security_orchestration_policies/pipeline_auditor.rb new file mode 100644 index 0000000000000000000000000000000000000000..7facd657700e567c001402a89736f209e794f31b --- /dev/null +++ b/ee/lib/security/security_orchestration_policies/pipeline_auditor.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +module Security + module SecurityOrchestrationPolicies + class PipelineAuditor + include Gitlab::Utils::StrongMemoize + + def initialize(pipeline:) + @pipeline = pipeline + end + + def audit + return unless pipeline + return unless security_orchestration_policy_configurations.present? + + security_orchestration_policy_configurations.each do |policy_configuration| + skipped_policies = skipped_policies(policy_configuration) + + next if skipped_policies.blank? + + ::Gitlab::Audit::Auditor.audit(audit_context(policy_configuration, skipped_policies)) + end + end + + private + + attr_reader :pipeline + + def event_name + raise NoMethodError, "#{self.class} must implement the method #{__method__}" + end + + def event_message + raise NoMethodError, "#{self.class} must implement the method #{__method__}" + end + + def audit_context(policy_configuration, skipped_policies) + { + name: event_name, + author: pipeline_author, + scope: policy_configuration.security_policy_management_project, + target: pipeline, + target_details: pipeline.id.to_s, + message: event_message, + additional_details: additional_details(skipped_policies) + } + end + + def additional_details(skipped_policies) + additional_details_base.merge({ skipped_policies: skipped_policies }) + end + + def additional_details_base + { + commit_sha: pipeline.sha, + merge_request_title: merge_request&.title, + merge_request_id: merge_request&.id, + merge_request_iid: merge_request&.iid, + source_branch: merge_request&.source_branch, + target_branch: merge_request&.target_branch, + project_id: project.id, + project_name: project.name, + project_full_path: project.full_path + }.compact + end + strong_memoize_attr :additional_details_base + + def skipped_policies(policy_configuration) + pipeline_execution_policies, scan_execution_policies = active_execution_policies(policy_configuration) + + skipped_seps = format_skipped_policies(scan_execution_policies, 'scan_execution_policy') + skipped_peps = format_skipped_policies(pipeline_execution_policies, 'pipeline_execution_policy') + + skipped_seps + skipped_peps + end + + def active_execution_policies(policy_configuration) + active_seps_for_policy = if target_branch_ref + policy_configuration.active_scan_execution_policy_names(target_branch_ref, project) + else + [] + end + + active_peps_for_policy = policy_configuration.active_pipeline_execution_policy_names || [] + + [active_peps_for_policy, active_seps_for_policy] + end + + def format_skipped_policies(policies, type) + policies.map { |name| { name: name, policy_type: type } } + end + + def security_orchestration_policy_configurations + project&.all_security_orchestration_policy_configurations + end + strong_memoize_attr :security_orchestration_policy_configurations + + def project + pipeline.project + end + strong_memoize_attr :project + + def merge_request + pipeline.all_merge_requests.first + end + strong_memoize_attr :merge_request + + def target_branch_ref + merge_request&.target_branch_ref + end + strong_memoize_attr :target_branch_ref + + def pipeline_author + pipeline.user || Gitlab::Audit::DeletedAuthor.new(id: -4, name: 'Unknown User') + end + strong_memoize_attr :pipeline_author + end + end +end diff --git a/ee/lib/security/security_orchestration_policies/pipeline_failed_auditor.rb b/ee/lib/security/security_orchestration_policies/pipeline_failed_auditor.rb new file mode 100644 index 0000000000000000000000000000000000000000..c885c9aa8361a2dc5cf7bbaac622b2a03b499ca4 --- /dev/null +++ b/ee/lib/security/security_orchestration_policies/pipeline_failed_auditor.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Security + module SecurityOrchestrationPolicies + class PipelineFailedAuditor < PipelineAuditor + private + + def event_name + 'policy_pipeline_failed' + end + + def event_message + "Pipeline: #{pipeline.id} created by security policies or with security policy jobs failed" + end + end + end +end diff --git a/ee/lib/security/security_orchestration_policies/pipeline_skipped_auditor.rb b/ee/lib/security/security_orchestration_policies/pipeline_skipped_auditor.rb new file mode 100644 index 0000000000000000000000000000000000000000..59f47780f9b8727001bb63033cb904d87a2e0312 --- /dev/null +++ b/ee/lib/security/security_orchestration_policies/pipeline_skipped_auditor.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Security + module SecurityOrchestrationPolicies + class PipelineSkippedAuditor < PipelineAuditor + private + + def event_name + 'policy_pipeline_skipped' + end + + def event_message + "Pipeline: #{pipeline.id} with security policy jobs skipped" + end + end + end +end diff --git a/ee/spec/lib/security/security_orchestration_policies/pipeline_failed_auditor_spec.rb b/ee/spec/lib/security/security_orchestration_policies/pipeline_failed_auditor_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..bc2db51c611c24aef2d2c44da5ca325b931b08ba --- /dev/null +++ b/ee/spec/lib/security/security_orchestration_policies/pipeline_failed_auditor_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::SecurityOrchestrationPolicies::PipelineFailedAuditor, feature_category: :security_policy_management do + it_behaves_like 'pipeline auditor' do + let(:event_name) { 'policy_pipeline_failed' } + let(:event_message) { "Pipeline: #{pipeline.id} created by security policies or with security policy jobs failed" } + end +end diff --git a/ee/spec/lib/security/security_orchestration_policies/pipeline_skipped_auditor_spec.rb b/ee/spec/lib/security/security_orchestration_policies/pipeline_skipped_auditor_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..eb8941327f69b648991e7131ec9054ba6f1defe1 --- /dev/null +++ b/ee/spec/lib/security/security_orchestration_policies/pipeline_skipped_auditor_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::SecurityOrchestrationPolicies::PipelineSkippedAuditor, feature_category: :security_policy_management do + it_behaves_like 'pipeline auditor' do + let(:event_name) { 'policy_pipeline_skipped' } + let(:event_message) { "Pipeline: #{pipeline.id} with security policy jobs skipped" } + end +end diff --git a/ee/spec/models/ci/pipeline_spec.rb b/ee/spec/models/ci/pipeline_spec.rb index e5d80af0b46c22ed0cf848f3cd31f6190f9dc787..2bf44bf3a4ef37c79782f1aae12a0f3aced96c46 100644 --- a/ee/spec/models/ci/pipeline_spec.rb +++ b/ee/spec/models/ci/pipeline_spec.rb @@ -696,6 +696,65 @@ end end end + + context 'Security::Policies::FailedPipelinesAuditWorker' do + let(:source) { :push } + let(:pipeline) { create(:ci_empty_pipeline, project: project, status: from_status, source: source) } + let(:from_status) { Ci::HasStatus::ACTIVE_STATUSES[-1] } + + context 'on pipeline failed' do + subject(:transition_pipeline) { pipeline.drop } + + shared_examples 'does not enqueue FailedPipelinesAuditWorker' do + specify do + expect(Security::Policies::FailedPipelinesAuditWorker).not_to receive(:perform_async).with(pipeline.id) + + transition_pipeline + end + end + + shared_examples 'enqueues FailedPipelinesAuditWorker' do + specify do + expect(Security::Policies::FailedPipelinesAuditWorker).to receive(:perform_async).with(pipeline.id) + + transition_pipeline + end + end + + context 'when the feature flag `collect_security_policy_skipped_pipelines_audit_events` is disabled' do + before do + stub_feature_flags(collect_security_policy_failed_pipelines_audit_events: false) + end + + it_behaves_like 'does not enqueue FailedPipelinesAuditWorker' + end + + context 'when the pipeline was not created by a security policy' do + let(:source) { :web } + + context 'when the pipeline has no builds created by security policies' do + it_behaves_like 'does not enqueue FailedPipelinesAuditWorker' + end + + context 'when the pipeline has builds created by security policies' do + let!(:build) { create(:ci_build, pipeline: pipeline, project: project) } + let!(:build_source) { create(:ci_build_source, build: build, source: 'scan_execution_policy') } + + it_behaves_like 'enqueues FailedPipelinesAuditWorker' + end + end + + context 'when the pipeline was created by a security policy' do + let(:source) { :security_orchestration_policy } + + it 'enqueue FailedPipelinesAuditWorker' do + expect(Security::Policies::FailedPipelinesAuditWorker).to receive(:perform_async).with(pipeline.id) + + transition_pipeline + end + end + end + end end describe '#latest_merged_result_pipeline?' do diff --git a/ee/spec/support/shared_examples/lib/security/security_orchestration_policies/pipeline_auditor_shared_examples.rb b/ee/spec/support/shared_examples/lib/security/security_orchestration_policies/pipeline_auditor_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..89dc8287fc5fa50fecbd395925592ede07326553 --- /dev/null +++ b/ee/spec/support/shared_examples/lib/security/security_orchestration_policies/pipeline_auditor_shared_examples.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.shared_examples_for 'pipeline auditor' do + let_it_be(:project) { build(:project) } + + describe '#audit' do + subject(:audit) { described_class.new(pipeline: pipeline).audit } + + shared_examples 'does not call Gitlab::Audit::Auditor' do + specify do + expect(::Gitlab::Audit::Auditor).not_to receive(:audit) + + audit + end + end + + context 'when pipeline is nil' do + let_it_be(:pipeline) { nil } + + it_behaves_like 'does not call Gitlab::Audit::Auditor' + end + + context 'when the pipeline is present' do + let_it_be(:user) { build(:user) } + let_it_be(:pipeline) { build(:ci_pipeline, project: project, user: user) } + + context 'when there is no security_orchestration_policy_configuration assigned to project' do + it_behaves_like 'does not call Gitlab::Audit::Auditor' + end + + context 'when there is a security_orchestration_policy_configuration assigned to project' do + let_it_be(:security_policy_management_project) { build(:project) } + let_it_be(:security_orchestration_policy_configuration) do + build(:security_orchestration_policy_configuration, project: project, + security_policy_management_project: security_policy_management_project) + end + + let_it_be(:all_policies) { [security_orchestration_policy_configuration] } + + before do + allow(project).to receive(:all_security_orchestration_policy_configurations).and_return( + all_policies) + + allow(security_orchestration_policy_configuration).to receive(:active_scan_execution_policy_names).with( + merge_request&.target_branch_ref, project).and_return(active_scan_execution_policy_names) + + allow(security_orchestration_policy_configuration).to receive(:active_pipeline_execution_policy_names) + .and_return(active_pipeline_execution_policy_names) + end + + context 'when there are no active policies' do + let(:active_scan_execution_policy_names) { [] } + let(:active_pipeline_execution_policy_names) { [] } + let(:merge_request) { nil } + + it_behaves_like 'does not call Gitlab::Audit::Auditor' + end + + context 'when there are active policies' do + shared_examples_for 'calls Gitlab::Audit::Auditor.audit with the expected context' do + specify do + expect(::Gitlab::Audit::Auditor).to receive(:audit).with( + hash_including( + { + name: event_name, + author: user, + scope: security_policy_management_project, + target: pipeline, + target_details: pipeline.id.to_s, + message: event_message, + additional_details: additional_details + } + ) + ) + + audit + end + end + + shared_examples_for 'when the merge_request is present' do + context 'when the merge_request is present' do + let_it_be(:merge_request) do + build(:merge_request, id: 1, iid: 1, source_project: project, target_project: project) + end + + let(:additional_details) do + { + commit_sha: pipeline.sha, + merge_request_title: merge_request.title, + merge_request_id: merge_request.id, + merge_request_iid: merge_request.iid, + source_branch: merge_request.source_branch, + target_branch: merge_request.target_branch, + project_id: project.id, + project_name: project.name, + project_full_path: project.full_path, + skipped_policies: skipped_policies_details + } + end + + before do + pipeline.merge_request = merge_request + + merge_requests_relation = instance_double(ActiveRecord::Relation) + + allow(pipeline).to receive(:all_merge_requests).and_return(merge_requests_relation) + allow(merge_requests_relation).to receive(:first).and_return(merge_request) + end + + it_behaves_like 'calls Gitlab::Audit::Auditor.audit with the expected context' + end + end + + context 'when there are active scan_execution_policies policies' do + let(:skipped_policy_name) { 'Skipped sep policy' } + let(:skipped_policies_details) { [{ name: skipped_policy_name, policy_type: 'scan_execution_policy' }] } + + let(:active_scan_execution_policy_names) { [skipped_policy_name] } + let(:active_pipeline_execution_policy_names) { [] } + + it_behaves_like 'when the merge_request is present' + end + + context 'when there are active pipeline_execution_policies policies' do + let(:skipped_policy_name) { 'Skipped pep policy' } + let(:skipped_policies_details) { [{ name: skipped_policy_name, policy_type: 'pipeline_execution_policy' }] } + + let(:active_scan_execution_policy_names) { [] } + let(:active_pipeline_execution_policy_names) { [skipped_policy_name] } + + it_behaves_like 'when the merge_request is present' + + context 'when merge_request is nil' do + let(:merge_request) { nil } + let(:additional_details) do + { + commit_sha: pipeline.sha, + project_id: project.id, + project_name: project.name, + project_full_path: project.full_path, + skipped_policies: skipped_policies_details + } + end + + before do + pipeline.merge_request = merge_request + end + + it_behaves_like 'calls Gitlab::Audit::Auditor.audit with the expected context' + end + end + + context 'when there are active scan_execution and pipeline_execution policies' do + let(:skipped_sep_policy_name) { 'Skipped sep policy' } + let(:skipped_pep_policy_name) { 'Skipped pep policy' } + let(:skipped_policies_details) do + [{ name: skipped_sep_policy_name, policy_type: 'scan_execution_policy' }, + { name: skipped_pep_policy_name, + policy_type: 'pipeline_execution_policy' }] + end + + let(:active_scan_execution_policy_names) { [skipped_sep_policy_name] } + let(:active_pipeline_execution_policy_names) { [skipped_pep_policy_name] } + + it_behaves_like 'when the merge_request is present' + + context 'when there are inherited security_orchestration_policy_configurations' do + let_it_be(:inherited_security_policy_management_project) { build(:project) } + let(:inherited_skipped_sep_policy_name) { 'Inherited skipped sep policy' } + let(:inherited_skipped_pep_policy_name) { 'Inherited skipped pep policy' } + let(:inherited_active_scan_execution_policy_names) { [inherited_skipped_sep_policy_name] } + let(:inherited_active_pipeline_execution_policy_names) { [inherited_skipped_pep_policy_name] } + let(:inherited_skipped_policies_details) do + [{ name: inherited_skipped_sep_policy_name, policy_type: 'scan_execution_policy' }, + { name: inherited_skipped_pep_policy_name, + policy_type: 'pipeline_execution_policy' }] + end + + let_it_be(:parent_group) { build(:group) } + let_it_be(:inherited_security_orchestration_policy_configuration) do + build(:security_orchestration_policy_configuration, namespace: parent_group, + security_policy_management_project: inherited_security_policy_management_project) + end + + let_it_be(:all_policies) do + [security_orchestration_policy_configuration, inherited_security_orchestration_policy_configuration] + end + + before do + allow(inherited_security_orchestration_policy_configuration) + .to receive(:active_scan_execution_policy_names).with( + merge_request&.target_branch_ref, project).and_return(inherited_active_scan_execution_policy_names) + + allow(inherited_security_orchestration_policy_configuration) + .to receive(:active_pipeline_execution_policy_names) + .and_return(inherited_active_pipeline_execution_policy_names) + end + + context 'when the merge_request is present' do + let_it_be(:merge_request) do + build(:merge_request, id: 1, iid: 1, source_project: project, target_project: project) + end + + let(:additional_details) do + { + commit_sha: pipeline.sha, + merge_request_title: merge_request.title, + merge_request_id: merge_request.id, + merge_request_iid: merge_request.iid, + source_branch: merge_request.source_branch, + target_branch: merge_request.target_branch, + project_id: project.id, + project_name: project.name, + project_full_path: project.full_path + } + end + + before do + pipeline.merge_request = merge_request + + merge_requests_relation = instance_double(ActiveRecord::Relation) + + allow(pipeline).to receive(:all_merge_requests).and_return(merge_requests_relation) + allow(merge_requests_relation).to receive(:first).and_return(merge_request) + end + + it 'calls Gitlab::Audit::Auditor.audit for each policy configuration with the expected context' do + security_policy_projects = [security_policy_management_project, + inherited_security_policy_management_project] + additional_details_including_policies = [ + additional_details.merge({ skipped_policies: skipped_policies_details }), + additional_details.merge({ skipped_policies: inherited_skipped_policies_details }) + ] + + all_policies.size.times do |index| + expect(::Gitlab::Audit::Auditor).to receive(:audit).with( + hash_including( + { + name: event_name, + author: user, + scope: security_policy_projects[index], + target: pipeline, + target_details: pipeline.id.to_s, + message: event_message, + additional_details: additional_details_including_policies[index] + } + ) + ) + end + + audit + end + end + end + end + end + end + end + end +end diff --git a/ee/spec/workers/security/policies/failed_pipelines_audit_worker_spec.rb b/ee/spec/workers/security/policies/failed_pipelines_audit_worker_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3b32c002c98e6e87792141463adf6ea28d741973 --- /dev/null +++ b/ee/spec/workers/security/policies/failed_pipelines_audit_worker_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::Policies::FailedPipelinesAuditWorker, feature_category: :security_policy_management do + describe '#perform' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + subject(:run_worker) { described_class.new.perform(pipeline_id) } + + shared_examples_for 'does not call PipelineFailedAuditor' do + specify do + expect(Security::SecurityOrchestrationPolicies::PipelineFailedAuditor).not_to receive(:new) + + run_worker + end + end + + context 'when pipeline is not found' do + let(:pipeline_id) { non_existing_record_id } + + it_behaves_like 'does not call PipelineFailedAuditor' + end + + context 'when pipeline exist' do + let(:pipeline_id) { pipeline.id } + + context 'when security_orchestration_policies feature is available' do + before do + stub_licensed_features(security_orchestration_policies: true) + end + + it 'calls PipelineFailedAuditor' do + expect_next_instance_of(Security::SecurityOrchestrationPolicies::PipelineFailedAuditor, + pipeline: pipeline) do |auditor| + expect(auditor).to receive(:audit) + end + + run_worker + end + + it_behaves_like 'an idempotent worker' do + let(:job_args) { pipeline.id } + end + end + + context 'when security_orchestration_policies feature is not available' do + let(:pipeline_id) { pipeline.id } + + before do + stub_licensed_features(security_orchestration_policies: false) + end + + it_behaves_like 'does not call PipelineFailedAuditor' + end + end + end +end