Skip to content

Add role_approvers to scan result policy

Sashi Kumar Kumaresan requested to merge sk/379060-add-role-approvers into master

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

Screenshot_2023-03-14_at_1.10.05_AM

Project members

Screenshot_2023-03-14_at_1.09.46_AM

MR Approval

Without FF With FF
Screenshot_2023-03-14_at_1.06.57_AM Screenshot_2023-03-14_at_1.09.34_AM

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.

Edited by Sashi Kumar Kumaresan

Merge request reports