diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue index 7048e5b8b60baad122435d47614814502324dc7a..00e35adc8f267b26f31a7d39d0f94562e34dfb82 100644 --- a/app/assets/javascripts/security_configuration/components/app.vue +++ b/app/assets/javascripts/security_configuration/components/app.vue @@ -1,5 +1,14 @@ + + + + + + + {{ data.label }} + + + + + + + + {{ item.scannerCode }} + + {{ item.scannerName }} + + + + + + {{ item.profile }} + + + + + + + + + {{ item.statusText }} + + + {{ item.statusDescription }} + + + + + + {{ item.lastScan }} + + + + + + + {{ __('Apply default profile') }} + + + + + {{ __('Detach') }} + + + + + + + + + + + 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 0000000000000000000000000000000000000000..27ed4952df4acafa899329ac33b559531426d6e9 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_profile_modal.vue @@ -0,0 +1,78 @@ + + + + + + {{ profile?.name }} + + + + Description + {{ profile?.description }} + + + + Analyzer type + {{ profile?.scanType }} + + + + Triggers + • {{ t.description }} + + + 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 0000000000000000000000000000000000000000..7541fbd19e011d094c3c21275ed42475cc453dd6 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/scan_profiles/scanner_row.vue @@ -0,0 +1,88 @@ + + + + + + + + {{ item.scannerCode }} + + {{ item.scannerName }} + + + + + + {{ item.profileLabel }} + + + + + {{ item.statusText }} + + + {{ item.lastScan }} + + + + + Apply default profile + + + + + Detach + + + + diff --git a/app/assets/javascripts/security_configuration/components/scanner_profile_configuration.vue b/app/assets/javascripts/security_configuration/components/scanner_profile_configuration.vue new file mode 100644 index 0000000000000000000000000000000000000000..f7a7c4bf1a44fffb897b85ab0e9e9792147231a9 --- /dev/null +++ b/app/assets/javascripts/security_configuration/components/scanner_profile_configuration.vue @@ -0,0 +1,124 @@ + + + + + + + + {{ data.label }} + + + + + + + + {{ item.scannerCode }} + + {{ item.scannerName }} + + + + + + {{ item.profile }} + + + + + + + {{ item.statusText }} + + + {{ item.statusDescription }} + + + + + + {{ item.lastScan }} + + + + + {{ __('Apply default profile') }} + + + + + diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js index 23666abad34bb74c930ab8dff8e91a689ae8ba24..861839f96bfee0480a0f115d840f1f674b4d0fa5 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/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 0000000000000000000000000000000000000000..f29dae27de9c832e0f57e18929b71f09b6daf61d --- /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 0000000000000000000000000000000000000000..446b422ef27177a3df81468ba0ebd3ef48b24796 --- /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 0000000000000000000000000000000000000000..09ef3a4ef1f633da18ea14ba08b7bcc01d6003c4 --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/scan_profiles/resolvers.js @@ -0,0 +1,150 @@ +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; +} + +/** + * 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]; +} + +/** + * 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: { + /** + * 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: `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: { + /** + * Attach profile to a list of projects + */ + securityScanProfileAttach(_, { input }) { + const { projectIds = [], securityScanProfileId } = input; + + // 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 { + errors: [], + clientMutationId: input.clientMutationId || null, + __typename: 'SecurityScanProfileAttachPayload', + }; + }, + + /** + * Detach profile from projects + */ + securityScanProfileDetach(_, { input }) { + const { projectIds = [], securityScanProfileId } = input; + + projectIds.forEach((pid) => { + const project = ensureProject(pid); + project.attached = project.attached.filter((id) => id !== securityScanProfileId); + }); + + return { + errors: [], + clientMutationId: input.clientMutationId || null, + __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 0000000000000000000000000000000000000000..734d35283ebe7e19381292f6eac93fab2fb8a584 --- /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 0000000000000000000000000000000000000000..184b441965fc1ead5458735f9391a5cbd28a4887 --- /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 0000000000000000000000000000000000000000..b7efaf608f853b6da6dc54775a194e18a8ec5dc3 --- /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 0000000000000000000000000000000000000000..170ad34bb53fcc35a455d0cb98f7b4342fbd905a --- /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"] +# } +#} diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ea52711dad2bdeda48190684073558a6643bb545..65a4cd0ddec0b7375fb582f4dcc8660b06df4d42 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 ""