Match rule schedules to scan execution policies by enabled position
What does this MR do and why?
Fixes the lookup between scan execution rule schedules and scan execution policies when a disabled policy comes first.
SyncPoliciesService builds OrchestrationPolicyRuleSchedule records from active_scan_execution_policies. That list holds only enabled policies, capped at the per-configuration limit. So a rule schedule's policy_index is the policy's position among enabled scan execution policies. OrchestrationPolicyRuleSchedule#policy already reads active_scan_execution_policies.at(policy_index), which matches.
security_policies.policy_index is different. It counts every scan execution policy in the YAML, enabled or not. Two lookups compared the two indexes directly:
Security::Policy.for_rule_schedule.PipelineCreation#security_policy_foruses it to record thePolicySchedulePipelinerow for each scheduled scan pipeline.RuleScheduleService#find_security_policyuses it in the self-heal that creates per-project schedules.OrchestrationPolicyRuleSchedule.for_scan_execution_policy.ScanExecutionPolicies::CreateProjectSchedulesServiceuses it.
When a disabled scan execution policy comes before an enabled one, both lookups return the wrong policy.
- With the per-project schedule flags off, scheduled scan pipelines are recorded against the wrong policy, for example the disabled one.
- With the WIP flags on, per-project schedules are created from the wrong rules, or not at all.
The fix:
- Add
Security::Policy#rule_schedule_index. It returns the policy's position among enabled, undeleted scan execution policies. It returns nil for disabled policies, deleted policies, and other policy types. for_scan_execution_policyusesrule_schedule_index.for_rule_scheduleselects enabled, undeleted scan execution policies ordered by index, then appliesoffset(rule_schedule.policy_index).limit(1).
What rule schedules store does not change.
Rollout: no migration and no feature flag. for_scan_execution_policy runs one extra COUNT query per call.
References
No linked issue.
Related feature issue: #592731 (closed)
Screenshots or screen recordings
Not applicable. No UI change. The before/after behavior is in the table below.
| Before | After | |
|---|---|---|
Rule schedule (cron 0 1 * * *) resolves to |
Disabled policy | Enabled policy |
for_scan_execution_policy(disabled) |
["0 1 * * *"] |
[] |
for_scan_execution_policy(enabled) |
[] |
["0 1 * * *"] |
How to set up and validate locally
- Create a security policy project. In
.gitlab/security-policies/policy.yml, define two scan execution policies withschedulerules.- The first has
enabled: falseand cadence0 0 * * *. - The second has
enabled: trueand cadence0 1 * * *.
- The first has
- Link the policy project to a project and let the policy sync run.
- In the Rails console, with
configset to the project's security orchestration policy configuration, run:config.rule_schedules.map { |rs| Security::Policy.for_rule_schedule(rs).first&.name } config.security_policies.type_scan_execution_policy.map { |p| [p.name, Security::OrchestrationPolicyRuleSchedule.for_scan_execution_policy(p).pluck(:cron)] } - The rule schedule with cron
0 1 * * *now resolves to the enabled policy.for_scan_execution_policyreturns[]for the disabled policy and["0 1 * * *"]for the enabled one.
Database
No migration. Plans are EXPLAIN (ANALYZE, BUFFERS) on the production-size gitlab-production-main thin clone, run through postgres.ai, using the configuration with the most scan execution policies (17 rows in security_policies). Each query is bounded by one policy configuration, which is capped per type.
Security::Policy#rule_schedule_index (new COUNT)
SELECT COUNT(*) FROM "security_policies" WHERE "security_policies"."type" = 1 AND "security_policies"."security_orchestration_policy_configuration_id" = 3183009 AND "security_policies"."enabled" = TRUE AND "security_policies"."policy_index" >= 0 AND "security_policies"."policy_index" < 12Query plan
Aggregate (cost=3.45..3.46 rows=1 width=8) (actual time=0.311..0.312 rows=1 loops=1)
Buffers: shared hit=20 read=1
I/O Timings: read=0.137 write=0.000
-> Index Scan using index_security_policies_on_unique_config_type_policy_index on public.security_policies (cost=0.42..3.44 rows=1 width=0) (actual time=0.301..0.307 rows=1 loops=1)
Index Cond: ((security_policies.security_orchestration_policy_configuration_id = 3183009) AND (security_policies.type = 1) AND (security_policies.policy_index >= 0) AND (security_policies.policy_index < 12))
Filter: security_policies.enabled
Rows Removed by Filter: 11
Buffers: shared hit=20 read=1
I/O Timings: read=0.137 write=0.000Security::Policy.for_rule_schedule (changed scope)
SELECT "security_policies".* FROM "security_policies" WHERE "security_policies"."type" = 1 AND "security_policies"."security_orchestration_policy_configuration_id" = 3183009 AND (policy_index >= 0) AND "security_policies"."enabled" = TRUE ORDER BY "security_policies"."policy_index" ASC LIMIT 1 OFFSET 8Query plan
Limit (cost=3.44..6.46 rows=1 width=560) (actual time=0.105..0.106 rows=0 loops=1)
Buffers: shared hit=26
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_security_policies_on_unique_config_type_policy_index on public.security_policies (cost=0.42..3.44 rows=1 width=560) (actual time=0.079..0.104 rows=5 loops=1)
Index Cond: ((security_policies.security_orchestration_policy_configuration_id = 3183009) AND (security_policies.type = 1) AND (security_policies.policy_index >= 0))
Filter: security_policies.enabled
Rows Removed by Filter: 12
Buffers: shared hit=26
I/O Timings: read=0.000 write=0.000OrchestrationPolicyRuleSchedule.for_scan_execution_policy (unchanged SQL, policy_index value now comes from rule_schedule_index)
SELECT "security_orchestration_policy_rule_schedules".* FROM "security_orchestration_policy_rule_schedules" WHERE "security_orchestration_policy_rule_schedules"."security_orchestration_policy_configuration_id" = 3183009 AND "security_orchestration_policy_rule_schedules"."policy_index" = 8 AND "security_orchestration_policy_rule_schedules"."policy_type" = 0Query plan
Index Scan using index_sop_schedules_on_sop_configuration_id on public.security_orchestration_policy_rule_schedules (cost=0.28..4.81 rows=1 width=84) (actual time=4.252..4.253 rows=0 loops=1)
Index Cond: (security_orchestration_policy_rule_schedules.security_orchestration_policy_configuration_id = 3183009)
Filter: ((security_orchestration_policy_rule_schedules.policy_index = 8) AND (security_orchestration_policy_rule_schedules.policy_type = 0))
Rows Removed by Filter: 2
Buffers: shared hit=3 read=4 dirtied=2
WAL: records=4 fpi=2 bytes=15598
I/O Timings: read=3.475 write=0.000MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.