Add role_approvers to scan result policy
What does this MR do and why?
Addresses #379060 (closed)
Scan Result Policy Action currently supports individual approvers or a group of users. However, when a customer has projects with lower criticality or the scan results detect a vulnerability of low criticality (I.e., Low/Medium), they want to require an approver from the project based on the role (I.e., owner/maintainer)
This MR adds the functionality of role_approvers
to scan result policies. This is also added to scan_result_policies
table with role_approvers
column.
Migration output
Up
main: == 20230303232426 AddRoleApproversToScanResultPolicies: migrating =============
main: -- add_column(:scan_result_policies, :role_approvers, :integer, {:array=>true, :default=>[]})
main: -> 0.0029s
main: == 20230303232426 AddRoleApproversToScanResultPolicies: migrated (0.0066s) ====
Down
main: == 20230303232426 AddRoleApproversToScanResultPolicies: reverting =============
main: -- remove_column(:scan_result_policies, :role_approvers, :integer, {:array=>true, :default=>[]})
main: -> 0.0019s
main: == 20230303232426 AddRoleApproversToScanResultPolicies: reverted (0.0071s) ====
Database query plan
SELECT
"users".*
FROM
(
(
SELECT
"users".*
FROM
"users"
INNER JOIN
"approval_merge_request_rules_users"
ON "users"."id" = "approval_merge_request_rules_users"."user_id"
WHERE
"approval_merge_request_rules_users"."approval_merge_request_rule_id" = 86980413
)
UNION
(
SELECT
DISTINCT "users".*
FROM
"users"
INNER JOIN
"members"
ON "users"."id" = "members"."user_id"
INNER JOIN
"namespaces"
ON "members"."source_id" = "namespaces"."id"
INNER JOIN
"approval_merge_request_rules_groups"
ON "namespaces"."id" = "approval_merge_request_rules_groups"."group_id"
WHERE
"members"."type" = 'GroupMember'
AND "namespaces"."type" = 'Group'
AND "approval_merge_request_rules_groups"."approval_merge_request_rule_id" = 86980413
AND "members"."source_type" = 'Namespace'
AND "members"."requested_at" IS NULL
AND "members"."access_level" != 5
)
UNION
(
SELECT
"users".*
FROM
"users"
INNER JOIN
"project_authorizations"
ON "users"."id" = "project_authorizations"."user_id"
WHERE
"project_authorizations"."project_id" = 43370987
AND "project_authorizations"."access_level" IN (
10, 30
)
)
) users
WHERE
"users"."id" != 8021015
Plan: console.postgres.ai
Collapsed
HashAggregate (cost=5632.59..5641.95 rows=936 width=10300) (actual time=8.521..9.820 rows=537 loops=1)
Group Key: users.id, users.email, users.encrypted_password, users.reset_password_token, users.reset_password_sent_at, users.remember_created_at, users.sign_in_count, users.current_sign_in_at, users.last_sign_in_at, users.current_sign_in_ip, users.last_sign_in_ip, users.created_at, users.updated_at, users.name, users.admin, users.projects_limit, users.failed_attempts, users.locked_at, users.username, users.can_create_group, users.can_create_team, users.state, users.color_scheme_id, users.password_expires_at, users.created_by_id, users.avatar, users.confirmation_token, users.confirmed_at, users.confirmation_sent_at, users.unconfirmed_email, users.hide_no_ssh_key, users.last_credential_check_at, users.admin_email_unsubscribed_at, users.notification_email, users.hide_no_password, users.password_automatically_set, users.public_email, users.encrypted_otp_secret, users.encrypted_otp_secret_iv, users.encrypted_otp_secret_salt, users.otp_required_for_login, users.otp_backup_codes, users.dashboard, users.project_view, users.consumed_timestep, users.layout, users.hide_project_limit, users.unlock_token, users.note, users.otp_grace_period_started_at, users.external, users.incoming_email_token, users.auditor, users.require_two_factor_authentication_from_group, users.two_factor_grace_period, users.notified_of_own_activity, users.last_activity_on, users.preferred_language, users.email_opted_in, users.email_opted_in_ip, users.email_opted_in_source_id, users.email_opted_in_at, users.theme_id, users.accepted_term_id, users.feed_token, users.private_profile, users.roadmap_layout, users.include_private_contributions, users.commit_email, users.group_view, users.managing_group_id, users.first_name, users.last_name, users.static_object_token, users.role, users.user_type, users.static_object_token_encrypted, users.otp_secret_expires_at, users.onboarding_in_progress
Buffers: shared hit=2729
I/O Timings: read=0.000 write=0.000
-> Append (cost=1.01..5447.73 rows=936 width=10300) (actual time=0.280..5.444 rows=537 loops=1)
Buffers: shared hit=2729
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.01..636.93 rows=182 width=1450) (actual time=0.068..0.070 rows=0 loops=1)
Buffers: shared hit=7
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_approval_merge_request_rules_users_1 on public.approval_merge_request_rules_users (cost=0.57..8.12 rows=182 width=4) (actual time=0.068..0.068 rows=0 loops=1)
Index Cond: (approval_merge_request_rules_users.approval_merge_request_rule_id = 86980413)
Heap Fetches: 0
Buffers: shared hit=7
I/O Timings: read=0.000 write=0.000
-> Index Scan using users_pkey on public.users (cost=0.43..3.46 rows=1 width=1450) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: (users.id = approval_merge_request_rules_users.user_id)
Filter: (users.id <> 8021015)
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
-> Unique (cost=11.34..11.55 rows=1 width=1450) (actual time=0.160..0.163 rows=0 loops=1)
Buffers: shared hit=26
I/O Timings: read=0.000 write=0.000
-> Sort (cost=11.34..11.35 rows=1 width=1450) (actual time=0.159..0.162 rows=0 loops=1)
Sort Key: users_1.id, users_1.email, users_1.encrypted_password, users_1.reset_password_token, users_1.reset_password_sent_at, users_1.remember_created_at, users_1.sign_in_count, users_1.current_sign_in_at, users_1.last_sign_in_at, users_1.current_sign_in_ip, users_1.last_sign_in_ip, users_1.created_at, users_1.updated_at, users_1.name, users_1.admin, users_1.projects_limit, users_1.failed_attempts, users_1.locked_at, users_1.username, users_1.can_create_group, users_1.can_create_team, users_1.state, users_1.color_scheme_id, users_1.password_expires_at, users_1.created_by_id, users_1.avatar, users_1.confirmation_token, users_1.confirmed_at, users_1.confirmation_sent_at, users_1.unconfirmed_email, users_1.hide_no_ssh_key, users_1.last_credential_check_at, users_1.admin_email_unsubscribed_at, users_1.notification_email, users_1.hide_no_password, users_1.password_automatically_set, users_1.public_email, users_1.encrypted_otp_secret, users_1.encrypted_otp_secret_iv, users_1.encrypted_otp_secret_salt, users_1.otp_required_for_login, users_1.otp_backup_codes, users_1.dashboard, users_1.project_view, users_1.consumed_timestep, users_1.layout, users_1.hide_project_limit, users_1.unlock_token, users_1.note, users_1.otp_grace_period_started_at, users_1.external, users_1.incoming_email_token, users_1.auditor, users_1.require_two_factor_authentication_from_group, users_1.two_factor_grace_period, users_1.notified_of_own_activity, users_1.last_activity_on, users_1.preferred_language, users_1.email_opted_in, users_1.email_opted_in_ip, users_1.email_opted_in_source_id, users_1.email_opted_in_at, users_1.theme_id, users_1.accepted_term_id, users_1.feed_token, users_1.private_profile, users_1.roadmap_layout, users_1.include_private_contributions, users_1.commit_email, users_1.group_view, users_1.managing_group_id, users_1.first_name, users_1.last_name, users_1.static_object_token, users_1.role, users_1.user_type, users_1.static_object_token_encrypted, users_1.otp_secret_expires_at, users_1.onboarding_in_progress
Sort Method: quicksort Memory: 25kB
Buffers: shared hit=26
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=2.00..11.33 rows=1 width=1450) (actual time=0.029..0.031 rows=0 loops=1)
Buffers: shared hit=3
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.56..10.02 rows=1 width=4) (actual time=0.029..0.030 rows=0 loops=1)
Buffers: shared hit=3
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.00..9.14 rows=1 width=8) (actual time=0.029..0.030 rows=0 loops=1)
Buffers: shared hit=3
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_approval_merge_request_rules_groups_1 on public.approval_merge_request_rules_groups (cost=0.43..3.47 rows=2 width=4) (actual time=0.028..0.028 rows=0 loops=1)
Index Cond: (approval_merge_request_rules_groups.approval_merge_request_rule_id = 86980413)
Heap Fetches: 0
Buffers: shared hit=3
I/O Timings: read=0.000 write=0.000
-> Index Only Scan using index_namespaces_on_type_and_id on public.namespaces (cost=0.56..2.83 rows=1 width=4) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: ((namespaces.type = 'Group'::text) AND (namespaces.id = approval_merge_request_rules_groups.group_id))
Heap Fetches: 0
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_members_on_source_id_and_source_type on public.members (cost=0.56..0.87 rows=1 width=8) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: ((members.source_id = namespaces.id) AND ((members.source_type)::text = 'Namespace'::text))
Filter: ((members.requested_at IS NULL) AND (members.access_level <> 5) AND ((members.type)::text = 'GroupMember'::text))
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
-> Index Scan using users_pkey on public.users users_1 (cost=0.43..1.31 rows=1 width=1450) (actual time=0.000..0.000 rows=0 loops=0)
Index Cond: (users_1.id = members.user_id)
Filter: (users_1.id <> 8021015)
Rows Removed by Filter: 0
I/O Timings: read=0.000 write=0.000
-> Nested Loop (cost=1.01..4785.21 rows=753 width=1450) (actual time=0.051..5.137 rows=537 loops=1)
Buffers: shared hit=2696
I/O Timings: read=0.000 write=0.000
-> Index Scan using index_unique_project_authorizations_on_project_id_user_id on public.project_authorizations (cost=0.57..2188.10 rows=753 width=4) (actual time=0.031..0.827 rows=537 loops=1)
Index Cond: (project_authorizations.project_id = 43370987)
Filter: (project_authorizations.access_level = ANY ('{10,30}'::integer[]))
Rows Removed by Filter: 12
Buffers: shared hit=548
I/O Timings: read=0.000 write=0.000
-> Index Scan using users_pkey on public.users users_2 (cost=0.43..3.45 rows=1 width=1450) (actual time=0.007..0.007 rows=1 loops=537)
Index Cond: (users_2.id = project_authorizations.user_id)
Filter: (users_2.id <> 8021015)
Rows Removed by Filter: 0
Buffers: shared hit=2148
I/O Timings: read=0.000 write=0.000
Time: 20.356 ms
- planning: 9.897 ms
- execution: 10.459 ms
- I/O read: 0.000 ms
- I/O write: 0.000 ms
Shared buffers:
- hits: 2729 (~21.30 MiB) from the buffer pool
- reads: 0 from the OS file cache, including disk I/O
- dirtied: 0
- writes: 0
Screenshots or screen recordings
Policy
Project members
MR Approval
Without FF | With FF |
---|---|
How to set up and validate locally
- Enable
the feature flag for the project:
Feature.enable(:scan_result_role_action, project)` - Create scan result policy in YAML mode from
Security and Compliance
->Policies
name: First Policy
description: ''
enabled: true
actions:
- type: require_approval
approvals_required: 1
group_approvers_ids:
- 76
role_approvers:
- maintainer
rules:
- type: scan_finding
branches: []
scanners: []
vulnerabilities_allowed: 0
severity_levels:
- critical
vulnerability_states:
- newly_detected
- Add few users as maintainers to the project
- Create an MR in the project that introduces a critical vulnerability. For eg, add this to
.gitlab-ci.yml
container_scanning:
image: "busybox:latest"
stage: test
allow_failure: true
artifacts:
reports:
container_scanning: gl-container-scanning-report.json
paths: [gl-container-scanning-report.json]
dependencies: []
script:
- wget -O gl-container-scanning-report.json https://gitlab.com/gitlab-org/gitlab/-/raw/master/ee/spec/fixtures/security_reports/master/gl-container-scanning-report.json
- Verify in the MR approvals the maintainers added would be in the list of approvers
MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.
-
I have evaluated the MR acceptance checklist for this MR.
Edited by Sashi Kumar Kumaresan