From e993abe241287f7bd868710ef665faa001570beb Mon Sep 17 00:00:00 2001 From: Dheeraj Joshi Date: Wed, 26 Nov 2025 20:24:23 +0530 Subject: [PATCH 1/3] Add scanner profile configuration section --- .../security_configuration/components/app.vue | 38 +++++- .../scanner_profile_configuration.vue | 124 ++++++++++++++++++ .../security_configuration/constants.js | 4 + locale/gitlab.pot | 30 +++++ 4 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 app/assets/javascripts/security_configuration/components/scanner_profile_configuration.vue diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index 7048e5b8b60baa..22251df4c2e39e 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -1,5 +1,14 @@ + + diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js index 23666abad34bb7..861839f96bfee0 100644 --- a/app/assets/javascripts/security_configuration/constants.js +++ b/app/assets/javascripts/security_configuration/constants.js @@ -165,4 +165,8 @@ export const i18n = { 'SecurityConfiguration|Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability. Please note that security training is not accessible in an environment that is offline.', ), securityTrainingDoc: s__('SecurityConfiguration|Learn more about vulnerability training'), + securityProfiles: s__('SecurityConfiguration|Profile-based scanner configuration'), + securityProfilesDesc: s__( + 'SecurityConfiguration|Define security settings once, reuse them everywhere. Update a profile and changes automatically apply to every project using it, ensuring consistent, predictable security coverage with minimal effort.', + ), }; diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ea52711dad2bde..65a4cd0ddec0b7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9102,6 +9102,12 @@ msgstr "" msgid "Apply a template" msgstr "" +msgid "Apply default profile" +msgstr "" + +msgid "Apply profile to enable" +msgstr "" + msgid "Apply selected attributes to projects (keeps existing attributes)" msgstr "" @@ -44374,6 +44380,9 @@ msgstr "" msgid "No prioritized labels yet!" msgstr "" +msgid "No profile applied" +msgstr "" + msgid "No project found" msgstr "" @@ -44557,6 +44566,9 @@ msgstr "" msgid "Not confidential" msgstr "" +msgid "Not configured" +msgstr "" + msgid "Not enabled" msgstr "" @@ -50697,6 +50709,9 @@ msgstr "" msgid "Prevent users from performing write operations while GitLab maintenance is in progress." msgstr "" +msgid "Prevents secrets from being pushed to your repository" +msgstr "" + msgid "Preview" msgstr "" @@ -51090,6 +51105,9 @@ msgstr "" msgid "ProfileSession|on" msgstr "" +msgid "Profiles define scanner configurations" +msgstr "" + msgid "ProfilesAuthentication|Add a passkey to sign in securely with your trusted device. %{link_start}Learn more about passkeys%{link_end}." msgstr "" @@ -54883,6 +54901,9 @@ msgstr "" msgid "Recently viewed" msgstr "" +msgid "Recommended" +msgstr "" + msgid "Recommended. Permanently delete an item:" msgstr "" @@ -59216,6 +59237,9 @@ msgstr "" msgid "Secret Detection" msgstr "" +msgid "Secret Push Protection" +msgstr "" + msgid "Secret detection" msgstr "" @@ -59915,6 +59939,9 @@ msgstr "" msgid "SecurityConfiguration|Customize common SAST settings to suit your requirements. Configuration changes made here override those provided by GitLab and are excluded from updates. For details of more advanced configuration options, see the %{linkStart}GitLab SAST documentation%{linkEnd}." msgstr "" +msgid "SecurityConfiguration|Define security settings once, reuse them everywhere. Update a profile and changes automatically apply to every project using it, ensuring consistent, predictable security coverage with minimal effort." +msgstr "" + msgid "SecurityConfiguration|Enable %{feature}" msgstr "" @@ -59960,6 +59987,9 @@ msgstr "" msgid "SecurityConfiguration|Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan. An enabled scanner will not be reflected as such until the pipeline has been successfully executed and it has generated valid artifacts." msgstr "" +msgid "SecurityConfiguration|Profile-based scanner configuration" +msgstr "" + msgid "SecurityConfiguration|Quickly enable all continuous testing and compliance tools by enabling %{linkStart}Auto DevOps%{linkEnd}" msgstr "" -- GitLab From 66a0d745881988b9961614ea9113d60bea7b1717 Mon Sep 17 00:00:00 2001 From: Dheeraj Joshi Date: Thu, 11 Dec 2025 12:00:32 +0530 Subject: [PATCH 2/3] Add graphql resolvers --- .../scanner_profile_configuration.vue | 263 ++++++++++++++++++ ...lable_security_scan_profiles.query.graphql | 11 + ...oject_security_scan_profiles.query.graphql | 11 + .../graphql/scan_profiles/resolvers.js | 64 +++++ .../scan_profile.fragment.graphql | 7 + ...security_scan_profile.client.query.graphql | 7 + ...urity_scan_profile_attach.mutation.graphql | 17 ++ ...urity_scan_profile_detach.mutation.graphql | 16 ++ 8 files changed, 396 insertions(+) create mode 100644 app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/group_available_security_scan_profiles.query.graphql create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/project_security_scan_profiles.query.graphql create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/scan_profile.fragment.graphql create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile.client.query.graphql create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_attach.mutation.graphql create mode 100644 app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_detach.mutation.graphql diff --git a/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue new file mode 100644 index 00000000000000..ae5a14a26e78e6 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue @@ -0,0 +1,263 @@ + + + diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/group_available_security_scan_profiles.query.graphql b/app/assets/javascripts/security_configuration/graphql/scan_profiles/group_available_security_scan_profiles.query.graphql new file mode 100644 index 00000000000000..f29dae27de9c83 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/group_available_security_scan_profiles.query.graphql @@ -0,0 +1,11 @@ +query GroupAvailableSecurityScanProfiles($fullPath: ID!, $type: SecurityScanProfileType) { + group(fullPath: $fullPath) { + id + name + availableSecurityScanProfiles(type: $type) { + ...ScanProfileFields + } + } +} + +#import "./scan_profile.fragment.graphql" diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/project_security_scan_profiles.query.graphql b/app/assets/javascripts/security_configuration/graphql/scan_profiles/project_security_scan_profiles.query.graphql new file mode 100644 index 00000000000000..446b422ef27177 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/project_security_scan_profiles.query.graphql @@ -0,0 +1,11 @@ +query ProjectSecurityScanProfiles($fullPath: ID!) { + project(fullPath: $fullPath) { + id + name + securityScanProfiles { + ...ScanProfileFields + } + } +} + +#import "./scan_profile.fragment.graphql" diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js b/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js new file mode 100644 index 00000000000000..e5134b40a74671 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js @@ -0,0 +1,64 @@ +// resolvers.js + +import { InMemoryCache } from '@apollo/client/core'; + +const mockAttachedByProject = {}; +// Example structure: +// { "gid://gitlab/Project/19": ["gid://gitlab/Security::ScanProfile/1"] } + +export default { + Query: { + securityScanProfile(_, { id }, { cache }) { + // Virtual object for now. + // Backend will replace this once merged. + return { + id, + name: 'Secret Push Protection (default)', + description: + 'Mock profile until backend merge. Provides secret detection using baseline rules.', + scanType: 'SECRET_DETECTION', + gitlabRecommended: true, + __typename: 'ScanProfileType', + }; + }, + }, + + Mutation: { + securityScanProfileAttach(_, { input }, { cache }) { + const { securityScanProfileId, projectIds = [] } = input; + + projectIds.forEach((projectId) => { + if (!mockAttachedByProject[projectId]) { + mockAttachedByProject[projectId] = []; + } + if (!mockAttachedByProject[projectId].includes(securityScanProfileId)) { + mockAttachedByProject[projectId].push(securityScanProfileId); + } + }); + + return { + clientMutationId: input.clientMutationId || null, + errors: [], + __typename: 'SecurityScanProfileAttachPayload', + }; + }, + + securityScanProfileDetach(_, { input }, { cache }) { + const { securityScanProfileId, projectIds = [] } = input; + + projectIds.forEach((projectId) => { + if (mockAttachedByProject[projectId]) { + mockAttachedByProject[projectId] = mockAttachedByProject[projectId].filter( + (id) => id !== securityScanProfileId, + ); + } + }); + + return { + clientMutationId: input.clientMutationId || null, + errors: [], + __typename: 'SecurityScanProfileDetachPayload', + }; + }, + }, +}; diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/scan_profile.fragment.graphql b/app/assets/javascripts/security_configuration/graphql/scan_profiles/scan_profile.fragment.graphql new file mode 100644 index 00000000000000..734d35283ebe7e --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/scan_profile.fragment.graphql @@ -0,0 +1,7 @@ +fragment ScanProfileFields on ScanProfileType { + id + name + description + scanType + gitlabRecommended +} diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile.client.query.graphql b/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile.client.query.graphql new file mode 100644 index 00000000000000..184b441965fc1e --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile.client.query.graphql @@ -0,0 +1,7 @@ +query SecurityScanProfile($id: SecurityScanProfileID!) { + securityScanProfile(id: $id) @client { + ...ScanProfileFields + } +} + +#import "./scan_profile.fragment.graphql" diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_attach.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_attach.mutation.graphql new file mode 100644 index 00000000000000..b7efaf608f853b --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_attach.mutation.graphql @@ -0,0 +1,17 @@ +mutation SecurityScanProfileAttach($input: SecurityScanProfileAttachInput!) { + securityScanProfileAttach(input: $input) @client { + clientMutationId + errors + } +} + +#import "./scan_profile.fragment.graphql" + +#Example +#{ +# "input": { +# "securityScanProfileId": "gid://gitlab/Security::ScanProfile/secret_detection", +# "projectIds": ["gid://gitlab/Project/19"], +# "groupIds": [] +# } +#} diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_detach.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_detach.mutation.graphql new file mode 100644 index 00000000000000..170ad34bb53fcc --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/security_scan_profile_detach.mutation.graphql @@ -0,0 +1,16 @@ +mutation SecurityScanProfileDetach($input: SecurityScanProfileDetachInput!) { + securityScanProfileDetach(input: $input) @client { + clientMutationId + errors + } +} + +#import "./scan_profile.fragment.graphql" + +#Example +#{ +# "input": { +# "securityScanProfileId": "gid://gitlab/Security::ScanProfile/1", +# "projectIds": ["gid://gitlab/Project/19"] +# } +#} -- GitLab From 41c1bf8ccc940641af9a0f3dea2ba097fef8ec0b Mon Sep 17 00:00:00 2001 From: Dheeraj Joshi Date: Thu, 11 Dec 2025 16:10:13 +0530 Subject: [PATCH 3/3] Add integration with resolvers --- .../security_configuration/components/app.vue | 2 +- .../scanner_profile_configuration.vue | 252 ++++++++++-------- .../scan_profiles/scanner_profile_modal.vue | 78 ++++++ .../components/scan_profiles/scanner_row.vue | 88 ++++++ .../graphql/scan_profiles/resolvers.js | 152 ++++++++--- 5 files changed, 422 insertions(+), 150 deletions(-) create mode 100644 app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_modal.vue create mode 100644 app/assets/javascripts/security_configuration/components/scan_profiles/scanner_row.vue diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index 22251df4c2e39e..00e35adc8f267b 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -34,7 +34,7 @@ import PipelineSecretDetectionFeatureCard from './pipeline_secret_detection_feat import SecretPushProtectionFeatureCard from './secret_push_protection_feature_card.vue'; import TrainingProviderList from './training_provider_list.vue'; import RefTrackingList from './ref_tracking_list.vue'; -import ScannerProfileConfiguration from './scanner_profile_configuration.vue'; +import ScannerProfileConfiguration from './scan_profiles/scanner_profile_configuration.vue'; export default { i18n, diff --git a/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue index ae5a14a26e78e6..4e9c445595def2 100644 --- a/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue +++ b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_configuration.vue @@ -1,38 +1,36 @@ + + diff --git a/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_modal.vue b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_modal.vue new file mode 100644 index 00000000000000..27ed4952df4aca --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_modal.vue @@ -0,0 +1,78 @@ + + + diff --git a/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_row.vue b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_row.vue new file mode 100644 index 00000000000000..7541fbd19e011d --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_row.vue @@ -0,0 +1,88 @@ + + + diff --git a/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js b/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js index e5134b40a74671..09ef3a4ef1f633 100644 --- a/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js @@ -1,62 +1,148 @@ -// resolvers.js +const mockDB = { + groups: {}, // groupPath → available profiles + projects: {}, // projectPath → attached profiles + profileStore: {}, // profileId → profile data +}; + +/** + * Utility: build default template profile + */ +function buildTemplateProfile(scanType) { + if (scanType === 'SECRET_DETECTION') { + return { + id: 'gid://gitlab/Security::ScanProfile/secret_detection', + name: 'Secret Push Protection (default)', + description: + 'GitLab recommended baseline protection using industry standard detection rules.', + scanType: 'SECRET_DETECTION', + gitlabRecommended: true, + triggers: [ + { + type: 'default', + description: 'Runs on push events', + }, + ], + projects: [], + __typename: 'ScanProfileType', + }; + } + + return null; +} -import { InMemoryCache } from '@apollo/client/core'; +/** + * Utility: ensure a group has an available profiles list + */ +function ensureGroup(groupPath) { + if (!mockDB.groups[groupPath]) { + mockDB.groups[groupPath] = { + available: [buildTemplateProfile('SECRET_DETECTION')], + }; + } + return mockDB.groups[groupPath]; +} -const mockAttachedByProject = {}; -// Example structure: -// { "gid://gitlab/Project/19": ["gid://gitlab/Security::ScanProfile/1"] } +/** + * Utility: ensure a project entry exists + */ +function ensureProject(projectPath) { + if (!mockDB.projects[projectPath]) { + mockDB.projects[projectPath] = { + attached: [], // list of profile IDs + }; + } + return mockDB.projects[projectPath]; +} export default { Query: { - securityScanProfile(_, { id }, { cache }) { - // Virtual object for now. - // Backend will replace this once merged. + /** + * Returns the list of available scan profiles for a group. + */ + group(_, { fullPath }) { + const group = ensureGroup(fullPath); + return { + id: `gid://gitlab/Group/${fullPath}`, + name: fullPath, + availableSecurityScanProfiles: group.available, + __typename: 'Group', + }; + }, + + /** + * Returns the list of attached profiles for a project. + */ + project(_, { fullPath }) { + const project = ensureProject(fullPath); + + const profiles = project.attached.map((id) => mockDB.profileStore[id]).filter(Boolean); + return { - id, - name: 'Secret Push Protection (default)', - description: - 'Mock profile until backend merge. Provides secret detection using baseline rules.', - scanType: 'SECRET_DETECTION', - gitlabRecommended: true, - __typename: 'ScanProfileType', + id: `gid://gitlab/Project/${fullPath}`, + fullPath, + name: fullPath, + securityScanProfiles: profiles, + __typename: 'Project', }; }, + + /** + * Resolve a profile by ID + */ + securityScanProfile(_, { id }) { + return mockDB.profileStore[id] || buildTemplateProfile('SECRET_DETECTION'); + }, }, Mutation: { - securityScanProfileAttach(_, { input }, { cache }) { - const { securityScanProfileId, projectIds = [] } = input; + /** + * Attach profile to a list of projects + */ + securityScanProfileAttach(_, { input }) { + const { projectIds = [], securityScanProfileId } = input; - projectIds.forEach((projectId) => { - if (!mockAttachedByProject[projectId]) { - mockAttachedByProject[projectId] = []; - } - if (!mockAttachedByProject[projectId].includes(securityScanProfileId)) { - mockAttachedByProject[projectId].push(securityScanProfileId); + // Ensure profile exists in store, otherwise clone the default template + if (!mockDB.profileStore[securityScanProfileId]) { + const crafted = { + ...buildTemplateProfile('SECRET_DETECTION'), + id: `gid://gitlab/Security::ScanProfile/${Math.floor(Math.random() * 99999)}`, + gitlabRecommended: true, + }; + mockDB.profileStore[crafted.id] = crafted; + } + + const profile = + mockDB.profileStore[securityScanProfileId] || + mockDB.profileStore[Object.keys(mockDB.profileStore)[0]]; + + projectIds.forEach((pid) => { + const project = ensureProject(pid); + if (!project.attached.includes(profile.id)) { + project.attached.push(profile.id); } }); return { - clientMutationId: input.clientMutationId || null, errors: [], + clientMutationId: input.clientMutationId || null, __typename: 'SecurityScanProfileAttachPayload', }; }, - securityScanProfileDetach(_, { input }, { cache }) { - const { securityScanProfileId, projectIds = [] } = input; + /** + * Detach profile from projects + */ + securityScanProfileDetach(_, { input }) { + const { projectIds = [], securityScanProfileId } = input; - projectIds.forEach((projectId) => { - if (mockAttachedByProject[projectId]) { - mockAttachedByProject[projectId] = mockAttachedByProject[projectId].filter( - (id) => id !== securityScanProfileId, - ); - } + projectIds.forEach((pid) => { + const project = ensureProject(pid); + project.attached = project.attached.filter((id) => id !== securityScanProfileId); }); return { - clientMutationId: input.clientMutationId || null, errors: [], + clientMutationId: input.clientMutationId || null, __typename: 'SecurityScanProfileDetachPayload', }; }, -- GitLab