Skip to content

BE: Apply policy based on configured policy scope

Why are we doing this work

In the scope of Security Policy Scopes (&5510 - closed), we would like to allow users to save Policy YAML with policy scope in it. Based on designs and descriptions from the Epic, we would like to be able to specify policy scope based on:

  • assigned compliance frameworks,
  • list with included projects,
  • list with excluded projects.
policy_scope:
  compliance_frameworks:
  - id: 12345
  - id: 23456
  projects:
    including:
    - id: 12345
    - id: 23456
    excluding:
    - id: 34567
    - id: 45678

In scope of this issue we want to add logic needed to include/exclude given Scan Execution Policy/Scan Result Policy based on policy_scope configuration from policy YAML. We should ensure that we are applying policy with selected Compliance Framework only when Compliance Framework belongs to the same Group as Security Policy Project.

Relevant links

Non-functional requirements

  • Documentation: add information to documentation about the possibility of specifying policy scope with information that it is currently disabled with a feature flag,
  • Feature flag: new feature flag security_policies_policy_scope needs to be added to be able to enable/disable this feature,
  • Performance:
  • Testing:

Implementation plan

  • MR1: backend add new service ee/app/services/security/security_orchestration_policies/policy_scope_service.rb with logic needed to support new policy_scope settings and new feature flag entry,
# frozen_string_literal: true

module Security
  module SecurityOrchestrationPolicies
    class PolicyScopeService < BaseProjectService

      def policy_applicable?(policy)
        Feature.enabled?(:security_policies_policy_scope, project) &&
          applicable_for_compliance_framework?(policy) &&
          applicable_for_project?(policy)
      end

      private

      def applicable_for_compliance_framework?(policy)
        policy_scope_compliance_frameworks = policy.dig(:policy_scope, :compliance_frameworks).to_a
        compliance_framework_id = project.compliance_framework_setting&.framework_id

        return true if policy_scope_compliance_frameworks.blank?
        return false if compliance_framework_id.nil?
        return true if policy_scope_compliance_frameworks.any? { |framework| framework[:id] == compliance_framework_id }

        false
      end

      def applicable_for_project?(policy)
        policy_scope_included_projects = policy.dig(:policy_scope, :projects, :including).to_a
        policy_scope_excluded_projects = policy.dig(:policy_scope, :projects, :excluding).to_a

        return false if policy_scope_excluded_projects.any? { |project| project[:id] == project.id }
        return true if policy_scope_included_projects.blank?
        return true if policy_scope_included_projects.any? { |project| project[:id] == project.id }

        false
      end
    end
  end
end
  • MR2: backend use newly added service whenever we ask for active_scan_result_policies or active_scan_execution_policies:
diff --git a/ee/app/services/security/security_orchestration_policies/protected_branches_deletion_check_service.rb b/ee/app/services/security/security_orchestration_policies/protected_branches_deletion_check_service.rb
index 3aa8a805c2a8..f81c0d927fee 100644
--- a/ee/app/services/security/security_orchestration_policies/protected_branches_deletion_check_service.rb
+++ b/ee/app/services/security/security_orchestration_policies/protected_branches_deletion_check_service.rb
@@ -19,13 +19,25 @@ def applicable_branches
 
       def rules
         project.all_security_orchestration_policy_configurations.flat_map do |config|
-          blocking_policies = config.active_scan_result_policies.select do |rule|
+          blocking_policies = applicable_scan_result_policies(config).select do |rule|
             rule.dig(:approval_settings, :block_unprotecting_branches)
           end
 
           blocking_policies.pluck(:rules).flatten # rubocop: disable CodeReuse/ActiveRecord
         end
       end
+
+      def applicable_scan_result_policies(config)
+        config
+          .active_scan_result_policies
+          .select { |policy| policy_applicable?(policy) }
+      end
+
+      def policy_applicable?(policy)
+        Security::SecurityOrchestrationPolicies::PolicyScopeService
+          .new(project: project)
+          .policy_applicable?(policy)
+      end
     end
   end
 end
diff --git a/ee/app/workers/security/process_scan_result_policy_worker.rb b/ee/app/workers/security/process_scan_result_policy_worker.rb
index 660fb423650e..4213a9b643c2 100644
--- a/ee/app/workers/security/process_scan_result_policy_worker.rb
+++ b/ee/app/workers/security/process_scan_result_policy_worker.rb
@@ -16,9 +16,7 @@ def perform(project_id, configuration_id)
       configuration = Security::OrchestrationPolicyConfiguration.find_by_id(configuration_id)
       return unless project && configuration
 
-      active_scan_result_policies = configuration.active_scan_result_policies
-
-      sync_policies(project, configuration, active_scan_result_policies)
+      sync_policies(project, configuration, applicable_active_policies(configuration))
 
       if Feature.enabled?(:sync_mr_approval_rules_security_policies, project)
         Security::ScanResultPolicies::SyncOpenedMergeRequestsWorker.perform_async(project_id, configuration_id)
@@ -31,6 +29,18 @@ def perform(project_id, configuration_id)
 
     private
 
+    def applicable_active_policies(configuration, project)
+      configuration
+        .active_scan_result_policies
+        .select { |policy| policy_applicable?(project, policy) }
+    end
+
+    def policy_applicable?(project, policy)
+      Security::SecurityOrchestrationPolicies::PolicyScopeService
+        .new(project: project)
+        .policy_applicable?(policy)
+    end
+
     def sync_policies(project, configuration, active_scan_result_policies)
       configuration.delete_scan_finding_rules_for_project(project.id)
       configuration.delete_software_license_policies(project)
diff --git a/ee/app/workers/security/scan_execution_policies/rule_schedule_worker.rb b/ee/app/workers/security/scan_execution_policies/rule_schedule_worker.rb
index d115e0fec97b..f559bf097dc3 100644
--- a/ee/app/workers/security/scan_execution_policies/rule_schedule_worker.rb
+++ b/ee/app/workers/security/scan_execution_policies/rule_schedule_worker.rb
@@ -19,10 +19,18 @@ def perform(project_id, user_id, rule_schedule_id)
         schedule = Security::OrchestrationPolicyRuleSchedule.find_by_id(rule_schedule_id)
         return unless schedule
 
+        return unless policy_applicable?(project, schedule.policy)
+
         Security::SecurityOrchestrationPolicies::RuleScheduleService
           .new(project: project, current_user: user)
           .execute(schedule)
       end
+
+      def policy_applicable?(project, policy)
+        Security::SecurityOrchestrationPolicies::PolicyScopeService
+          .new(project: project)
+          .policy_applicable?(policy)
+      end
     end
   end
 end
diff --git a/ee/lib/ee/gitlab/checks/security/policy_check.rb b/ee/lib/ee/gitlab/checks/security/policy_check.rb
index 25efa301a695..b8d1ba924808 100644
--- a/ee/lib/ee/gitlab/checks/security/policy_check.rb
+++ b/ee/lib/ee/gitlab/checks/security/policy_check.rb
@@ -24,7 +24,7 @@ def branch_name_affected_by_policy?
             configurations = project.all_security_orchestration_policy_configurations
             return if configurations.empty?
 
-            active_policies = configurations.flat_map(&:active_scan_result_policies)
+            active_policies = applicable_active_policies(configurations)
             return if active_policies.empty?
 
             rules = active_policies.pluck(:rules).flatten # rubocop: disable CodeReuse/ActiveRecord
@@ -37,6 +37,18 @@ def branch_name_affected_by_policy?
           def force_push?
             ::Gitlab::Checks::ForcePush.force_push?(project, oldrev, newrev)
           end
+
+          def applicable_active_policies
+            configurations
+              .flat_map(&:active_scan_result_policies)
+              .select { |policy| policy_applicable?(policy) }
+          end
+
+          def policy_applicable?(policy)
+            Security::SecurityOrchestrationPolicies::PolicyScopeService
+              .new(project: project)
+              .policy_applicable?(policy)
+          end
         end
       end
     end
diff --git a/ee/lib/gitlab/ci/project_config/security_policy_default.rb b/ee/lib/gitlab/ci/project_config/security_policy_default.rb
index 0ab6116ac9f3..9ff38e650bef 100644
--- a/ee/lib/gitlab/ci/project_config/security_policy_default.rb
+++ b/ee/lib/gitlab/ci/project_config/security_policy_default.rb
@@ -26,7 +26,13 @@ def active_scan_execution_policies?
             .new(@project).all
             .to_a
             .flat_map(&:active_scan_execution_policies_for_pipelines)
-            .any?
+            .any? { |policy| policy_applicable?(policy) }
+        end
+
+        def policy_applicable?(project)
+          Security::SecurityOrchestrationPolicies::PolicyScopeService
+            .new(project: project)
+            .policy_applicable?(policy)
         end
       end
     end

Verification steps

  1. In your main group with Ultimate license, create 2 compliance frameworks (go to Group's Settings -> Compliance Frameworks): compliance-framework-1 and compliance-framework-2
  2. Create a subgroup in this group
  3. Create three projects: project-without-framework, project-with-framework-1, project-with-framework-2.
  4. Assign created compliance frameworks accordingly (Project's Settings -> Compliance Framework)
  5. In GraphQL Explorer, gather IDs of created compliance frameworks:
query {
  group(fullPath: "govern-team-test") {
    complianceFrameworks {
      nodes {
        id
        name
      }
    }
  }
}
  1. In your subgroup create 4 policies:
---
scan_execution_policy:
- name: Policy applicable for all projects except one
  description: ''
  enabled: true
  policy_scope:
    projects:
      excluding:
      - id: PROJECT_WITH_SECOND_FRAMEWORK_ID
  rules:
  - type: pipeline
    branches:
    - "*"
  actions:
  - scan: container_scanning
    variables:
      CS_IMAGE: all-projects-except-project-test:1.0.0
- name: Policy applicable for single project only
  description: ''
  enabled: true
  policy_scope:
    projects:
      including:
      - id: PROJECT_WITH_SECOND_FRAMEWORK_ID
  rules:
  - type: pipeline
    branches:
    - "*"
  actions:
  - scan: container_scanning
    variables:
      CS_IMAGE: single-project-test:2.0.0
- name: Policy applicable for single compliance framework only
  description: ''
  enabled: true
  policy_scope:
    compliance_frameworks:
    - id: FIRST_FRAMEWORK_ID
  rules:
  - type: pipeline
    branches:
    - "*"
  actions:
  - scan: container_scanning
    variables:
      CS_IMAGE: project-with-compliance-framework-test:3.0.0
- name: Policy applicable for second compliance framework only
  description: ''
  enabled: true
  policy_scope:
    compliance_frameworks:
    - id: SECOND_FRAMEWORK_ID
  rules:
  - type: pipeline
    branches:
    - "*"
  actions:
  - scan: container_scanning
    variables:
      CS_IMAGE: project-with-second-compliance-framework-test:4.0.0
  1. For each project Run Pipeline, go to errored container-scanning-X jobs, see logs, and verify that proper jobs were executed (you should see that every Container Scanning job attempted to scan the container provided with CS_IMAGE).
Edited by Alan (Maciej) Paruszewski