Draft: Add security scan profile attach mutation
What does this MR do and why?
Introduces bulk mutations to attach security scan profiles to projects. This MR includes:
-
securityScanProfileAttachGraphQL mutation for attaching profiles to multiple projects and groups -
Security::ScanProfiles::AttachServicefor handling attachment logic -
Security::ScanProfiles::FindOrCreateServicefor creating GitLab-recommended profiles -
apply_security_scan_profilespermission for authorization -
security_scan_profiles_featurefeature flag
The mutation supports both template based profile ids (e.g., gid://gitlab/Security::ScanProfile/secret_detection) and persisted profile ids for attaching existing custom profiles.
Changelog: added
EE: true
How to set up and validate locally
Setup
- Enable the
security_scan_profilesfeature flag:Feature.enable(:security_scan_profiles_feature) - Select a root group
rgwhere you have at least maintainer permissions. - Select (or create) two projects
p1andp2underrg.
Test 1: Attach template-based profile and verify creation
- Use this GraphQL mutation to attach a template-based secret detection profile to
p1:mutation { securityScanProfileAttach( input: { securityScanProfileId: "gid://gitlab/Security::ScanProfile/secret_detection" projectIds: ["gid://gitlab/Project/<P1_ID>"] } ) { errors } } - Query
p1to verify the GitLab-recommended profile was created and attached:{ project(fullPath: "<p1_full_path>") { securityScanProfiles { id name description scanType gitlabRecommended } } } - Verify the response includes a profile with:
gitlabRecommended: true
Test 2: Attach persisted profile to another project
- Copy the profile
idfrom Test 1, Step 2. - Use this GraphQL mutation to attach the same profile to
p2:mutation { securityScanProfileAttach( input: { securityScanProfileId: "<PROFILE_ID_FROM_TEST_1>" projectIds: ["gid://gitlab/Project/<P2_ID>"] } ) { errors } } - Query
p2to verify the profile was attached.
Test 3: Bulk attachment to multiple projects
- Use this GraphQL mutation to attach the profile to both projects in a single request:
mutation { securityScanProfileAttach( input: { securityScanProfileId: "<PROFILE_ID>" projectIds: ["gid://gitlab/Project/<P1_ID>", "gid://gitlab/Project/<P2_ID>"] } ) { errors } } - Verify the mutation creates no duplicates and returns with empty errors.
Query plans
insert_all:
SQL
INSERT INTO "security_scan_profiles_projects"
("project_id", "security_scan_profile_id", "created_at", "updated_at")
VALUES
(22, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(23, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(24, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(25, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(26, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(27, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(28, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(29, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(30, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040'),
(31, 1, '2025-12-11 15:49:47.538040', '2025-12-11 15:49:47.538040')
ON CONFLICT ("project_id","security_scan_profile_id") DO NOTHING
RETURNING "id";
Query plan
See details here
ModifyTable on public.security_scan_profiles_projects (cost=0.00..0.15 rows=10 width=40) (actual time=0.227..0.333 rows=10 loops=1)
Buffers: shared hit=107
WAL: records=50 fpi=0 bytes=3430
-> Values Scan on "*VALUES*" (cost=0.00..0.15 rows=10 width=40) (actual time=0.089..0.100 rows=10 loops=1)
Buffers: shared hit=26
Trigger RI_ConstraintTrigger_c_4226359321 for constraint fk_rails_36ece30d24: time=0.864 calls=10
Settings: seq_page_cost = '4', effective_cache_size = '338688MB', random_page_cost = '1.5', work_mem = '100MB', jit = 'off'
by_scan_profile:
SQL
SELECT
"security_scan_profiles_projects".*
FROM
"security_scan_profiles_projects"
WHERE
"security_scan_profiles_projects"."security_scan_profile_id" = 1
Query plan
See details here
Index Scan using idx_security_scan_profiles_projects_on_security_scan_profile_id on public.security_scan_profiles_projects (cost=0.14..3.16 rows=1 width=40) (actual time=0.034..0.035 rows=0 loops=1)
Index Cond: (security_scan_profiles_projects.security_scan_profile_id = 1)
Buffers: shared hit=6
Settings: effective_cache_size = '338688MB', random_page_cost = '1.5', work_mem = '100MB', jit = 'off', seq_page_cost = '4'
by_project:
SQL
SELECT
"security_scan_profiles_projects".*
FROM
"security_scan_profiles_projects"
WHERE
"security_scan_profiles_projects"."project_id" IN (22, 23, 24)
Query plan
See details here
Index Scan using index_security_scan_profiles_projects_on_unique_project_profile on public.security_scan_profiles_projects (cost=0.14..3.19 rows=3 width=40) (actual time=0.020..0.021 rows=3 loops=1)
Index Cond: (security_scan_profiles_projects.project_id = ANY ('{22,23,24}'::bigint[]))
Buffers: shared hit=2
Settings: seq_page_cost = '4', effective_cache_size = '338688MB', random_page_cost = '1.5', work_mem = '100MB', jit = 'off'
MR 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.
Related to [Backend] Add mutation to bulk apply and remove... (#582824) • Gal Katz • 18.8
Edited by Gal Katz