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 newpolicy_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
oractive_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
- In your main group with Ultimate license, create 2 compliance frameworks (go to Group's Settings -> Compliance Frameworks):
compliance-framework-1
andcompliance-framework-2
- Create a subgroup in this group
- Create three projects:
project-without-framework
,project-with-framework-1
,project-with-framework-2
. - Assign created compliance frameworks accordingly (Project's Settings -> Compliance Framework)
- In GraphQL Explorer, gather IDs of created compliance frameworks:
query {
group(fullPath: "govern-team-test") {
complianceFrameworks {
nodes {
id
name
}
}
}
}
- 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
- 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 withCS_IMAGE
).
Edited by Alan (Maciej) Paruszewski