diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-02 18:08:56 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-02 18:08:56 +0300 |
commit | 6f998d352988f93f875db862353e814e95db1fe3 (patch) | |
tree | 3596c4b8cbc4d426c3aaf571ea5ba1a57ffe031e | |
parent | 3eec6c2511af2b10cd25be64dcd84c4a35a7bcdb (diff) |
Add latest changes from gitlab-org/gitlab@master
172 files changed, 7306 insertions, 309 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 99c31a13179..570b9c98af1 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -ce833c4ea66902f46b197d336e168a79ac29be81 +f69cea16bcc88ddf29fb6c4c67a5d788fbc00f9a @@ -335,6 +335,8 @@ gem 'method_source', '~> 1.0', require: false gem 'webrick', '~> 1.6.1', require: false gem 'prometheus-client-mmap', '~> 0.12.0', require: 'prometheus/client' +gem 'warning', '~> 1.2.0' + group :development do gem 'lefthook', '~> 0.7.0', require: false gem 'solargraph', '~> 0.42', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 999bed3eb4d..dd372a929cd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1339,6 +1339,7 @@ GEM vmstat (2.3.0) warden (1.2.8) rack (>= 2.0.6) + warning (1.2.0) webauthn (2.3.0) android_key_attestation (~> 0.3.0) awrence (~> 1.1) @@ -1648,6 +1649,7 @@ DEPENDENCIES validates_hostname (~> 1.0.11) version_sorter (~> 2.2.4) vmstat (~> 2.3.0) + warning (~> 1.2.0) webauthn (~> 2.3) webmock (~> 3.9.1) webrick (~> 1.6.1) diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index 431900aecf0..5881ec08a2e 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -9,13 +9,14 @@ import { GlSprintf, GlButton, GlFormInput, + GlFormCheckboxGroup, } from '@gitlab/ui'; import { partition, isString } from 'lodash'; import Api from '~/api'; import ExperimentTracking from '~/experimentation/experiment_tracking'; -import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants'; +import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import { s__, sprintf } from '~/locale'; -import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS } from '../constants'; +import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS, MEMBER_AREAS_OF_FOCUS } from '../constants'; import eventHub from '../event_hub'; import { responseMessageFromError, @@ -36,6 +37,7 @@ export default { GlSprintf, GlButton, GlFormInput, + GlFormCheckboxGroup, MembersTokenSelect, GroupSelect, }, @@ -74,6 +76,14 @@ export default { type: String, required: true, }, + areasOfFocusOptions: { + type: Array, + required: true, + }, + noSelectionAreasOfFocus: { + type: Array, + required: true, + }, }, data() { return { @@ -83,6 +93,7 @@ export default { inviteeType: 'members', newUsersToInvite: [], selectedDate: undefined, + selectedAreasOfFocus: [], groupToBeSharedWith: {}, source: 'unknown', invalidFeedbackMessage: '', @@ -128,10 +139,21 @@ export default { this.newUsersToInvite.length === 0 && Object.keys(this.groupToBeSharedWith).length === 0 ); }, + areasOfFocusEnabled() { + return this.areasOfFocusOptions.length !== 0; + }, + areasOfFocusForPost() { + if (this.selectedAreasOfFocus.length === 0 && this.areasOfFocusEnabled) { + return this.noSelectionAreasOfFocus; + } + + return this.selectedAreasOfFocus; + }, }, mounted() { eventHub.$on('openModal', (options) => { this.openModal(options); + this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.view); }); }, methods: { @@ -152,9 +174,12 @@ export default { this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, + trackEvent(experimentName, eventName) { + const tracking = new ExperimentTracking(experimentName); + tracking.event(eventName); + }, closeModal() { - this.resetFields(); - this.$root.$emit(BV_HIDE_MODAL, this.modalId); + this.$refs.modal.hide(); }, sendInvite() { if (this.isInviteGroup) { @@ -165,9 +190,10 @@ export default { }, trackInvite() { if (this.source === INVITE_MEMBERS_IN_COMMENT) { - const tracking = new ExperimentTracking(INVITE_MEMBERS_IN_COMMENT); - tracking.event('comment_invite_success'); + this.trackEvent(INVITE_MEMBERS_IN_COMMENT, 'comment_invite_success'); } + + this.trackEvent(MEMBER_AREAS_OF_FOCUS.name, MEMBER_AREAS_OF_FOCUS.submit); }, resetFields() { this.isLoading = false; @@ -176,6 +202,7 @@ export default { this.newUsersToInvite = []; this.groupToBeSharedWith = {}; this.invalidFeedbackMessage = ''; + this.selectedAreasOfFocus = []; }, changeSelectedItem(item) { this.selectedAccessLevel = item; @@ -223,6 +250,7 @@ export default { email: usersToInviteByEmail, access_level: this.selectedAccessLevel, invite_source: this.source, + areas_of_focus: this.areasOfFocusForPost, }; }, addByUserIdPostData(usersToAddById) { @@ -231,6 +259,7 @@ export default { user_id: usersToAddById, access_level: this.selectedAccessLevel, invite_source: this.source, + areas_of_focus: this.areasOfFocusForPost, }; }, shareWithGroupPostData(groupToBeSharedWith) { @@ -304,18 +333,22 @@ export default { inviteButtonText: s__('InviteMembersModal|Invite'), cancelButtonText: s__('InviteMembersModal|Cancel'), headerCloseLabel: s__('InviteMembersModal|Close invite team members'), + areasOfFocusLabel: s__( + 'InviteMembersModal|What would you like new member(s) to focus on? (optional)', + ), }, membersTokenSelectLabelId: 'invite-members-input', }; </script> <template> <gl-modal + ref="modal" :modal-id="modalId" size="sm" data-qa-selector="invite_members_modal_content" :title="$options.labels[inviteeType].modalTitle" :header-close-label="$options.labels.headerCloseLabel" - @close="resetFields" + @hidden="resetFields" > <div> <p ref="introText"> @@ -351,7 +384,7 @@ export default { /> </gl-form-group> - <label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label> + <label class="gl-mt-3">{{ $options.labels.accessLevel }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full"> <gl-dropdown class="gl-shadow-none gl-w-full" @@ -381,7 +414,7 @@ export default { </gl-sprintf> </div> - <label class="gl-font-weight-bold gl-mt-5 gl-display-block" for="expires_at">{{ + <label class="gl-mt-5 gl-display-block" for="expires_at">{{ $options.labels.accessExpireDate }}</label> <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block"> @@ -400,6 +433,16 @@ export default { </template> </gl-datepicker> </div> + <div v-if="areasOfFocusEnabled"> + <label class="gl-mt-5"> + {{ $options.labels.areasOfFocusLabel }} + </label> + <gl-form-checkbox-group + v-model="selectedAreasOfFocus" + :options="areasOfFocusOptions" + data-testid="area-of-focus-checks" + /> + </div> </div> <template #modal-footer> diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 83e6cac0ac0..01b35f2a656 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -3,6 +3,11 @@ import { __ } from '~/locale'; export const SEARCH_DELAY = 200; export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment'; +export const MEMBER_AREAS_OF_FOCUS = { + name: 'member_areas_of_focus', + view: 'view', + submit: 'submit', +}; export const GROUP_FILTERS = { ALL: 'all', diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js index 7501e9f4e6e..db7e2ca4a71 100644 --- a/app/assets/javascripts/invite_members/init_invite_members_modal.js +++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js @@ -23,6 +23,8 @@ export default function initInviteMembersModal() { defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10), groupSelectFilter: el.dataset.groupsFilter, groupSelectParentId: parseInt(el.dataset.parentId, 10), + areasOfFocusOptions: JSON.parse(el.dataset.areasOfFocusOptions), + noSelectionAreasOfFocus: JSON.parse(el.dataset.noSelectionAreasOfFocus), }, }), }); diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue index f3dd26b02cb..3a4453bc7ae 100644 --- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue +++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue @@ -3,7 +3,7 @@ import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/securi import createFlash from '~/flash'; import { s__ } from '~/locale'; import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils'; export default { diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql new file mode 100644 index 00000000000..ae77a2ce5e4 --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql @@ -0,0 +1,13 @@ +fragment JobArtifacts on Pipeline { + jobs(securityReportTypes: $reportTypes) { + nodes { + name + artifacts { + nodes { + downloadPath + fileType + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql index 4ce13827da2..4ce13827da2 100644 --- a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql +++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql diff --git a/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql new file mode 100644 index 00000000000..b5858ab012b --- /dev/null +++ b/app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql @@ -0,0 +1,10 @@ +#import "../fragments/job_artifacts.fragment.graphql" + +query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) { + project(fullPath: $projectPath) { + pipeline(iid: $iid) { + id + ...JobArtifacts + } + } +} diff --git a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql b/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql deleted file mode 100644 index c7e9fa16418..00000000000 --- a/app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql +++ /dev/null @@ -1,18 +0,0 @@ -query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) { - project(fullPath: $projectPath) { - pipeline(iid: $iid) { - id - jobs(securityReportTypes: $reportTypes) { - nodes { - name - artifacts { - nodes { - downloadPath - fileType - } - } - } - } - } - } -} diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue index 3e0310e173e..ad40ea6a964 100644 --- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue +++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue @@ -13,7 +13,7 @@ import { REPORT_TYPE_SECRET_DETECTION, reportTypeToSecurityReportTypeEnum, } from './constants'; -import securityReportMergeRequestDownloadPathsQuery from './queries/security_report_merge_request_download_paths.query.graphql'; +import securityReportMergeRequestDownloadPathsQuery from './graphql/queries/security_report_merge_request_download_paths.query.graphql'; import store from './store'; import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants'; import { extractSecurityReportArtifactsFromMergeRequest } from './utils'; diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 145b4d10b16..0a8a629cd8a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -136,7 +136,9 @@ class Admin::UsersController < Admin::ApplicationController end def unban - if update_user { |user| user.activate } + result = Users::UnbanService.new(current_user).execute(user) + + if result[:status] == :success redirect_back_or_admin_user(notice: _("Successfully unbanned")) else redirect_back_or_admin_user(alert: _("Error occurred. User was not unbanned")) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 48d00163f3b..bdb645e1934 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -388,6 +388,7 @@ class ProjectsController < Projects::ApplicationController analytics_access_level operations_access_level security_and_compliance_access_level + container_registry_access_level ] end diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb index 3c290701a5f..3274ea15b8b 100644 --- a/app/helpers/invite_members_helper.rb +++ b/app/helpers/invite_members_helper.rb @@ -39,4 +39,43 @@ module InviteMembersHelper {} end end + + def common_invite_modal_dataset(source) + dataset = { + id: source.id, + name: source.name, + default_access_level: Gitlab::Access::GUEST + } + + experiment(:member_areas_of_focus, user: current_user) do |e| + e.publish_to_database + + e.control { dataset.merge!(areas_of_focus_options: [], no_selection_areas_of_focus: []) } + e.candidate { dataset.merge!(areas_of_focus_options: member_areas_of_focus_options.to_json, no_selection_areas_of_focus: ['no_selection']) } + end + + dataset + end + + private + + def member_areas_of_focus_options + [ + { + value: 'Contribute to the codebase', text: s_('InviteMembersModal|Contribute to the codebase') + }, + { + value: 'Collaborate on open issues and merge requests', text: s_('InviteMembersModal|Collaborate on open issues and merge requests') + }, + { + value: 'Configure CI/CD', text: s_('InviteMembersModal|Configure CI/CD') + }, + { + value: 'Configure security features', text: s_('InviteMembersModal|Configure security features') + }, + { + value: 'Other', text: s_('InviteMembersModal|Other') + } + ] + end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 08aa532de24..bff09951b43 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -354,6 +354,29 @@ module ProjectsHelper project.repository_languages.with_programming_language('HCL').exists? && project.terraform_states.empty? end + def project_permissions_panel_data(project) + { + packagesAvailable: ::Gitlab.config.packages.enabled, + packagesHelpPath: help_page_path('user/packages/index'), + currentSettings: project_permissions_settings(project), + canDisableEmails: can_disable_emails?(project, current_user), + canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), + allowedVisibilityOptions: project_allowed_visibility_levels(project), + visibilityHelpPath: help_page_path('public_access/public_access'), + registryAvailable: Gitlab.config.registry.enabled, + registryHelpPath: help_page_path('user/packages/container_registry/index'), + lfsAvailable: Gitlab.config.lfs.enabled, + lfsHelpPath: help_page_path('topics/git/lfs/index'), + lfsObjectsExist: project.lfs_objects.exists?, + lfsObjectsRemovalHelpPath: help_page_path('topics/git/lfs/index', anchor: 'removing-objects-from-lfs'), + pagesAvailable: Gitlab.config.pages.enabled, + pagesAccessControlEnabled: Gitlab.config.pages.access_control, + pagesAccessControlForced: ::Gitlab::Pages.access_control_is_forced?, + pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control'), + issuesHelpPath: help_page_path('user/project/issues/index') + } + end + private def tab_ability_map @@ -510,37 +533,11 @@ module ProjectsHelper metricsDashboardAccessLevel: feature.metrics_dashboard_access_level, operationsAccessLevel: feature.operations_access_level, showDefaultAwardEmojis: project.show_default_award_emojis?, - securityAndComplianceAccessLevel: project.security_and_compliance_access_level - } - end - - def project_permissions_panel_data(project) - { - packagesAvailable: ::Gitlab.config.packages.enabled, - packagesHelpPath: help_page_path('user/packages/index'), - currentSettings: project_permissions_settings(project), - canDisableEmails: can_disable_emails?(project, current_user), - canChangeVisibilityLevel: can_change_visibility_level?(project, current_user), - allowedVisibilityOptions: project_allowed_visibility_levels(project), - visibilityHelpPath: help_page_path('public_access/public_access'), - registryAvailable: Gitlab.config.registry.enabled, - registryHelpPath: help_page_path('user/packages/container_registry/index'), - lfsAvailable: Gitlab.config.lfs.enabled, - lfsHelpPath: help_page_path('topics/git/lfs/index'), - lfsObjectsExist: project.lfs_objects.exists?, - lfsObjectsRemovalHelpPath: help_page_path('topics/git/lfs/index', anchor: 'removing-objects-from-lfs'), - pagesAvailable: Gitlab.config.pages.enabled, - pagesAccessControlEnabled: Gitlab.config.pages.access_control, - pagesAccessControlForced: ::Gitlab::Pages.access_control_is_forced?, - pagesHelpPath: help_page_path('user/project/pages/introduction', anchor: 'gitlab-pages-access-control'), - issuesHelpPath: help_page_path('user/project/issues/index') + securityAndComplianceAccessLevel: project.security_and_compliance_access_level, + containerRegistryAccessLevel: feature.container_registry_access_level } end - def project_permissions_panel_data_json(project) - project_permissions_panel_data(project).to_json.html_safe - end - def project_allowed_visibility_levels(project) Gitlab::VisibilityLevel.values.select do |level| project.visibility_level_allowed?(level) && !restricted_levels.include?(level) diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb index f0e5e010e70..a656856487d 100644 --- a/app/models/concerns/vulnerability_finding_helpers.rb +++ b/app/models/concerns/vulnerability_finding_helpers.rb @@ -2,6 +2,35 @@ module VulnerabilityFindingHelpers extend ActiveSupport::Concern -end + def matches_signatures(other_signatures, other_uuid) + other_signature_types = other_signatures.index_by(&:algorithm_type) + + # highest first + match_result = nil + signatures.sort_by(&:priority).reverse_each do |signature| + matching_other_signature = other_signature_types[signature.algorithm_type] + next if matching_other_signature.nil? + + match_result = matching_other_signature == signature + break + end -VulnerabilityFindingHelpers.prepend_mod_with('VulnerabilityFindingHelpers') + if match_result.nil? + [uuid, *signature_uuids].include?(other_uuid) + else + match_result + end + end + + def signature_uuids + signatures.map do |signature| + hex_sha = signature.signature_hex + ::Security::VulnerabilityUUID.generate( + report_type: report_type, + location_fingerprint: hex_sha, + primary_identifier_fingerprint: primary_identifier&.fingerprint, + project_id: project_id + ) + end + end +end diff --git a/app/models/concerns/vulnerability_finding_signature_helpers.rb b/app/models/concerns/vulnerability_finding_signature_helpers.rb index f98c1e93aaf..71a12b4077b 100644 --- a/app/models/concerns/vulnerability_finding_signature_helpers.rb +++ b/app/models/concerns/vulnerability_finding_signature_helpers.rb @@ -2,6 +2,30 @@ module VulnerabilityFindingSignatureHelpers extend ActiveSupport::Concern -end + # If the location object describes a physical location within a file + # (filename + line numbers), the 'location' algorithm_type should be used + # If the location object describes arbitrary data, then the 'hash' + # algorithm_type should be used. + + ALGORITHM_TYPES = { hash: 1, location: 2, scope_offset: 3 }.with_indifferent_access.freeze + + class_methods do + def priority(algorithm_type) + raise ArgumentError, "No priority for #{algorithm_type.inspect}" unless ALGORITHM_TYPES.key?(algorithm_type) + + ALGORITHM_TYPES[algorithm_type] + end -VulnerabilityFindingSignatureHelpers.prepend_mod_with('VulnerabilityFindingSignatureHelpers') + def algorithm_types + ALGORITHM_TYPES + end + end + + def priority + self.class.priority(algorithm_type) + end + + def algorithm_types + self.class.algorithm_types + end +end diff --git a/app/models/user.rb b/app/models/user.rb index a2e9768eb94..98281102458 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -205,6 +205,7 @@ class User < ApplicationRecord has_one :user_canonical_email has_one :credit_card_validation, class_name: '::Users::CreditCardValidation' has_one :atlassian_identity, class_name: 'Atlassian::Identity' + has_one :banned_user, class_name: '::Users::BannedUser' has_many :reviews, foreign_key: :author_id, inverse_of: :author @@ -326,7 +327,6 @@ class User < ApplicationRecord transition deactivated: :blocked transition ldap_blocked: :blocked transition blocked_pending_approval: :blocked - transition banned: :blocked end event :ldap_block do @@ -380,6 +380,14 @@ class User < ApplicationRecord NotificationService.new.user_deactivated(user.name, user.notification_email) end # rubocop: enable CodeReuse/ServiceClass + + after_transition active: :banned do |user| + user.create_banned_user + end + + after_transition banned: :active do |user| + user.banned_user&.destroy + end end # Scopes diff --git a/app/models/users/banned_user.rb b/app/models/users/banned_user.rb new file mode 100644 index 00000000000..c52b6d4b728 --- /dev/null +++ b/app/models/users/banned_user.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module Users + class BannedUser < ApplicationRecord + self.primary_key = :user_id + + belongs_to :user + + validates :user, presence: true + validates :user_id, uniqueness: { message: _("banned user already exists") } + end +end diff --git a/app/services/security/merge_reports_service.rb b/app/services/security/merge_reports_service.rb new file mode 100644 index 00000000000..5f6f98a3c39 --- /dev/null +++ b/app/services/security/merge_reports_service.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Security + class MergeReportsService + attr_reader :source_reports + + def initialize(*source_reports) + @source_reports = source_reports + end + + def execute + copy_resources_to_target_report + copy_findings_to_target + target_report + end + + private + + def target_report + @target_report ||= ::Gitlab::Ci::Reports::Security::Report.new( + source_reports.first.type, + source_reports.first.pipeline, + source_reports.first.created_at + ).tap { |report| report.errors = source_reports.flat_map(&:errors) } + end + + def copy_resources_to_target_report + sorted_source_reports.each do |source_report| + copy_scanners_to_target(source_report) + copy_identifiers_to_target(source_report) + copy_scanned_resources_to_target(source_report) + end + end + + def sorted_source_reports + source_reports.sort { |a, b| a.primary_scanner_order_to(b) } + end + + def copy_scanners_to_target(source_report) + # no need for de-duping: it's done by Report internally + source_report.scanners.values.each { |scanner| target_report.add_scanner(scanner) } + end + + def copy_identifiers_to_target(source_report) + # no need for de-duping: it's done by Report internally + source_report.identifiers.values.each { |identifier| target_report.add_identifier(identifier) } + end + + def copy_scanned_resources_to_target(source_report) + target_report.scanned_resources.concat(source_report.scanned_resources).uniq! + end + + def copy_findings_to_target + deduplicated_findings.sort.each { |finding| target_report.add_finding(finding) } + end + + def deduplicated_findings + prioritized_findings.each_with_object([[], Set.new]) do |finding, (deduplicated, seen_identifiers)| + next if seen_identifiers.intersect?(finding.keys.to_set) + + seen_identifiers.merge(finding.keys) + deduplicated << finding + end.first + end + + def prioritized_findings + source_reports.flat_map(&:findings).sort { |a, b| a.scanner_order_to(b) } + end + end +end diff --git a/app/services/users/ban_service.rb b/app/services/users/ban_service.rb index 247ed14966b..88e92ebff9b 100644 --- a/app/services/users/ban_service.rb +++ b/app/services/users/ban_service.rb @@ -1,25 +1,15 @@ # frozen_string_literal: true module Users - class BanService < BaseService - def initialize(current_user) - @current_user = current_user - end + class BanService < BannedUserBaseService + private - def execute(user) - if user.ban - log_event(user) - success - else - messages = user.errors.full_messages - error(messages.uniq.join('. ')) - end + def update_user(user) + user.ban end - private - - def log_event(user) - Gitlab::AppLogger.info(message: "User banned", user: "#{user.username}", email: "#{user.email}", banned_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + def action + :ban end end end diff --git a/app/services/users/banned_user_base_service.rb b/app/services/users/banned_user_base_service.rb new file mode 100644 index 00000000000..16041075941 --- /dev/null +++ b/app/services/users/banned_user_base_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Users + class BannedUserBaseService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + return permission_error unless allowed? + + if update_user(user) + log_event(user) + success + else + messages = user.errors.full_messages + error(messages.uniq.join('. ')) + end + end + + private + + attr_reader :current_user + + def allowed? + can?(current_user, :admin_all_resources) + end + + def permission_error + error(_("You are not allowed to %{action} a user" % { action: action.to_s }), :forbidden) + end + + def log_event(user) + Gitlab::AppLogger.info(message: "User #{action}", user: "#{user.username}", email: "#{user.email}", "#{action}_by": "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end + end +end diff --git a/app/services/users/unban_service.rb b/app/services/users/unban_service.rb new file mode 100644 index 00000000000..363783cf240 --- /dev/null +++ b/app/services/users/unban_service.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Users + class UnbanService < BannedUserBaseService + private + + def update_user(user) + user.activate + end + + def action + :unban + end + end +end diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml index 3be1a142ca6..8801ad98b8c 100644 --- a/app/views/groups/_invite_members_modal.html.haml +++ b/app/views/groups/_invite_members_modal.html.haml @@ -1,8 +1,5 @@ - return unless can_manage_members?(group) -.js-invite-members-modal{ data: { id: group.id, - name: group.name, - is_project: 'false', +.js-invite-members-modal{ data: { is_project: 'false', access_levels: GroupMember.access_level_roles.to_json, - default_access_level: Gitlab::Access::GUEST, - help_link: help_page_url('user/permissions') }.merge(group_select_data(group)) } + help_link: help_page_url('user/permissions') }.merge(group_select_data(group)).merge(common_invite_modal_dataset(group)) } diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml index 5dd6ec0addf..16964d2154a 100644 --- a/app/views/projects/_invite_members_modal.html.haml +++ b/app/views/projects/_invite_members_modal.html.haml @@ -1,8 +1,5 @@ - return unless can_import_members? -.js-invite-members-modal{ data: { id: project.id, - name: project.name, - is_project: 'true', +.js-invite-members-modal{ data: { is_project: 'true', access_levels: ProjectMember.access_level_roles.to_json, - default_access_level: Gitlab::Access::GUEST, - help_link: help_page_url('user/permissions') } } + help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)) } diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index f2f753b4e86..41333c416de 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -5,7 +5,7 @@ .file-holder-bottom-radius.file-holder.file.gl-mb-3 .js-file-title.file-title.gl-display-flex.gl-align-items-center.clearfix{ data: { current_action: action } } .editor-ref.block-truncated.has-tooltip{ title: ref } - = sprite_icon('fork', size: 12) + = sprite_icon('branch', size: 12) = ref - if current_action?(:edit) || current_action?(:update) %span.float-left.gl-mr-3 diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 60cb06f71ba..99a9535b8e8 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -4,7 +4,7 @@ %li{ class: "branch-item js-branch-item js-branch-#{branch.name}", data: { name: branch.name } } .branch-info .branch-title - = sprite_icon('fork', size: 12, css_class: 'gl-flex-shrink-0') + = sprite_icon('branch', size: 12, css_class: 'gl-flex-shrink-0') = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name gl-ml-3 qa-branch-name' do = branch.name - if branch.name == @repository.root_ref diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ce02c64623f..926a0610577 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -19,7 +19,7 @@ .settings-content = form_for @project, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f| %input{ name: 'update_section', type: 'hidden', value: 'js-shared-permissions' } - %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project) + %template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe .js-project-permissions-form - if show_visibility_confirm_modal?(@project) = render "visibility_modal" diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index b70bc740175..3e2c5f088f7 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -33,7 +33,7 @@ %span.project-ref-path.has-tooltip{ title: _('Target branch') } = link_to project_ref_path(merge_request.project, merge_request.target_branch), class: 'ref-name' do - = sprite_icon('fork', size: 12, css_class: 'fork-sprite') + = sprite_icon('branch', size: 12, css_class: 'fork-sprite') = merge_request.target_branch - if merge_request.labels.any? diff --git a/config/feature_flags/experiment/member_areas_of_focus.yml b/config/feature_flags/experiment/member_areas_of_focus.yml new file mode 100644 index 00000000000..e728ee7e3d3 --- /dev/null +++ b/config/feature_flags/experiment/member_areas_of_focus.yml @@ -0,0 +1,8 @@ +--- +name: member_areas_of_focus +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65273 +rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/406 +milestone: '14.2' +type: experiment +group: group::expansion +default_enabled: false diff --git a/config/initializers/0_log_deprecations.rb b/config/initializers/0_log_deprecations.rb new file mode 100644 index 00000000000..20fb5144937 --- /dev/null +++ b/config/initializers/0_log_deprecations.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +def log_deprecations? + via_env_var = Gitlab::Utils.to_boolean(ENV['GITLAB_LOG_DEPRECATIONS']) + # enable by default during development unless explicitly turned off + via_env_var.nil? ? Rails.env.development? : via_env_var +end + +if log_deprecations? + # Log deprecation warnings emitted through Kernel#warn, such as from gems or + # the Ruby VM. + Warning.process(/.+is deprecated$/) do |warning| + Gitlab::DeprecationJsonLogger.info(message: warning.strip, source: 'ruby') + # Returning :default means we continue emitting this to stderr as well. + :default + end + + # Log deprecation warnings emitted from Rails (see ActiveSupport::Deprecation). + ActiveSupport::Notifications.subscribe('deprecation.rails') do |name, start, finish, id, payload| + Gitlab::DeprecationJsonLogger.info(message: payload[:message].strip, source: 'rails') + end +end diff --git a/db/migrate/20210713211008_create_banned_users.rb b/db/migrate/20210713211008_create_banned_users.rb new file mode 100644 index 00000000000..7e5eb7f95b8 --- /dev/null +++ b/db/migrate/20210713211008_create_banned_users.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class CreateBannedUsers < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + def up + with_lock_retries do + create_table :banned_users, id: false do |t| + t.timestamps_with_timezone null: false + t.references :user, primary_key: true, default: nil, foreign_key: { on_delete: :cascade }, type: :bigint, index: false, null: false + end + end + end + + def down + with_lock_retries do + drop_table :banned_users + end + end +end diff --git a/db/schema_migrations/20210713211008 b/db/schema_migrations/20210713211008 new file mode 100644 index 00000000000..75ccad3e348 --- /dev/null +++ b/db/schema_migrations/20210713211008 @@ -0,0 +1 @@ +f66d8f3bc32996fe7743cc98cfb96fedd86784d38c8debb5143b7adabdfebd18
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1fcc8804893..2e31660b31e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9959,6 +9959,12 @@ CREATE SEQUENCE badges_id_seq ALTER SEQUENCE badges_id_seq OWNED BY badges.id; +CREATE TABLE banned_users ( + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + user_id bigint NOT NULL +); + CREATE TABLE batched_background_migration_jobs ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, @@ -21092,6 +21098,9 @@ ALTER TABLE ONLY background_migration_jobs ALTER TABLE ONLY badges ADD CONSTRAINT badges_pkey PRIMARY KEY (id); +ALTER TABLE ONLY banned_users + ADD CONSTRAINT banned_users_pkey PRIMARY KEY (user_id); + ALTER TABLE ONLY batched_background_migration_jobs ADD CONSTRAINT batched_background_migration_jobs_pkey PRIMARY KEY (id); @@ -28235,6 +28244,9 @@ ALTER TABLE ONLY merge_trains ALTER TABLE ONLY ci_runner_namespaces ADD CONSTRAINT fk_rails_f9d9ed3308 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY banned_users + ADD CONSTRAINT fk_rails_fa5bb598e5 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY requirements_management_test_reports ADD CONSTRAINT fk_rails_fb3308ad55 FOREIGN KEY (requirement_id) REFERENCES requirements(id) ON DELETE CASCADE; diff --git a/doc/administration/integration/kroki.md b/doc/administration/integration/kroki.md index e36b8a0be9d..729894052b2 100644 --- a/doc/administration/integration/kroki.md +++ b/doc/administration/integration/kroki.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 9302e9a1edc..1be234c2771 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/api_resources.md b/doc/api/api_resources.md index 6c9baab83e9..aae76697841 100644 --- a/doc/api/api_resources.md +++ b/doc/api/api_resources.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/custom_attributes.md b/doc/api/custom_attributes.md index 56a9f6881cd..9908c58de35 100644 --- a/doc/api/custom_attributes.md +++ b/doc/api/custom_attributes.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/audit_report.md b/doc/api/graphql/audit_report.md index a68af6e8646..ba9967f85f2 100644 --- a/doc/api/graphql/audit_report.md +++ b/doc/api/graphql/audit_report.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/getting_started.md b/doc/api/graphql/getting_started.md index 5b482d15c51..e3cf81148c2 100644 --- a/doc/api/graphql/getting_started.md +++ b/doc/api/graphql/getting_started.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index b7a82dba7e9..e77e6102594 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/graphql/users_example.md b/doc/api/graphql/users_example.md index 1222cd8ee8e..8fbfb67d166 100644 --- a/doc/api/graphql/users_example.md +++ b/doc/api/graphql/users_example.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/index.md b/doc/api/index.md index 445cd72f2f3..d9b7afc2dc8 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/openapi/openapi_interactive.md b/doc/api/openapi/openapi_interactive.md index c9434147609..f83ac985131 100644 --- a/doc/api/openapi/openapi_interactive.md +++ b/doc/api/openapi/openapi_interactive.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/services.md b/doc/api/services.md index 0a699aee4e6..8daaa532ff4 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md index 1f0bce1c78f..39a3ccb2bc3 100644 --- a/doc/api/system_hooks.md +++ b/doc/api/system_hooks.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 69e1ea56c2c..8875e4daa87 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/api/version.md b/doc/api/version.md index 313ba4da7d4..b23930e70f9 100644 --- a/doc/api/version.md +++ b/doc/api/version.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/architecture/blueprints/consolidating_groups_and_projects/index.md b/doc/architecture/blueprints/consolidating_groups_and_projects/index.md new file mode 100644 index 00000000000..fab886808e2 --- /dev/null +++ b/doc/architecture/blueprints/consolidating_groups_and_projects/index.md @@ -0,0 +1,155 @@ +--- +stage: none +group: unassigned +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +comments: false +description: Consolidating groups and projects +--- + +# Consolidating Group and Project + +There are numerous features that exist exclusively within groups or +projects. The boundary between group and project features used to be clear. +However, there is growing demand to have group features within projects, and +project features within groups. For example, having issues in groups, and epics +in projects. + +The [Simplify Groups & Projects Working Group](https://about.gitlab.com/company/team/structure/working-groups/simplify-groups-and-projects/) +determined that our architecture is a significant hurdle in sharing features +across groups and projects. + +Architecture issue: <https://gitlab.com/gitlab-org/architecture/tasks/-/issues/7> + +## Challenges + +### Feature duplication + +When a feature needs to be made available on a different level, we have +no established process in place. This results in the reimplementation of +the same feature. Those implementations diverge from each other over time as +they all live on their own. A few more problems with this approach: + +- Features are coupled to their container. In practice it is not straight + forward to decouple a feature from its container. The degree of coupling + varies across features. +- Naive duplication of features will result in a more complex and fragile code base. +- Generalizing solutions across groups and projects may degrade system performance. +- The range of features span across many teams, and these changes will need to + manage development interference. +- The group/project hierarchy creates a natural feature hierarchy. When features + exist across containers the feature hierarchy becomes ambiguous. +- Duplication of features slows down development velocity. + +There is potential for significant architectural changes. These changes will +have to be independent of the product design, so that customer experience +remains consistent. + +### Performance + +Resources can only be queried in elaborate / complicated ways. This caused +performance issues with authorization, epics, and many other places. As an +example, to query the projects a user has access to, the following sources need +to be considered: + +- personal projects +- direct group membership +- direct project membership +- inherited group membership +- inherited project membership +- group sharing +- inherited membership via group sharing +- project sharing + +Group / project membership, group / project sharing are also examples of +duplicated features. + +## Goals + +For now this blueprint strictly relates to the engineering challenges. + +- Consolidate the group and project container architecture. +- Develop a set of solutions to decouple features from their container. +- Decouple engineering changes from product changes. +- Develop a strategy to make architectural changes without adversely affecting + other teams. +- Provide a solution for requests asking for features availability of other levels. + +## Proposal + +Use our existing `Namespace` model as a container for features. We already have +a `Namespace` associated with `User` (personal namespace), and with `Group` +(which is a subclass of `Namespace`). We can extend this further, by associating +`Namespace` with `Projects` by introducing `ProjectNamespaces`. Each `Project` +should be owned by its `ProjectNamespace`, and this relation should replace the +existing `Project` <-> `Group` / personal namespace relation. + +We also lack a model specific for personal namespaces, and we use the generic +`Namespace` model instead. This is confusing, but can be fixed by creating a +dedicated subclass: `UserNamespace`. + +As a result, the `Namespace` hierarchy will transition to: + +```mermaid +classDiagram + Namespace <|-- UserNamespace + Namespace <|-- Group + Namespace <|-- ProjectNamespace +``` + +New features should be implemented on `Namespace`. Similarly, when a feature +need to be reimplemented on a different level, moving it to `Namespace` +essentially makes it available on all levels: + +- personal namespaces +- groups +- projects + +Various traversal queries are already available on `Namespaces` to query the +group hierarchy. `Projects` represents the leaf nodes in the hierarchy, but with +the introduction of `ProjectNamespace`, these traversal queries can be used to +retrieve projects as well. + +This also enables further simplification of some of our core features: + +- routes should be generated based on the `Namespace` hierarchy, instead of + mixing project with the group hierarchy. +- there is no need to differentiate between `GroupMembers` and `ProjectMembers`. + All `Members` should be related to a `Namespace`. This can lead to simplified + querying, and potentially deduplicating policies. + +As more and more features will be migrated to `Namespace`, the role of `Project` +model will diminish over time to essentially a container around repository +related functionality. + +## Iterations + +The work required to establish `Namespace` as a container for our features is +tracked under [Consolidate Groups and Projects](https://gitlab.com/groups/gitlab-org/-/epics/6473) +epic. + +## Who + +Proposal: + +<!-- vale gitlab.Spelling = NO --> + +| Role | Who +|------------------------------|-------------------------------------| +| Author | Alex Pooley, Imre Farkas | +| Architecture Evolution Coach | Dmitriy Zaporozhets, Grzegorz Bizon | +| Engineering Leader | Michelle Gill | +| Domain Expert | Jan Provaznik | + +<!-- vale gitlab.Spelling = YES --> + +DRIs: + +<!-- vale gitlab.Spelling = NO --> + +| Role | Who +|------------------------------|------------------------| +| Product | Melissa Ushakov | +| Leadership | Michelle Gill | +| Engineering | Imre Farkas | + +<!-- vale gitlab.Spelling = YES --> diff --git a/doc/development/integrations/jenkins.md b/doc/development/integrations/jenkins.md index 2dce6956958..a1ad259319d 100644 --- a/doc/development/integrations/jenkins.md +++ b/doc/development/integrations/jenkins.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/development/integrations/jira_connect.md b/doc/development/integrations/jira_connect.md index 98a48007238..e38ab8b19d5 100644 --- a/doc/development/integrations/jira_connect.md +++ b/doc/development/integrations/jira_connect.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/development/service_ping/metrics_dictionary.md b/doc/development/service_ping/metrics_dictionary.md index 5dec8d53079..b969286e053 100644 --- a/doc/development/service_ping/metrics_dictionary.md +++ b/doc/development/service_ping/metrics_dictionary.md @@ -41,7 +41,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields: | `instrumentation_class` | no | `string`; [the class that implements the metric](metrics_instrumentation.md). | | `distribution` | yes | `array`; may be set to one of `ce, ee` or `ee`. The [distribution](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the tracked feature is available. | | `performance_indicator_type` | no | `array`; may be set to one of [`gmau`, `smau`, `paid_gmau`, or `umau`](https://about.gitlab.com/handbook/business-technology/data-team/data-catalog/xmau-analysis/). | -| `tier` | yes | `array`; may be set to one of `free, premium, ultimate`, `premium, ultimate` or `ultimate`. The [tier]( https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/) where the tracked feature is available. | +| `tier` | yes | `array`; may contain one or a combination of `free`, `premium` or `ultimate`. The [tier]( https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/) where the tracked feature is available. | | `milestone` | no | The milestone when the metric is introduced. | | `milestone_removed` | no | The milestone when the metric is removed. | | `introduced_by_url` | no | The URL to the Merge Request that introduced the metric. | diff --git a/doc/integration/akismet.md b/doc/integration/akismet.md index 41da5f2298b..a652025387e 100644 --- a/doc/integration/akismet.md +++ b/doc/integration/akismet.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index 6b8b07c3707..34ee326d6d5 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/azure.md b/doc/integration/azure.md index 61a8290e664..dceb135ad89 100644 --- a/doc/integration/azure.md +++ b/doc/integration/azure.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md index a492b891248..44aca1ca6b1 100644 --- a/doc/integration/bitbucket.md +++ b/doc/integration/bitbucket.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/cas.md b/doc/integration/cas.md index 59f41ab4368..be54c31ec01 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/datadog.md b/doc/integration/datadog.md index 857d807ea04..e06cca19e60 100644 --- a/doc/integration/datadog.md +++ b/doc/integration/datadog.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 38bcc2b9932..19f789832b9 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index c901fdfdd10..ded89dd93a4 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/github.md b/doc/integration/github.md index 7459691831c..f3192e0af6c 100644 --- a/doc/integration/github.md +++ b/doc/integration/github.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md index 7e21685fd54..a0b438c9ffa 100644 --- a/doc/integration/gitlab.md +++ b/doc/integration/gitlab.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index fa0e79d4c0b..f0bcc00c0fa 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/google.md b/doc/integration/google.md index 0e4c9b78b5f..a08944f65f1 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/index.md b/doc/integration/index.md index f163b64c4ec..00b65263d32 100644 --- a/doc/integration/index.md +++ b/doc/integration/index.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments comments: false --- diff --git a/doc/integration/jenkins.md b/doc/integration/jenkins.md index b6d720d2714..8910e0978b0 100644 --- a/doc/integration/jenkins.md +++ b/doc/integration/jenkins.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md index 61d1deace4f..b7e4c4f0e26 100644 --- a/doc/integration/jenkins_deprecated.md +++ b/doc/integration/jenkins_deprecated.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/configure.md b/doc/integration/jira/configure.md index d1aab144aa5..b11f367258d 100644 --- a/doc/integration/jira/configure.md +++ b/doc/integration/jira/configure.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index 9181547ce33..d8b1e9aa867 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/development_panel.md b/doc/integration/jira/development_panel.md index 5eeb6818fd2..9005ded8a0a 100644 --- a/doc/integration/jira/development_panel.md +++ b/doc/integration/jira/development_panel.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index d69243e50a6..38817f6921b 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/index.md b/doc/integration/jira/index.md index 86898247e54..0be2cab6d76 100644 --- a/doc/integration/jira/index.md +++ b/doc/integration/jira/index.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/issues.md b/doc/integration/jira/issues.md index 06b0afb55bb..060995d9f13 100644 --- a/doc/integration/jira/issues.md +++ b/doc/integration/jira/issues.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/jira_cloud_configuration.md b/doc/integration/jira/jira_cloud_configuration.md index 37edd34b34d..e42a102e030 100644 --- a/doc/integration/jira/jira_cloud_configuration.md +++ b/doc/integration/jira/jira_cloud_configuration.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/jira/jira_server_configuration.md b/doc/integration/jira/jira_server_configuration.md index 395ed6fdc97..52e7e5e412b 100644 --- a/doc/integration/jira/jira_server_configuration.md +++ b/doc/integration/jira/jira_server_configuration.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/oauth2_generic.md b/doc/integration/oauth2_generic.md index 84490757c16..867108d4597 100644 --- a/doc/integration/oauth2_generic.md +++ b/doc/integration/oauth2_generic.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index d5f49041f41..211c5947287 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/openid_connect_provider.md b/doc/integration/openid_connect_provider.md index b37c5064063..84457485382 100644 --- a/doc/integration/openid_connect_provider.md +++ b/doc/integration/openid_connect_provider.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md index 9ffc89e2c13..656ed8b8647 100644 --- a/doc/integration/recaptcha.md +++ b/doc/integration/recaptcha.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md index 102b89298a1..56d9feb14e0 100644 --- a/doc/integration/salesforce.md +++ b/doc/integration/salesforce.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/slash_commands.md b/doc/integration/slash_commands.md index 1f2259a2d57..b9b5f394e3c 100644 --- a/doc/integration/slash_commands.md +++ b/doc/integration/slash_commands.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/trello_power_up.md b/doc/integration/trello_power_up.md index e8956271508..df1d9270bd5 100644 --- a/doc/integration/trello_power_up.md +++ b/doc/integration/trello_power_up.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index 58e111be73c..1d711ea271e 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/admin_area/settings/project_integration_management.md b/doc/user/admin_area/settings/project_integration_management.md index 3140eecfa53..b8f9fef41ec 100644 --- a/doc/user/admin_area/settings/project_integration_management.md +++ b/doc/user/admin_area/settings/project_integration_management.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/asana.md b/doc/user/project/integrations/asana.md index b9552fff110..e1e926da19b 100644 --- a/doc/user/project/integrations/asana.md +++ b/doc/user/project/integrations/asana.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md index 64a11ac3532..58cfd8c3a2f 100644 --- a/doc/user/project/integrations/bamboo.md +++ b/doc/user/project/integrations/bamboo.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md index e8427e36015..a54a3adc408 100644 --- a/doc/user/project/integrations/bugzilla.md +++ b/doc/user/project/integrations/bugzilla.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/custom_issue_tracker.md b/doc/user/project/integrations/custom_issue_tracker.md index 19beafd6663..eaab1933b79 100644 --- a/doc/user/project/integrations/custom_issue_tracker.md +++ b/doc/user/project/integrations/custom_issue_tracker.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/discord_notifications.md b/doc/user/project/integrations/discord_notifications.md index 2ec657eec22..c9333b879f3 100644 --- a/doc/user/project/integrations/discord_notifications.md +++ b/doc/user/project/integrations/discord_notifications.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md index 3ef4a4e5004..33c197b962e 100644 --- a/doc/user/project/integrations/emails_on_push.md +++ b/doc/user/project/integrations/emails_on_push.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/ewm.md b/doc/user/project/integrations/ewm.md index 5b0059673ad..bc9b2d59db3 100644 --- a/doc/user/project/integrations/ewm.md +++ b/doc/user/project/integrations/ewm.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/github.md b/doc/user/project/integrations/github.md index 019ca9da9f1..6b342392bdf 100644 --- a/doc/user/project/integrations/github.md +++ b/doc/user/project/integrations/github.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/gitlab_slack_application.md b/doc/user/project/integrations/gitlab_slack_application.md index ac70c7e4b4e..0d8ea636eba 100644 --- a/doc/user/project/integrations/gitlab_slack_application.md +++ b/doc/user/project/integrations/gitlab_slack_application.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md index d5dc02d5455..bcaedbc4b10 100644 --- a/doc/user/project/integrations/hangouts_chat.md +++ b/doc/user/project/integrations/hangouts_chat.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md index f9e15ced858..6f86098b33d 100644 --- a/doc/user/project/integrations/index.md +++ b/doc/user/project/integrations/index.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 79df556ac8f..b96605ff5c9 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index 18ff6e324e3..92e5feefb73 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md index 619ae52481b..7ed94fb53d8 100644 --- a/doc/user/project/integrations/mattermost_slash_commands.md +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/microsoft_teams.md b/doc/user/project/integrations/microsoft_teams.md index 795ead573f2..192fe8c92ba 100644 --- a/doc/user/project/integrations/microsoft_teams.md +++ b/doc/user/project/integrations/microsoft_teams.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md index 934510fd155..631c53dcc44 100644 --- a/doc/user/project/integrations/mock_ci.md +++ b/doc/user/project/integrations/mock_ci.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index d03afac3221..13def74450c 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/pivotal_tracker.md b/doc/user/project/integrations/pivotal_tracker.md index c2c827c240b..d464007dd5e 100644 --- a/doc/user/project/integrations/pivotal_tracker.md +++ b/doc/user/project/integrations/pivotal_tracker.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md index 77e6eb75b9f..05d7c31a288 100644 --- a/doc/user/project/integrations/redmine.md +++ b/doc/user/project/integrations/redmine.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/servicenow.md b/doc/user/project/integrations/servicenow.md index bdc05552c31..fdcbb498621 100644 --- a/doc/user/project/integrations/servicenow.md +++ b/doc/user/project/integrations/servicenow.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md index 0e1c6abc052..5db4e839b54 100644 --- a/doc/user/project/integrations/slack.md +++ b/doc/user/project/integrations/slack.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md index 4f206cd3e45..dfebf9a1123 100644 --- a/doc/user/project/integrations/slack_slash_commands.md +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/unify_circuit.md b/doc/user/project/integrations/unify_circuit.md index 3e5e368722e..2e166e87ff5 100644 --- a/doc/user/project/integrations/unify_circuit.md +++ b/doc/user/project/integrations/unify_circuit.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/webex_teams.md b/doc/user/project/integrations/webex_teams.md index 2851fe0b299..3632fdf0e0c 100644 --- a/doc/user/project/integrations/webex_teams.md +++ b/doc/user/project/integrations/webex_teams.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index cebfbbe45a7..35e85f58b0e 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/doc/user/project/integrations/youtrack.md b/doc/user/project/integrations/youtrack.md index f39c34ccc0a..eda0874ac08 100644 --- a/doc/user/project/integrations/youtrack.md +++ b/doc/user/project/integrations/youtrack.md @@ -1,6 +1,6 @@ --- -stage: Create -group: Ecosystem +stage: Ecosystem +group: Integrations info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- diff --git a/lib/gitlab/ci/parsers.rb b/lib/gitlab/ci/parsers.rb index 3469537a2e2..1223d664214 100644 --- a/lib/gitlab/ci/parsers.rb +++ b/lib/gitlab/ci/parsers.rb @@ -11,7 +11,9 @@ module Gitlab cobertura: ::Gitlab::Ci::Parsers::Coverage::Cobertura, terraform: ::Gitlab::Ci::Parsers::Terraform::Tfplan, accessibility: ::Gitlab::Ci::Parsers::Accessibility::Pa11y, - codequality: ::Gitlab::Ci::Parsers::Codequality::CodeClimate + codequality: ::Gitlab::Ci::Parsers::Codequality::CodeClimate, + sast: ::Gitlab::Ci::Parsers::Security::Sast, + secret_detection: ::Gitlab::Ci::Parsers::Security::SecretDetection } end diff --git a/lib/gitlab/ci/parsers/security/common.rb b/lib/gitlab/ci/parsers/security/common.rb new file mode 100644 index 00000000000..a4cbff3f3a2 --- /dev/null +++ b/lib/gitlab/ci/parsers/security/common.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Parsers + module Security + class Common + SecurityReportParserError = Class.new(Gitlab::Ci::Parsers::ParserError) + + def self.parse!(json_data, report, vulnerability_finding_signatures_enabled = false, validate: false) + new(json_data, report, vulnerability_finding_signatures_enabled, validate: validate).parse! + end + + def initialize(json_data, report, vulnerability_finding_signatures_enabled = false, validate: false) + @json_data = json_data + @report = report + @validate = validate + @vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled + end + + def parse! + return report_data unless valid? + + raise SecurityReportParserError, "Invalid report format" unless report_data.is_a?(Hash) + + create_scanner + create_scan + create_analyzer + set_report_version + + create_vulnerabilities + + report_data + rescue JSON::ParserError + raise SecurityReportParserError, 'JSON parsing failed' + rescue StandardError => e + Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e) + raise SecurityReportParserError, "#{report.type} security report parsing failed" + end + + private + + attr_reader :json_data, :report, :validate + + def valid? + return true if !validate || schema_validator.valid? + + schema_validator.errors.each { |error| report.add_error('Schema', error) } + + false + end + + def schema_validator + @schema_validator ||= ::Gitlab::Ci::Parsers::Security::Validators::SchemaValidator.new(report.type, report_data) + end + + def report_data + @report_data ||= Gitlab::Json.parse!(json_data) + end + + def report_version + @report_version ||= report_data['version'] + end + + def top_level_scanner + @top_level_scanner ||= report_data.dig('scan', 'scanner') + end + + def scan_data + @scan_data ||= report_data.dig('scan') + end + + def analyzer_data + @analyzer_data ||= report_data.dig('scan', 'analyzer') + end + + def tracking_data(data) + data['tracking'] + end + + def create_vulnerabilities + if report_data["vulnerabilities"] + report_data["vulnerabilities"].each { |vulnerability| create_vulnerability(vulnerability) } + end + end + + def create_vulnerability(data, remediations = []) + identifiers = create_identifiers(data['identifiers']) + links = create_links(data['links']) + location = create_location(data['location'] || {}) + signatures = create_signatures(tracking_data(data)) + + if @vulnerability_finding_signatures_enabled && !signatures.empty? + # NOT the signature_sha - the compare key is hashed + # to create the project_fingerprint + highest_priority_signature = signatures.max_by(&:priority) + uuid = calculate_uuid_v5(identifiers.first, highest_priority_signature.signature_hex) + else + uuid = calculate_uuid_v5(identifiers.first, location&.fingerprint) + end + + report.add_finding( + ::Gitlab::Ci::Reports::Security::Finding.new( + uuid: uuid, + report_type: report.type, + name: finding_name(data, identifiers, location), + compare_key: data['cve'] || '', + location: location, + severity: parse_severity_level(data['severity']), + confidence: parse_confidence_level(data['confidence']), + scanner: create_scanner(data['scanner']), + scan: report&.scan, + identifiers: identifiers, + links: links, + remediations: remediations, + raw_metadata: data.to_json, + metadata_version: report_version, + details: data['details'] || {}, + signatures: signatures, + project_id: report.project_id, + vulnerability_finding_signatures_enabled: @vulnerability_finding_signatures_enabled)) + end + + def create_signatures(tracking) + tracking ||= { 'items' => [] } + + signature_algorithms = Hash.new { |hash, key| hash[key] = [] } + + tracking['items'].each do |item| + next unless item.key?('signatures') + + item['signatures'].each do |signature| + alg = signature['algorithm'] + signature_algorithms[alg] << signature['value'] + end + end + + signature_algorithms.map do |algorithm, values| + value = values.join('|') + signature = ::Gitlab::Ci::Reports::Security::FindingSignature.new( + algorithm_type: algorithm, + signature_value: value + ) + + if signature.valid? + signature + else + e = SecurityReportParserError.new("Vulnerability tracking signature is not valid: #{signature}") + Gitlab::ErrorTracking.track_exception(e) + nil + end + end.compact + end + + def create_scan + return unless scan_data.is_a?(Hash) + + report.scan = ::Gitlab::Ci::Reports::Security::Scan.new(scan_data) + end + + def set_report_version + report.version = report_version + end + + def create_analyzer + return unless analyzer_data.is_a?(Hash) + + params = { + id: analyzer_data.dig('id'), + name: analyzer_data.dig('name'), + version: analyzer_data.dig('version'), + vendor: analyzer_data.dig('vendor', 'name') + } + + return unless params.values.all? + + report.analyzer = ::Gitlab::Ci::Reports::Security::Analyzer.new(**params) + end + + def create_scanner(scanner_data = top_level_scanner) + return unless scanner_data.is_a?(Hash) + + report.add_scanner( + ::Gitlab::Ci::Reports::Security::Scanner.new( + external_id: scanner_data['id'], + name: scanner_data['name'], + vendor: scanner_data.dig('vendor', 'name'), + version: scanner_data.dig('version'))) + end + + def create_identifiers(identifiers) + return [] unless identifiers.is_a?(Array) + + identifiers.map { |identifier| create_identifier(identifier) }.compact + end + + def create_identifier(identifier) + return unless identifier.is_a?(Hash) + + report.add_identifier( + ::Gitlab::Ci::Reports::Security::Identifier.new( + external_type: identifier['type'], + external_id: identifier['value'], + name: identifier['name'], + url: identifier['url'])) + end + + def create_links(links) + return [] unless links.is_a?(Array) + + links.map { |link| create_link(link) }.compact + end + + def create_link(link) + return unless link.is_a?(Hash) + + ::Gitlab::Ci::Reports::Security::Link.new(name: link['name'], url: link['url']) + end + + def parse_severity_level(input) + input&.downcase.then { |value| ::Enums::Vulnerability.severity_levels.key?(value) ? value : 'unknown' } + end + + def parse_confidence_level(input) + input&.downcase.then { |value| ::Enums::Vulnerability.confidence_levels.key?(value) ? value : 'unknown' } + end + + def create_location(location_data) + raise NotImplementedError + end + + def finding_name(data, identifiers, location) + return data['message'] if data['message'].present? + return data['name'] if data['name'].present? + + identifier = identifiers.find(&:cve?) || identifiers.find(&:cwe?) || identifiers.first + "#{identifier.name} in #{location&.fingerprint_path}" + end + + def calculate_uuid_v5(primary_identifier, location_fingerprint) + uuid_v5_name_components = { + report_type: report.type, + primary_identifier_fingerprint: primary_identifier&.fingerprint, + location_fingerprint: location_fingerprint, + project_id: report.project_id + } + + if uuid_v5_name_components.values.any?(&:nil?) + Gitlab::AppLogger.warn(message: "One or more UUID name components are nil", components: uuid_v5_name_components) + return + end + + ::Security::VulnerabilityUUID.generate( + report_type: uuid_v5_name_components[:report_type], + primary_identifier_fingerprint: uuid_v5_name_components[:primary_identifier_fingerprint], + location_fingerprint: uuid_v5_name_components[:location_fingerprint], + project_id: uuid_v5_name_components[:project_id] + ) + end + end + end + end + end +end + +Gitlab::Ci::Parsers::Security::Common.prepend_mod_with("Gitlab::Ci::Parsers::Security::Common") diff --git a/lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb b/lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb new file mode 100644 index 00000000000..24613a441be --- /dev/null +++ b/lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Parsers + module Security + module Concerns + module DeprecatedSyntax + extend ActiveSupport::Concern + + included do + extend ::Gitlab::Utils::Override + + override :parse_report + end + + def report_data + @report_data ||= begin + data = super + + if data.is_a?(Array) + data = { + "version" => self.class::DEPRECATED_REPORT_VERSION, + "vulnerabilities" => data + } + end + + data + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/parsers/security/sast.rb b/lib/gitlab/ci/parsers/security/sast.rb new file mode 100644 index 00000000000..e3c62614cd8 --- /dev/null +++ b/lib/gitlab/ci/parsers/security/sast.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Parsers + module Security + class Sast < Common + include Security::Concerns::DeprecatedSyntax + + DEPRECATED_REPORT_VERSION = "1.2" + + private + + def create_location(location_data) + ::Gitlab::Ci::Reports::Security::Locations::Sast.new( + file_path: location_data['file'], + start_line: location_data['start_line'], + end_line: location_data['end_line'], + class_name: location_data['class'], + method_name: location_data['method']) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/parsers/security/secret_detection.rb b/lib/gitlab/ci/parsers/security/secret_detection.rb new file mode 100644 index 00000000000..c6d95c1d391 --- /dev/null +++ b/lib/gitlab/ci/parsers/security/secret_detection.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Parsers + module Security + class SecretDetection < Common + include Security::Concerns::DeprecatedSyntax + + DEPRECATED_REPORT_VERSION = "1.2" + + private + + def create_location(location_data) + ::Gitlab::Ci::Reports::Security::Locations::SecretDetection.new( + file_path: location_data['file'], + start_line: location_data['start_line'], + end_line: location_data['end_line'], + class_name: location_data['class'], + method_name: location_data['method'] + ) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/parsers/security/validators/schema_validator.rb b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb new file mode 100644 index 00000000000..3d92886cba8 --- /dev/null +++ b/lib/gitlab/ci/parsers/security/validators/schema_validator.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Parsers + module Security + module Validators + class SchemaValidator + class Schema + def root_path + File.join(__dir__, 'schemas') + end + + def initialize(report_type) + @report_type = report_type + end + + delegate :validate, to: :schemer + + private + + attr_reader :report_type + + def schemer + JSONSchemer.schema(pathname) + end + + def pathname + Pathname.new(schema_path) + end + + def schema_path + File.join(root_path, file_name) + end + + def file_name + "#{report_type}.json" + end + end + + def initialize(report_type, report_data) + @report_type = report_type + @report_data = report_data + end + + def valid? + errors.empty? + end + + def errors + @errors ||= schema.validate(report_data).map { |error| JSONSchemer::Errors.pretty(error) } + end + + private + + attr_reader :report_type, :report_data + + def schema + Schema.new(report_type) + end + end + end + end + end + end +end + +Gitlab::Ci::Parsers::Security::Validators::SchemaValidator::Schema.prepend_mod_with("Gitlab::Ci::Parsers::Security::Validators::SchemaValidator::Schema") diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/sast.json b/lib/gitlab/ci/parsers/security/validators/schemas/sast.json new file mode 100644 index 00000000000..a7159be0190 --- /dev/null +++ b/lib/gitlab/ci/parsers/security/validators/schemas/sast.json @@ -0,0 +1,706 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Report format for GitLab SAST", + "description": "This schema provides the report format for Static Application Security Testing analyzers (https://docs.gitlab.com/ee/user/application_security/sast).", + "definitions": { + "detail_type": { + "oneOf": [ + { + "$ref": "#/definitions/named_list" + }, + { + "$ref": "#/definitions/list" + }, + { + "$ref": "#/definitions/table" + }, + { + "$ref": "#/definitions/text" + }, + { + "$ref": "#/definitions/url" + }, + { + "$ref": "#/definitions/code" + }, + { + "$ref": "#/definitions/value" + }, + { + "$ref": "#/definitions/diff" + }, + { + "$ref": "#/definitions/markdown" + }, + { + "$ref": "#/definitions/commit" + }, + { + "$ref": "#/definitions/file_location" + }, + { + "$ref": "#/definitions/module_location" + } + ] + }, + "text_value": { + "type": "string" + }, + "named_field": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "$ref": "#/definitions/text_value", + "minLength": 1 + }, + "description": { + "$ref": "#/definitions/text_value" + } + } + }, + "named_list": { + "type": "object", + "description": "An object with named and typed fields", + "required": [ + "type", + "items" + ], + "properties": { + "type": { + "const": "named-list" + }, + "items": { + "type": "object", + "patternProperties": { + "^.*$": { + "allOf": [ + { + "$ref": "#/definitions/named_field" + }, + { + "$ref": "#/definitions/detail_type" + } + ] + } + } + } + } + }, + "list": { + "type": "object", + "description": "A list of typed fields", + "required": [ + "type", + "items" + ], + "properties": { + "type": { + "const": "list" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/detail_type" + } + } + } + }, + "table": { + "type": "object", + "description": "A table of typed fields", + "required": [ + "type", + "rows" + ], + "properties": { + "type": { + "const": "table" + }, + "header": { + "type": "array", + "items": { + "$ref": "#/definitions/detail_type" + } + }, + "rows": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/detail_type" + } + } + } + } + }, + "text": { + "type": "object", + "description": "Raw text", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "text" + }, + "value": { + "$ref": "#/definitions/text_value" + } + } + }, + "url": { + "type": "object", + "description": "A single URL", + "required": [ + "type", + "href" + ], + "properties": { + "type": { + "const": "url" + }, + "text": { + "$ref": "#/definitions/text_value" + }, + "href": { + "type": "string", + "minLength": 1, + "examples": [ + "http://mysite.com" + ] + } + } + }, + "code": { + "type": "object", + "description": "A codeblock", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "code" + }, + "value": { + "type": "string" + }, + "lang": { + "type": "string", + "description": "A programming language" + } + } + }, + "value": { + "type": "object", + "description": "A field that can store a range of types of value", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "value" + }, + "value": { + "type": [ + "number", + "string", + "boolean" + ] + } + } + }, + "diff": { + "type": "object", + "description": "A diff", + "required": [ + "type", + "before", + "after" + ], + "properties": { + "type": { + "const": "diff" + }, + "before": { + "type": "string" + }, + "after": { + "type": "string" + } + } + }, + "markdown": { + "type": "object", + "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "markdown" + }, + "value": { + "$ref": "#/definitions/text_value", + "examples": [ + "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)" + ] + } + } + }, + "commit": { + "type": "object", + "description": "A commit/tag/branch within the GitLab project", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "commit" + }, + "value": { + "type": "string", + "description": "The commit SHA", + "minLength": 1 + } + } + }, + "file_location": { + "type": "object", + "description": "A location within a file in the project", + "required": [ + "type", + "file_name", + "line_start" + ], + "properties": { + "type": { + "const": "file-location" + }, + "file_name": { + "type": "string", + "minLength": 1 + }, + "line_start": { + "type": "integer" + }, + "line_end": { + "type": "integer" + } + } + }, + "module_location": { + "type": "object", + "description": "A location within a binary module of the form module+relative_offset", + "required": [ + "type", + "module_name", + "offset" + ], + "properties": { + "type": { + "const": "module-location" + }, + "module_name": { + "type": "string", + "minLength": 1, + "examples": [ + "compiled_binary" + ] + }, + "offset": { + "type": "integer", + "examples": [ + 100 + ] + } + } + } + }, + "self": { + "version": "14.0.0" + }, + "required": [ + "version", + "vulnerabilities" + ], + "additionalProperties": true, + "properties": { + "scan": { + "type": "object", + "required": [ + "end_time", + "scanner", + "start_time", + "status", + "type" + ], + "properties": { + "end_time": { + "type": "string", + "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$", + "examples": [ + "2020-01-28T03:26:02" + ] + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "description": "Communication intended for the initiator of a scan.", + "required": [ + "level", + "value" + ], + "properties": { + "level": { + "type": "string", + "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.", + "enum": [ + "info", + "warn", + "fatal" + ], + "examples": [ + "info" + ] + }, + "value": { + "type": "string", + "description": "The message to communicate.", + "minLength": 1, + "examples": [ + "Permission denied, scanning aborted" + ] + } + } + } + }, + "scanner": { + "type": "object", + "description": "Object defining the scanner used to perform the scan.", + "required": [ + "id", + "name", + "version", + "vendor" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique id that identifies the scanner.", + "minLength": 1, + "examples": [ + "my-sast-scanner" + ] + }, + "name": { + "type": "string", + "description": "A human readable value that identifies the scanner, not required to be unique.", + "minLength": 1, + "examples": [ + "My SAST Scanner" + ] + }, + "url": { + "type": "string", + "description": "A link to more information about the scanner.", + "examples": [ + "https://scanner.url" + ] + }, + "version": { + "type": "string", + "description": "The version of the scanner.", + "minLength": 1, + "examples": [ + "1.0.2" + ] + }, + "vendor": { + "type": "object", + "description": "The vendor/maintainer of the scanner.", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the vendor.", + "minLength": 1, + "examples": [ + "GitLab" + ] + } + } + } + } + }, + "start_time": { + "type": "string", + "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$", + "examples": [ + "2020-02-14T16:01:59" + ] + }, + "status": { + "type": "string", + "description": "Result of the scan.", + "enum": [ + "success", + "failure" + ] + }, + "type": { + "type": "string", + "description": "Type of the scan.", + "enum": [ + "sast" + ] + } + } + }, + "schema": { + "type": "string", + "description": "URI pointing to the validating security report schema.", + "format": "uri" + }, + "version": { + "type": "string", + "description": "The version of the schema to which the JSON report conforms.", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "vulnerabilities": { + "type": "array", + "description": "Array of vulnerability objects.", + "items": { + "type": "object", + "description": "Describes the vulnerability.", + "required": [ + "category", + "cve", + "identifiers", + "location", + "scanner" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.", + "examples": [ + "642735a5-1425-428d-8d4e-3c854885a3c9" + ] + }, + "category": { + "type": "string", + "minLength": 1, + "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)." + }, + "name": { + "type": "string", + "description": "The name of the vulnerability. This must not include the finding's specific information." + }, + "message": { + "type": "string", + "description": "A short text section that describes the vulnerability. This may include the finding's specific information." + }, + "description": { + "type": "string", + "description": "A long text section describing the vulnerability more fully." + }, + "cve": { + "type": "string", + "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/." + }, + "severity": { + "type": "string", + "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.", + "enum": [ + "Info", + "Unknown", + "Low", + "Medium", + "High", + "Critical" + ] + }, + "confidence": { + "type": "string", + "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.", + "enum": [ + "Ignore", + "Unknown", + "Experimental", + "Low", + "Medium", + "High", + "Confirmed" + ] + }, + "solution": { + "type": "string", + "description": "Explanation of how to fix the vulnerability." + }, + "scanner": { + "description": "Describes the scanner used to find this vulnerability.", + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "The scanner's ID, as a snake_case string." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable name of the scanner." + } + } + }, + "identifiers": { + "type": "array", + "minItems": 1, + "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.", + "items": { + "type": "object", + "required": [ + "type", + "name", + "value" + ], + "properties": { + "type": { + "type": "string", + "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).", + "minLength": 1 + }, + "name": { + "type": "string", + "description": "Human-readable name of the identifier.", + "minLength": 1 + }, + "url": { + "type": "string", + "description": "URL of the identifier's documentation.", + "format": "uri" + }, + "value": { + "type": "string", + "description": "Value of the identifier, for matching purpose.", + "minLength": 1 + } + } + } + }, + "links": { + "type": "array", + "description": "An array of references to external documentation or articles that describe the vulnerability.", + "items": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the vulnerability details link." + }, + "url": { + "type": "string", + "description": "URL of the vulnerability details document.", + "format": "uri" + } + } + } + }, + "details": { + "$ref": "#/definitions/named_list/properties/items" + }, + "location": { + "type": "object", + "description": "Identifies the vulnerability's location.", + "properties": { + "file": { + "type": "string", + "description": "Path to the file where the vulnerability is located." + }, + "start_line": { + "type": "number", + "description": "The first line of the code affected by the vulnerability." + }, + "end_line": { + "type": "number", + "description": "The last line of the code affected by the vulnerability." + }, + "class": { + "type": "string", + "description": "Provides the name of the class where the vulnerability is located." + }, + "method": { + "type": "string", + "description": "Provides the name of the method where the vulnerability is located." + } + } + }, + "raw_source_code_extract": { + "type": "string", + "description": "Provides an unsanitized excerpt of the affected source code." + } + } + } + }, + "remediations": { + "type": "array", + "description": "An array of objects containing information on available remediations, along with patch diffs to apply.", + "items": { + "type": "object", + "required": [ + "fixes", + "summary", + "diff" + ], + "properties": { + "fixes": { + "type": "array", + "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.", + "items": { + "type": "object", + "required": [ + "cve" + ], + "properties": { + "cve": { + "type": "string", + "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/." + } + } + } + }, + "summary": { + "type": "string", + "minLength": 1, + "description": "An overview of how the vulnerabilities were fixed." + }, + "diff": { + "type": "string", + "minLength": 1, + "description": "A base64-encoded remediation code diff, compatible with git apply." + } + } + } + } + } +} diff --git a/lib/gitlab/ci/parsers/security/validators/schemas/secret_detection.json b/lib/gitlab/ci/parsers/security/validators/schemas/secret_detection.json new file mode 100644 index 00000000000..462e23a151c --- /dev/null +++ b/lib/gitlab/ci/parsers/security/validators/schemas/secret_detection.json @@ -0,0 +1,729 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Report format for GitLab Secret Detection", + "description": "This schema provides the the report format for the Secret Detection analyzer (https://docs.gitlab.com/ee/user/application_security/secret_detection)", + "definitions": { + "detail_type": { + "oneOf": [ + { + "$ref": "#/definitions/named_list" + }, + { + "$ref": "#/definitions/list" + }, + { + "$ref": "#/definitions/table" + }, + { + "$ref": "#/definitions/text" + }, + { + "$ref": "#/definitions/url" + }, + { + "$ref": "#/definitions/code" + }, + { + "$ref": "#/definitions/value" + }, + { + "$ref": "#/definitions/diff" + }, + { + "$ref": "#/definitions/markdown" + }, + { + "$ref": "#/definitions/commit" + }, + { + "$ref": "#/definitions/file_location" + }, + { + "$ref": "#/definitions/module_location" + } + ] + }, + "text_value": { + "type": "string" + }, + "named_field": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "$ref": "#/definitions/text_value", + "minLength": 1 + }, + "description": { + "$ref": "#/definitions/text_value" + } + } + }, + "named_list": { + "type": "object", + "description": "An object with named and typed fields", + "required": [ + "type", + "items" + ], + "properties": { + "type": { + "const": "named-list" + }, + "items": { + "type": "object", + "patternProperties": { + "^.*$": { + "allOf": [ + { + "$ref": "#/definitions/named_field" + }, + { + "$ref": "#/definitions/detail_type" + } + ] + } + } + } + } + }, + "list": { + "type": "object", + "description": "A list of typed fields", + "required": [ + "type", + "items" + ], + "properties": { + "type": { + "const": "list" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/detail_type" + } + } + } + }, + "table": { + "type": "object", + "description": "A table of typed fields", + "required": [ + "type", + "rows" + ], + "properties": { + "type": { + "const": "table" + }, + "header": { + "type": "array", + "items": { + "$ref": "#/definitions/detail_type" + } + }, + "rows": { + "type": "array", + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/detail_type" + } + } + } + } + }, + "text": { + "type": "object", + "description": "Raw text", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "text" + }, + "value": { + "$ref": "#/definitions/text_value" + } + } + }, + "url": { + "type": "object", + "description": "A single URL", + "required": [ + "type", + "href" + ], + "properties": { + "type": { + "const": "url" + }, + "text": { + "$ref": "#/definitions/text_value" + }, + "href": { + "type": "string", + "minLength": 1, + "examples": [ + "http://mysite.com" + ] + } + } + }, + "code": { + "type": "object", + "description": "A codeblock", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "code" + }, + "value": { + "type": "string" + }, + "lang": { + "type": "string", + "description": "A programming language" + } + } + }, + "value": { + "type": "object", + "description": "A field that can store a range of types of value", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "value" + }, + "value": { + "type": [ + "number", + "string", + "boolean" + ] + } + } + }, + "diff": { + "type": "object", + "description": "A diff", + "required": [ + "type", + "before", + "after" + ], + "properties": { + "type": { + "const": "diff" + }, + "before": { + "type": "string" + }, + "after": { + "type": "string" + } + } + }, + "markdown": { + "type": "object", + "description": "GitLab flavoured markdown, see https://docs.gitlab.com/ee/user/markdown.html", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "markdown" + }, + "value": { + "$ref": "#/definitions/text_value", + "examples": [ + "Here is markdown `inline code` #1 [test](gitlab.com)\n\n![GitLab Logo](https://about.gitlab.com/images/press/logo/preview/gitlab-logo-white-preview.png)" + ] + } + } + }, + "commit": { + "type": "object", + "description": "A commit/tag/branch within the GitLab project", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "const": "commit" + }, + "value": { + "type": "string", + "description": "The commit SHA", + "minLength": 1 + } + } + }, + "file_location": { + "type": "object", + "description": "A location within a file in the project", + "required": [ + "type", + "file_name", + "line_start" + ], + "properties": { + "type": { + "const": "file-location" + }, + "file_name": { + "type": "string", + "minLength": 1 + }, + "line_start": { + "type": "integer" + }, + "line_end": { + "type": "integer" + } + } + }, + "module_location": { + "type": "object", + "description": "A location within a binary module of the form module+relative_offset", + "required": [ + "type", + "module_name", + "offset" + ], + "properties": { + "type": { + "const": "module-location" + }, + "module_name": { + "type": "string", + "minLength": 1, + "examples": [ + "compiled_binary" + ] + }, + "offset": { + "type": "integer", + "examples": [ + 100 + ] + } + } + } + }, + "self": { + "version": "14.0.0" + }, + "required": [ + "version", + "vulnerabilities" + ], + "additionalProperties": true, + "properties": { + "scan": { + "type": "object", + "required": [ + "end_time", + "scanner", + "start_time", + "status", + "type" + ], + "properties": { + "end_time": { + "type": "string", + "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan finished.", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$", + "examples": [ + "2020-01-28T03:26:02" + ] + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "description": "Communication intended for the initiator of a scan.", + "required": [ + "level", + "value" + ], + "properties": { + "level": { + "type": "string", + "description": "Describes the severity of the communication. Use info to communicate normal scan behaviour; warn to communicate a potentially recoverable problem, or a partial error; fatal to communicate an issue that causes the scan to halt.", + "enum": [ + "info", + "warn", + "fatal" + ], + "examples": [ + "info" + ] + }, + "value": { + "type": "string", + "description": "The message to communicate.", + "minLength": 1, + "examples": [ + "Permission denied, scanning aborted" + ] + } + } + } + }, + "scanner": { + "type": "object", + "description": "Object defining the scanner used to perform the scan.", + "required": [ + "id", + "name", + "version", + "vendor" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique id that identifies the scanner.", + "minLength": 1, + "examples": [ + "my-sast-scanner" + ] + }, + "name": { + "type": "string", + "description": "A human readable value that identifies the scanner, not required to be unique.", + "minLength": 1, + "examples": [ + "My SAST Scanner" + ] + }, + "url": { + "type": "string", + "description": "A link to more information about the scanner.", + "examples": [ + "https://scanner.url" + ] + }, + "version": { + "type": "string", + "description": "The version of the scanner.", + "minLength": 1, + "examples": [ + "1.0.2" + ] + }, + "vendor": { + "type": "object", + "description": "The vendor/maintainer of the scanner.", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the vendor.", + "minLength": 1, + "examples": [ + "GitLab" + ] + } + } + } + } + }, + "start_time": { + "type": "string", + "description": "ISO8601 UTC value with format yyyy-mm-ddThh:mm:ss, representing when the scan started.", + "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}\\:\\d{2}\\:\\d{2}$", + "examples": [ + "2020-02-14T16:01:59" + ] + }, + "status": { + "type": "string", + "description": "Result of the scan.", + "enum": [ + "success", + "failure" + ] + }, + "type": { + "type": "string", + "description": "Type of the scan.", + "enum": [ + "secret_detection" + ] + } + } + }, + "schema": { + "type": "string", + "description": "URI pointing to the validating security report schema.", + "format": "uri" + }, + "version": { + "type": "string", + "description": "The version of the schema to which the JSON report conforms.", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + }, + "vulnerabilities": { + "type": "array", + "description": "Array of vulnerability objects.", + "items": { + "type": "object", + "description": "Describes the vulnerability.", + "required": [ + "category", + "cve", + "identifiers", + "location", + "scanner" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier of the vulnerability. This is recommended to be a UUID.", + "examples": [ + "642735a5-1425-428d-8d4e-3c854885a3c9" + ] + }, + "category": { + "type": "string", + "minLength": 1, + "description": "Describes where this vulnerability belongs (for example, SAST, Dependency Scanning, and so on)." + }, + "name": { + "type": "string", + "description": "The name of the vulnerability. This must not include the finding's specific information." + }, + "message": { + "type": "string", + "description": "A short text section that describes the vulnerability. This may include the finding's specific information." + }, + "description": { + "type": "string", + "description": "A long text section describing the vulnerability more fully." + }, + "cve": { + "type": "string", + "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/." + }, + "severity": { + "type": "string", + "description": "How much the vulnerability impacts the software. Possible values are Info, Unknown, Low, Medium, High, or Critical. Note that some analyzers may not report all these possible values.", + "enum": [ + "Info", + "Unknown", + "Low", + "Medium", + "High", + "Critical" + ] + }, + "confidence": { + "type": "string", + "description": "How reliable the vulnerability's assessment is. Possible values are Ignore, Unknown, Experimental, Low, Medium, High, and Confirmed. Note that some analyzers may not report all these possible values.", + "enum": [ + "Ignore", + "Unknown", + "Experimental", + "Low", + "Medium", + "High", + "Confirmed" + ] + }, + "solution": { + "type": "string", + "description": "Explanation of how to fix the vulnerability." + }, + "scanner": { + "description": "Describes the scanner used to find this vulnerability.", + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string", + "minLength": 1, + "description": "The scanner's ID, as a snake_case string." + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Human-readable name of the scanner." + } + } + }, + "identifiers": { + "type": "array", + "minItems": 1, + "description": "An ordered array of references that identify a vulnerability on internal or external databases. The first identifier is the Primary Identifier, which has special meaning.", + "items": { + "type": "object", + "required": [ + "type", + "name", + "value" + ], + "properties": { + "type": { + "type": "string", + "description": "for example, cve, cwe, osvdb, usn, or an analyzer-dependent type such as gemnasium).", + "minLength": 1 + }, + "name": { + "type": "string", + "description": "Human-readable name of the identifier.", + "minLength": 1 + }, + "url": { + "type": "string", + "description": "URL of the identifier's documentation.", + "format": "uri" + }, + "value": { + "type": "string", + "description": "Value of the identifier, for matching purpose.", + "minLength": 1 + } + } + } + }, + "links": { + "type": "array", + "description": "An array of references to external documentation or articles that describe the vulnerability.", + "items": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the vulnerability details link." + }, + "url": { + "type": "string", + "description": "URL of the vulnerability details document.", + "format": "uri" + } + } + } + }, + "details": { + "$ref": "#/definitions/named_list/properties/items" + }, + "location": { + "required": [ + "commit" + ], + "properties": { + "file": { + "type": "string", + "description": "Path to the file where the vulnerability is located" + }, + "commit": { + "type": "object", + "description": "Represents the commit in which the vulnerability was detected", + "required": [ + "sha" + ], + "properties": { + "author": { + "type": "string" + }, + "date": { + "type": "string" + }, + "message": { + "type": "string" + }, + "sha": { + "type": "string", + "minLength": 1 + } + } + }, + "start_line": { + "type": "number", + "description": "The first line of the code affected by the vulnerability" + }, + "end_line": { + "type": "number", + "description": "The last line of the code affected by the vulnerability" + }, + "class": { + "type": "string", + "description": "Provides the name of the class where the vulnerability is located" + }, + "method": { + "type": "string", + "description": "Provides the name of the method where the vulnerability is located" + } + } + }, + "raw_source_code_extract": { + "type": "string", + "description": "Provides an unsanitized excerpt of the affected source code." + } + } + } + }, + "remediations": { + "type": "array", + "description": "An array of objects containing information on available remediations, along with patch diffs to apply.", + "items": { + "type": "object", + "required": [ + "fixes", + "summary", + "diff" + ], + "properties": { + "fixes": { + "type": "array", + "description": "An array of strings that represent references to vulnerabilities fixed by this remediation.", + "items": { + "type": "object", + "required": [ + "cve" + ], + "properties": { + "cve": { + "type": "string", + "description": "(Deprecated - use vulnerabilities[].id instead) A fingerprint string value that represents a concrete finding. This is used to determine whether two findings are same, which may not be 100% accurate. Note that this is NOT a CVE as described by https://cve.mitre.org/." + } + } + } + }, + "summary": { + "type": "string", + "minLength": 1, + "description": "An overview of how the vulnerabilities were fixed." + }, + "diff": { + "type": "string", + "minLength": 1, + "description": "A base64-encoded remediation code diff, compatible with git apply." + } + } + } + } + } +} diff --git a/lib/gitlab/ci/reports/security/aggregated_report.rb b/lib/gitlab/ci/reports/security/aggregated_report.rb new file mode 100644 index 00000000000..a8bb2196043 --- /dev/null +++ b/lib/gitlab/ci/reports/security/aggregated_report.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Used to represent combined Security Reports. This is typically done for vulnerability deduplication purposes. + +module Gitlab + module Ci + module Reports + module Security + class AggregatedReport + attr_reader :findings + + def initialize(reports, findings) + @reports = reports + @findings = findings + end + + def created_at + @reports.map(&:created_at).compact.min + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb new file mode 100644 index 00000000000..2fc466e356d --- /dev/null +++ b/lib/gitlab/ci/reports/security/finding.rb @@ -0,0 +1,151 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Finding + include ::VulnerabilityFindingHelpers + + UNSAFE_SEVERITIES = %w[unknown high critical].freeze + + attr_reader :compare_key + attr_reader :confidence + attr_reader :identifiers + attr_reader :links + attr_reader :location + attr_reader :metadata_version + attr_reader :name + attr_reader :old_location + attr_reader :project_fingerprint + attr_reader :raw_metadata + attr_reader :report_type + attr_reader :scanner + attr_reader :scan + attr_reader :severity + attr_reader :uuid + attr_reader :remediations + attr_reader :details + attr_reader :signatures + attr_reader :project_id + + delegate :file_path, :start_line, :end_line, to: :location + + def initialize(compare_key:, identifiers:, links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists + @compare_key = compare_key + @confidence = confidence + @identifiers = identifiers + @links = links + @location = location + @metadata_version = metadata_version + @name = name + @raw_metadata = raw_metadata + @report_type = report_type + @scanner = scanner + @scan = scan + @severity = severity + @uuid = uuid + @remediations = remediations + @details = details + @signatures = signatures + @project_id = project_id + @vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled + + @project_fingerprint = generate_project_fingerprint + end + + def to_hash + %i[ + compare_key + confidence + identifiers + links + location + metadata_version + name + project_fingerprint + raw_metadata + report_type + scanner + scan + severity + uuid + details + signatures + ].each_with_object({}) do |key, hash| + hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def primary_identifier + identifiers.first + end + + def update_location(new_location) + @old_location = location + @location = new_location + end + + def unsafe? + severity.in?(UNSAFE_SEVERITIES) + end + + def eql?(other) + return false unless report_type == other.report_type && primary_identifier_fingerprint == other.primary_identifier_fingerprint + + if @vulnerability_finding_signatures_enabled + matches_signatures(other.signatures, other.uuid) + else + location.fingerprint == other.location.fingerprint + end + end + + def hash + if @vulnerability_finding_signatures_enabled && !signatures.empty? + highest_signature = signatures.max_by(&:priority) + report_type.hash ^ highest_signature.signature_hex.hash ^ primary_identifier_fingerprint.hash + else + report_type.hash ^ location.fingerprint.hash ^ primary_identifier_fingerprint.hash + end + end + + def valid? + scanner.present? && primary_identifier.present? && location.present? && uuid.present? + end + + def keys + @keys ||= identifiers.reject(&:type_identifier?).map do |identifier| + FindingKey.new(location_fingerprint: location&.fingerprint, identifier_fingerprint: identifier.fingerprint) + end + end + + def primary_identifier_fingerprint + primary_identifier&.fingerprint + end + + def <=>(other) + if severity == other.severity + compare_key <=> other.compare_key + else + ::Enums::Vulnerability.severity_levels[other.severity] <=> + ::Enums::Vulnerability.severity_levels[severity] + end + end + + def scanner_order_to(other) + return 1 unless scanner + return -1 unless other&.scanner + + scanner <=> other.scanner + end + + private + + def generate_project_fingerprint + Digest::SHA1.hexdigest(compare_key) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/finding_key.rb b/lib/gitlab/ci/reports/security/finding_key.rb new file mode 100644 index 00000000000..0acd923a60f --- /dev/null +++ b/lib/gitlab/ci/reports/security/finding_key.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class FindingKey + def initialize(location_fingerprint:, identifier_fingerprint:) + @location_fingerprint = location_fingerprint + @identifier_fingerprint = identifier_fingerprint + end + + def ==(other) + has_fingerprints? && other.has_fingerprints? && + location_fingerprint == other.location_fingerprint && + identifier_fingerprint == other.identifier_fingerprint + end + + def hash + location_fingerprint.hash ^ identifier_fingerprint.hash + end + + alias_method :eql?, :== + + protected + + attr_reader :location_fingerprint, :identifier_fingerprint + + def has_fingerprints? + location_fingerprint.present? && identifier_fingerprint.present? + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/finding_signature.rb b/lib/gitlab/ci/reports/security/finding_signature.rb new file mode 100644 index 00000000000..d1d7ef5c377 --- /dev/null +++ b/lib/gitlab/ci/reports/security/finding_signature.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class FindingSignature + include VulnerabilityFindingSignatureHelpers + + attr_accessor :algorithm_type, :signature_value + + def initialize(params = {}) + @algorithm_type = params.dig(:algorithm_type) + @signature_value = params.dig(:signature_value) + end + + def signature_sha + Digest::SHA1.digest(signature_value) + end + + def signature_hex + signature_sha.unpack1("H*") + end + + def to_hash + { + algorithm_type: algorithm_type, + signature_sha: signature_sha + } + end + + def valid? + algorithm_types.key?(algorithm_type) + end + + def eql?(other) + other.algorithm_type == algorithm_type && + other.signature_sha == signature_sha + end + + alias_method :==, :eql? + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/locations/base.rb b/lib/gitlab/ci/reports/security/locations/base.rb new file mode 100644 index 00000000000..9ad1d81287f --- /dev/null +++ b/lib/gitlab/ci/reports/security/locations/base.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + module Locations + class Base + include ::Gitlab::Utils::StrongMemoize + + def ==(other) + other.fingerprint == fingerprint + end + + def fingerprint + strong_memoize(:fingerprint) do + Digest::SHA1.hexdigest(fingerprint_data) + end + end + + def as_json(options = nil) + fingerprint # side-effect call to initialize the ivar for serialization + + super + end + + def fingerprint_path + fingerprint_data + end + + private + + def fingerprint_data + raise NotImplementedError + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/locations/sast.rb b/lib/gitlab/ci/reports/security/locations/sast.rb new file mode 100644 index 00000000000..23ffa91e720 --- /dev/null +++ b/lib/gitlab/ci/reports/security/locations/sast.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + module Locations + class Sast < Base + include Security::Concerns::FingerprintPathFromFile + + attr_reader :class_name + attr_reader :end_line + attr_reader :file_path + attr_reader :method_name + attr_reader :start_line + + def initialize(file_path:, start_line:, end_line: nil, class_name: nil, method_name: nil) + @class_name = class_name + @end_line = end_line + @file_path = file_path + @method_name = method_name + @start_line = start_line + end + + def fingerprint_data + "#{file_path}:#{start_line}:#{end_line}" + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/locations/secret_detection.rb b/lib/gitlab/ci/reports/security/locations/secret_detection.rb new file mode 100644 index 00000000000..0fd5cc5af11 --- /dev/null +++ b/lib/gitlab/ci/reports/security/locations/secret_detection.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + module Locations + class SecretDetection < Base + include Security::Concerns::FingerprintPathFromFile + + attr_reader :class_name + attr_reader :end_line + attr_reader :file_path + attr_reader :method_name + attr_reader :start_line + + def initialize(file_path:, start_line:, end_line: nil, class_name: nil, method_name: nil) + @class_name = class_name + @end_line = end_line + @file_path = file_path + @method_name = method_name + @start_line = start_line + end + + def fingerprint_data + "#{file_path}:#{start_line}:#{end_line}" + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/report.rb b/lib/gitlab/ci/reports/security/report.rb new file mode 100644 index 00000000000..1ba2d909d99 --- /dev/null +++ b/lib/gitlab/ci/reports/security/report.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Report + attr_reader :created_at, :type, :pipeline, :findings, :scanners, :identifiers + attr_accessor :scan, :scanned_resources, :errors, :analyzer, :version + + delegate :project_id, to: :pipeline + + def initialize(type, pipeline, created_at) + @type = type + @pipeline = pipeline + @created_at = created_at + @findings = [] + @scanners = {} + @identifiers = {} + @scanned_resources = [] + @errors = [] + end + + def commit_sha + pipeline.sha + end + + def add_error(type, message = 'An unexpected error happened!') + errors << { type: type, message: message } + end + + def errored? + errors.present? + end + + def add_scanner(scanner) + scanners[scanner.key] ||= scanner + end + + def add_identifier(identifier) + identifiers[identifier.key] ||= identifier + end + + def add_finding(finding) + findings << finding + end + + def clone_as_blank + Report.new(type, pipeline, created_at) + end + + def replace_with!(other) + instance_variables.each do |ivar| + instance_variable_set(ivar, other.public_send(ivar.to_s[1..-1])) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def merge!(other) + replace_with!(::Security::MergeReportsService.new(self, other).execute) + end + + def primary_scanner + scanners.first&.second + end + + def primary_scanner_order_to(other) + return 1 unless primary_scanner + return -1 unless other.primary_scanner + + primary_scanner <=> other.primary_scanner + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/reports.rb b/lib/gitlab/ci/reports/security/reports.rb new file mode 100644 index 00000000000..a7a6e5b2593 --- /dev/null +++ b/lib/gitlab/ci/reports/security/reports.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Reports + attr_reader :reports, :pipeline + + delegate :each, :empty?, to: :reports + + def initialize(pipeline) + @reports = {} + @pipeline = pipeline + end + + def get_report(report_type, report_artifact) + reports[report_type] ||= Report.new(report_type, pipeline, report_artifact.created_at) + end + + def findings + reports.values.flat_map(&:findings) + end + + def violates_default_policy_against?(target_reports, vulnerabilities_allowed) + unsafe_findings_count(target_reports) > vulnerabilities_allowed + end + + private + + def findings_diff(target_reports) + findings - target_reports&.findings.to_a + end + + def unsafe_findings_count(target_reports) + findings_diff(target_reports).count(&:unsafe?) + end + end + end + end + end +end diff --git a/lib/gitlab/deprecation_json_logger.rb b/lib/gitlab/deprecation_json_logger.rb new file mode 100644 index 00000000000..9796b24868b --- /dev/null +++ b/lib/gitlab/deprecation_json_logger.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Gitlab + class DeprecationJsonLogger < Gitlab::JsonLogger + def self.file_name_noext + 'deprecation_json' + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 44b3b308b51..da2b57cce80 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -18032,6 +18032,18 @@ msgstr "" msgid "InviteMembersModal|Close invite team members" msgstr "" +msgid "InviteMembersModal|Collaborate on open issues and merge requests" +msgstr "" + +msgid "InviteMembersModal|Configure CI/CD" +msgstr "" + +msgid "InviteMembersModal|Configure security features" +msgstr "" + +msgid "InviteMembersModal|Contribute to the codebase" +msgstr "" + msgid "InviteMembersModal|GitLab member or email address" msgstr "" @@ -18047,6 +18059,9 @@ msgstr "" msgid "InviteMembersModal|Members were successfully added" msgstr "" +msgid "InviteMembersModal|Other" +msgstr "" + msgid "InviteMembersModal|Search for a group to invite" msgstr "" @@ -18062,6 +18077,9 @@ msgstr "" msgid "InviteMembersModal|Something went wrong" msgstr "" +msgid "InviteMembersModal|What would you like new member(s) to focus on? (optional)" +msgstr "" + msgid "InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} group." msgstr "" @@ -37537,6 +37555,9 @@ msgstr "" msgid "You are going to turn on confidentiality. Only team members with %{strongStart}at least Reporter access%{strongEnd} will be able to see and leave comments on the %{issuableType}." msgstr "" +msgid "You are not allowed to %{action} a user" +msgstr "" + msgid "You are not allowed to approve a user" msgstr "" @@ -38442,6 +38463,9 @@ msgstr "" msgid "authored" msgstr "" +msgid "banned user already exists" +msgstr "" + msgid "blocks" msgstr "" diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 6dc5c38cb76..be21fffb296 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -359,13 +359,12 @@ RSpec.describe Admin::UsersController do end end - describe 'PUT ban/:id' do + describe 'PUT ban/:id', :aggregate_failures do context 'when ban_user_feature_flag is enabled' do it 'bans user' do put :ban, params: { id: user.username } - user.reload - expect(user.banned?).to be_truthy + expect(user.reload.banned?).to be_truthy expect(flash[:notice]).to eq _('Successfully banned') end @@ -390,21 +389,19 @@ RSpec.describe Admin::UsersController do it 'does not ban user, renders 404' do put :ban, params: { id: user.username } - user.reload - expect(user.banned?).to be_falsey + expect(user.reload.banned?).to be_falsey expect(response).to have_gitlab_http_status(:not_found) end end end - describe 'PUT unban/:id' do + describe 'PUT unban/:id', :aggregate_failures do let(:banned_user) { create(:user, :banned) } it 'unbans user' do put :unban, params: { id: banned_user.username } - banned_user.reload - expect(banned_user.banned?).to be_falsey + expect(banned_user.reload.banned?).to be_falsey expect(flash[:notice]).to eq _('Successfully unbanned') end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index f91172d6499..8afb80d9cc5 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -778,6 +778,33 @@ RSpec.describe ProjectsController do end end end + + context 'with project feature attributes' do + using RSpec::Parameterized::TableSyntax + + where(:feature, :initial_value, :update_to) do + :metrics_dashboard_access_level | ProjectFeature::PRIVATE | ProjectFeature::ENABLED + :container_registry_access_level | ProjectFeature::ENABLED | ProjectFeature::PRIVATE + end + + with_them do + it "updates the project_feature new" do + params = { + namespace_id: project.namespace, + id: project.path, + project: { + project_feature_attributes: { + "#{feature}": update_to + } + } + } + + expect { put :update, params: params }.to change { + project.reload.project_feature.public_send(feature) + }.from(initial_value).to(update_to) + end + end + end end describe '#transfer', :enable_admin_mode do diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb index 642437b1119..2f4eb99a073 100644 --- a/spec/factories/ci/job_artifacts.rb +++ b/spec/factories/ci/job_artifacts.rb @@ -287,6 +287,76 @@ FactoryBot.define do end end + trait :common_security_report do + file_format { :raw } + file_type { :dependency_scanning } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/security_reports/master/gl-common-scanning-report.json'), 'application/json') + end + end + + trait :common_security_report_with_blank_names do + file_format { :raw } + file_type { :dependency_scanning } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/security_reports/master/gl-common-scanning-report-names.json'), 'application/json') + end + end + + trait :sast_deprecated do + file_type { :sast } + file_format { :raw } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/security_reports/deprecated/gl-sast-report.json'), 'application/json') + end + end + + trait :sast_with_corrupted_data do + file_type { :sast } + file_format { :raw } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/trace/sample_trace'), 'application/json') + end + end + + trait :sast_feature_branch do + file_format { :raw } + file_type { :sast } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/security_reports/feature-branch/gl-sast-report.json'), 'application/json') + end + end + + trait :secret_detection_feature_branch do + file_format { :raw } + file_type { :secret_detection } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json'), 'application/json') + end + end + + trait :sast_with_missing_scanner do + file_type { :sast } + file_format { :raw } + + after(:build) do |artifact, _| + artifact.file = fixture_file_upload( + Rails.root.join('spec/fixtures/security_reports/master/gl-sast-missing-scanner.json'), 'application/json') + end + end + trait :secret_detection do file_type { :secret_detection } file_format { :raw } diff --git a/spec/factories/ci/reports/security/aggregated_reports.rb b/spec/factories/ci/reports/security/aggregated_reports.rb new file mode 100644 index 00000000000..eb678dc9766 --- /dev/null +++ b/spec/factories/ci/reports/security/aggregated_reports.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_aggregated_reports, class: '::Gitlab::Ci::Reports::Security::AggregatedReport' do + reports { FactoryBot.build_list(:ci_reports_security_report, 1) } + findings { FactoryBot.build_list(:ci_reports_security_finding, 1) } + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::AggregatedReport.new(reports, findings) + end + end +end diff --git a/spec/factories/ci/reports/security/finding_keys.rb b/spec/factories/ci/reports/security/finding_keys.rb new file mode 100644 index 00000000000..f00a043012e --- /dev/null +++ b/spec/factories/ci/reports/security/finding_keys.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_finding_key, class: '::Gitlab::Ci::Reports::Security::FindingKey' do + sequence :location_fingerprint do |a| + Digest::SHA1.hexdigest(a.to_s) + end + sequence :identifier_fingerprint do |a| + Digest::SHA1.hexdigest(a.to_s) + end + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::FindingKey.new(**attributes) + end + end +end diff --git a/spec/factories/ci/reports/security/findings.rb b/spec/factories/ci/reports/security/findings.rb new file mode 100644 index 00000000000..e3971bc48f3 --- /dev/null +++ b/spec/factories/ci/reports/security/findings.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_finding, class: '::Gitlab::Ci::Reports::Security::Finding' do + compare_key { "#{identifiers.first&.external_type}:#{identifiers.first&.external_id}:#{location.fingerprint}" } + confidence { :medium } + identifiers { Array.new(1) { association(:ci_reports_security_identifier) } } + location factory: :ci_reports_security_locations_sast + metadata_version { 'sast:1.0' } + name { 'Cipher with no integrity' } + report_type { :sast } + raw_metadata do + { + description: "The cipher does not provide data integrity update 1", + solution: "GCM mode introduces an HMAC into the resulting encrypted data, providing integrity of the result.", + location: { + file: "maven/src/main/java/com/gitlab/security_products/tests/App.java", + start_line: 29, + end_line: 29, + class: "com.gitlab.security_products.tests.App", + method: "insecureCypher" + }, + links: [ + { + name: "Cipher does not check for integrity first?", + url: "https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first" + } + ] + }.to_json + end + scanner factory: :ci_reports_security_scanner + severity { :high } + scan factory: :ci_reports_security_scan + sequence(:uuid) do |n| + ::Security::VulnerabilityUUID.generate( + report_type: report_type, + primary_identifier_fingerprint: identifiers.first&.fingerprint, + location_fingerprint: location.fingerprint, + project_id: n + ) + end + vulnerability_finding_signatures_enabled { false } + + skip_create + + trait :dynamic do + location { association(:ci_reports_security_locations_sast, :dynamic) } + end + + initialize_with do + ::Gitlab::Ci::Reports::Security::Finding.new(**attributes) + end + end +end diff --git a/spec/factories/ci/reports/security/locations/sast.rb b/spec/factories/ci/reports/security/locations/sast.rb new file mode 100644 index 00000000000..59b54ecd8f2 --- /dev/null +++ b/spec/factories/ci/reports/security/locations/sast.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_locations_sast, class: '::Gitlab::Ci::Reports::Security::Locations::Sast' do + file_path { 'maven/src/main/java/com/gitlab/security_products/tests/App.java' } + start_line { 29 } + end_line { 31 } + class_name { 'com.gitlab.security_products.tests.App' } + method_name { 'insecureCypher' } + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::Locations::Sast.new(**attributes) + end + + trait :dynamic do + sequence(:file_path, 'a') { |n| "path/#{n}" } + start_line { Random.rand(20) } + end_line { start_line + Random.rand(5) } + end + end +end diff --git a/spec/factories/ci/reports/security/reports.rb b/spec/factories/ci/reports/security/reports.rb new file mode 100644 index 00000000000..5699b8fee3e --- /dev/null +++ b/spec/factories/ci/reports/security/reports.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_report, class: '::Gitlab::Ci::Reports::Security::Report' do + type { :sast } + pipeline { association(:ci_pipeline) } + created_at { 2.weeks.ago } + scanned_resources { [] } + + transient do + findings { [] } + scanners { [] } + identifiers { [] } + end + + after :build do |report, evaluator| + evaluator.scanners.each { |s| report.add_scanner(s) } + evaluator.identifiers.each { |id| report.add_identifier(id) } + evaluator.findings.each { |o| report.add_finding(o) } + end + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::Report.new(type, pipeline, created_at) + end + end +end diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index 1d57d0a9103..38e829bafcc 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -84,6 +84,33 @@ RSpec.describe 'Groups > Members > Manage members' do property: 'existing_user', user: user1 ) + expect_no_snowplow_event( + category: 'Members::CreateService', + action: 'area_of_focus' + ) + end + + it 'adds a user to group with area_of_focus', :js, :snowplow, :aggregate_failures do + stub_experiments(member_areas_of_focus: :candidate) + group.add_owner(user1) + + visit group_group_members_path(group) + + invite_member(user2.name, role: 'Reporter', area_of_focus: true) + wait_for_requests + + expect_snowplow_event( + category: 'Members::CreateService', + action: 'area_of_focus', + label: 'Contribute to the codebase', + property: group.members.last.id.to_s + ) + expect_snowplow_event( + category: 'Members::CreateService', + action: 'area_of_focus', + label: 'Collaborate on open issues and merge requests', + property: group.members.last.id.to_s + ) end it 'do not disclose email addresses', :js do @@ -193,9 +220,36 @@ RSpec.describe 'Groups > Members > Manage members' do property: 'net_new_user', user: user1 ) + expect_no_snowplow_event( + category: 'Members::CreateService', + action: 'area_of_focus' + ) end end + it 'invite user to group with area_of_focus', :js, :snowplow, :aggregate_failures do + stub_experiments(member_areas_of_focus: :candidate) + group.add_owner(user1) + + visit group_group_members_path(group) + + invite_member('test@example.com', role: 'Reporter', area_of_focus: true) + wait_for_requests + + expect_snowplow_event( + category: 'Members::InviteService', + action: 'area_of_focus', + label: 'Contribute to the codebase', + property: group.members.last.id.to_s + ) + expect_snowplow_event( + category: 'Members::InviteService', + action: 'area_of_focus', + label: 'Collaborate on open issues and merge requests', + property: group.members.last.id.to_s + ) + end + context 'when user is a guest' do before do group.add_guest(user1) diff --git a/spec/fixtures/security_reports/deprecated/gl-sast-report.json b/spec/fixtures/security_reports/deprecated/gl-sast-report.json new file mode 100644 index 00000000000..2f7e47281e2 --- /dev/null +++ b/spec/fixtures/security_reports/deprecated/gl-sast-report.json @@ -0,0 +1,964 @@ +[ + { + "category": "sast", + "message": "Probable insecure usage of temp file/directory.", + "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108", + "severity": "Medium", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-tmp.py", + "start_line": 1, + "end_line": 1 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B108", + "value": "B108", + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + } + ], + "priority": "Medium", + "file": "python/hardcoded/hardcoded-tmp.py", + "line": 1, + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", + "tool": "bandit" + }, + { + "category": "sast", + "name": "Predictable pseudorandom number generator", + "message": "Predictable pseudorandom number generator", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", + "severity": "Medium", + "confidence": "Medium", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 47, + "end_line": 47, + "class": "com.gitlab.security_products.tests.App", + "method": "generateSecretToken2" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-PREDICTABLE_RANDOM", + "value": "PREDICTABLE_RANDOM", + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 47, + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", + "tool": "find_sec_bugs" + }, + { + "category": "sast", + "name": "Predictable pseudorandom number generator", + "message": "Predictable pseudorandom number generator", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", + "severity": "Medium", + "confidence": "Medium", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 41, + "end_line": 41, + "class": "com.gitlab.security_products.tests.App", + "method": "generateSecretToken1" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-PREDICTABLE_RANDOM", + "value": "PREDICTABLE_RANDOM", + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 41, + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM", + "tool": "find_sec_bugs" + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 11, + "end_line": 11 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 11, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 12, + "end_line": 12 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 12, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 13, + "end_line": 13 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 13, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 14, + "end_line": 14 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 14, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Pickle library appears to be in use, possible security issue.", + "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 15, + "end_line": 15 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B301", + "value": "B301" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 15, + "tool": "bandit" + }, + { + "category": "sast", + "name": "ECB mode is insecure", + "message": "ECB mode is insecure", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 29, + "end_line": 29, + "class": "com.gitlab.security_products.tests.App", + "method": "insecureCypher" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-ECB_MODE", + "value": "ECB_MODE", + "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 29, + "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE", + "tool": "find_sec_bugs" + }, + { + "category": "sast", + "name": "Cipher with no integrity", + "message": "Cipher with no integrity", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 29, + "end_line": 29, + "class": "com.gitlab.security_products.tests.App", + "method": "insecureCypher" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-CIPHER_INTEGRITY", + "value": "CIPHER_INTEGRITY", + "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 29, + "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY", + "tool": "find_sec_bugs" + }, + { + "category": "sast", + "message": "Probable insecure usage of temp file/directory.", + "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108", + "severity": "Medium", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-tmp.py", + "start_line": 14, + "end_line": 14 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B108", + "value": "B108", + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + } + ], + "priority": "Medium", + "file": "python/hardcoded/hardcoded-tmp.py", + "line": 14, + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Probable insecure usage of temp file/directory.", + "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108", + "severity": "Medium", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-tmp.py", + "start_line": 10, + "end_line": 10 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B108", + "value": "B108", + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + } + ], + "priority": "Medium", + "file": "python/hardcoded/hardcoded-tmp.py", + "line": 10, + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with Popen module.", + "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 1, + "end_line": 1 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 1, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with pickle module.", + "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports.py", + "start_line": 2, + "end_line": 2 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports.py", + "line": 2, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with subprocess module.", + "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports.py", + "start_line": 4, + "end_line": 4 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports.py", + "line": 4, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'blerg'", + "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106", + "severity": "Low", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 22, + "end_line": 22 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B106", + "value": "B106", + "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 22, + "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'root'", + "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105", + "severity": "Low", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 5, + "end_line": 5 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 5, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Possible hardcoded password: ''", + "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105", + "severity": "Low", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 9, + "end_line": 9 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 9, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'", + "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105", + "severity": "Low", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 13, + "end_line": 13 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 13, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'blerg'", + "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105", + "severity": "Low", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 23, + "end_line": 23 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 23, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'blerg'", + "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105", + "severity": "Low", + "confidence": "Medium", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 24, + "end_line": 24 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 24, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with subprocess module.", + "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-function.py", + "start_line": 4, + "end_line": 4 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-function.py", + "line": 4, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with pickle module.", + "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-function.py", + "start_line": 2, + "end_line": 2 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports-function.py", + "line": 2, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with Popen module.", + "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-from.py", + "start_line": 7, + "end_line": 7 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-from.py", + "line": 7, + "tool": "bandit" + }, + { + "category": "sast", + "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell", + "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 9, + "end_line": 9 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B602", + "value": "B602", + "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 9, + "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html", + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with subprocess module.", + "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-from.py", + "start_line": 6, + "end_line": 6 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-from.py", + "line": 6, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with Popen module.", + "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-from.py", + "start_line": 1, + "end_line": 2 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-from.py", + "line": 1, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with pickle module.", + "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 7, + "end_line": 8 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 7, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with loads module.", + "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403", + "severity": "Low", + "confidence": "High", + "scanner": { + "id": "bandit", + "name": "Bandit" + }, + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 6, + "end_line": 6 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 6, + "tool": "bandit" + }, + { + "category": "sast", + "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", + "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120", + "confidence": "Low", + "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", + "scanner": { + "id": "flawfinder", + "name": "Flawfinder" + }, + "location": { + "file": "c/subdir/utils.c", + "start_line": 4 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - char", + "value": "char" + }, + { + "type": "cwe", + "name": "CWE-119", + "value": "119", + "url": "https://cwe.mitre.org/data/definitions/119.html" + }, + { + "type": "cwe", + "name": "CWE-120", + "value": "120", + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ], + "file": "c/subdir/utils.c", + "line": 4, + "url": "https://cwe.mitre.org/data/definitions/119.html", + "tool": "flawfinder" + }, + { + "category": "sast", + "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)", + "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362", + "confidence": "Low", + "scanner": { + "id": "flawfinder", + "name": "Flawfinder" + }, + "location": { + "file": "c/subdir/utils.c", + "start_line": 8 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - fopen", + "value": "fopen" + }, + { + "type": "cwe", + "name": "CWE-362", + "value": "362", + "url": "https://cwe.mitre.org/data/definitions/362.html" + } + ], + "file": "c/subdir/utils.c", + "line": 8, + "url": "https://cwe.mitre.org/data/definitions/362.html", + "tool": "flawfinder" + }, + { + "category": "sast", + "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", + "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120", + "confidence": "Low", + "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", + "scanner": { + "id": "flawfinder", + "name": "Flawfinder" + }, + "location": { + "file": "cplusplus/src/hello.cpp", + "start_line": 6 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - char", + "value": "char" + }, + { + "type": "cwe", + "name": "CWE-119", + "value": "119", + "url": "https://cwe.mitre.org/data/definitions/119.html" + }, + { + "type": "cwe", + "name": "CWE-120", + "value": "120", + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ], + "file": "cplusplus/src/hello.cpp", + "line": 6, + "url": "https://cwe.mitre.org/data/definitions/119.html", + "tool": "flawfinder" + }, + { + "category": "sast", + "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)", + "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120", + "confidence": "Low", + "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)", + "scanner": { + "id": "flawfinder", + "name": "Flawfinder" + }, + "location": { + "file": "cplusplus/src/hello.cpp", + "start_line": 7 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - strcpy", + "value": "strcpy" + }, + { + "type": "cwe", + "name": "CWE-120", + "value": "120", + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ], + "file": "cplusplus/src/hello.cpp", + "line": 7, + "url": "https://cwe.mitre.org/data/definitions/120.html", + "tool": "flawfinder" + } +] diff --git a/spec/fixtures/security_reports/feature-branch/gl-sast-report.json b/spec/fixtures/security_reports/feature-branch/gl-sast-report.json new file mode 100644 index 00000000000..f93233e0ebb --- /dev/null +++ b/spec/fixtures/security_reports/feature-branch/gl-sast-report.json @@ -0,0 +1,177 @@ +{ + "version": "14.0.0", + "vulnerabilities": [ + { + "category": "sast", + "name": "Predictable pseudorandom number generator", + "message": "Predictable pseudorandom number generator", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", + "severity": "Medium", + "confidence": "Medium", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 47, + "end_line": 47, + "class": "com.gitlab.security_products.tests.App", + "method": "generateSecretToken2" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-PREDICTABLE_RANDOM", + "value": "PREDICTABLE_RANDOM", + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + } + ] + }, + { + "category": "sast", + "name": "Predictable pseudorandom number generator", + "message": "Predictable pseudorandom number generator", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", + "severity": "Low", + "confidence": "Low", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 41, + "end_line": 41, + "class": "com.gitlab.security_products.tests.App", + "method": "generateSecretToken1" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-PREDICTABLE_RANDOM", + "value": "PREDICTABLE_RANDOM", + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + } + ] + }, + { + "category": "sast", + "name": "ECB mode is insecure", + "message": "ECB mode is insecure", + "description": "The cipher uses ECB mode, which provides poor confidentiality for encrypted data", + "cve": "ea0f905fc76f2739d5f10a1fd1e37a10:ECB_MODE:java-maven/src/main/java/com/gitlab/security_products/tests/App.java:29", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "java-maven/src/main/java/com/gitlab/security_products/tests/App.java", + "start_line": 29, + "end_line": 29, + "class": "com.gitlab.security_products.tests.App", + "method": "insecureCypher" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-ECB_MODE", + "value": "ECB_MODE", + "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" + }, + { + "type": "cwe", + "name": "CWE-327", + "value": "327", + "url": "https://cwe.mitre.org/data/definitions/327.html" + } + ] + }, + { + "category": "sast", + "name": "Hard coded key", + "message": "Hard coded key", + "description": "Hard coded cryptographic key found", + "cve": "102ac67e0975ecec02a056008e0faad8:HARD_CODE_KEY:scala-sbt/src/main/scala/example/Main.scala:12", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "scala-sbt/src/main/scala/example/Main.scala", + "start_line": 12, + "end_line": 12, + "class": "example.Main$", + "method": "getBytes" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-HARD_CODE_KEY", + "value": "HARD_CODE_KEY", + "url": "https://find-sec-bugs.github.io/bugs.htm#HARD_CODE_KEY" + }, + { + "type": "cwe", + "name": "CWE-321", + "value": "321", + "url": "https://cwe.mitre.org/data/definitions/321.html" + } + ] + }, + { + "category": "sast", + "name": "ECB mode is insecure", + "message": "ECB mode is insecure", + "description": "The cipher uses ECB mode, which provides poor confidentiality for encrypted data", + "cve": "ea0f905fc76f2739d5f10a1fd1e37a10:ECB_MODE:app/src/main/groovy/com/gitlab/security_products/tests/App.groovy:29", + "severity": "Medium", + "confidence": "High", + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs" + }, + "location": { + "file": "app/src/main/groovy/com/gitlab/security_products/tests/App.groovy", + "start_line": 29, + "end_line": 29, + "class": "com.gitlab.security_products.tests.App", + "method": "insecureCypher" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-ECB_MODE", + "value": "ECB_MODE", + "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" + }, + { + "type": "cwe", + "name": "CWE-327", + "value": "327", + "url": "https://cwe.mitre.org/data/definitions/327.html" + } + ] + } + ], + "remediations": [], + "scan": { + "scanner": { + "id": "find_sec_bugs", + "name": "Find Security Bugs", + "url": "https://spotbugs.github.io", + "vendor": { + "name": "GitLab" + }, + "version": "4.0.2" + }, + "type": "sast", + "status": "success", + "start_time": "placeholder-value", + "end_time": "placeholder-value" + } +} diff --git a/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json b/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json new file mode 100644 index 00000000000..57a4dee3ddd --- /dev/null +++ b/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json @@ -0,0 +1,5 @@ +{ + "version": "3.0", + "vulnerabilities": [], + "remediations": [] +} diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json b/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json new file mode 100644 index 00000000000..3cfb3e51ef7 --- /dev/null +++ b/spec/fixtures/security_reports/master/gl-common-scanning-report-names.json @@ -0,0 +1,168 @@ +{ + "vulnerabilities": [ + { + "category": "dependency_scanning", + "name": "Vulnerabilities in libxml2", + "message": "Vulnerabilities in libxml2 in nokogiri", + "description": "", + "cve": "CVE-1020", + "severity": "High", + "solution": "Upgrade to latest version.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": {}, + "identifiers": [], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020" + } + ] + }, + { + "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3", + "category": "dependency_scanning", + "name": "Regular Expression Denial of Service", + "message": "", + "description": "", + "cve": "CVE-1030", + "severity": "Unknown", + "solution": "Upgrade to latest versions.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": {}, + "identifiers": [], + "links": [ + { + "name": "CVE-1030", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030" + } + ] + }, + { + "category": "dependency_scanning", + "name": "", + "message": "", + "description": "", + "cve": "CVE-2017-11429", + "severity": "Unknown", + "solution": "Upgrade to fixed version.\r\n", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "yarn/yarn.lock", + "dependency": { + "package": { + "name": "io.netty/netty" + }, + "version": "3.9.1.Final" + } + }, + "identifiers": [ + { + "value": "2017-11429", + "type": "cwe", + "name": "CWE-2017-11429", + "url": "https://cve.mitre.org/cgi-bin/cwename.cgi?name=CWE-2017-11429" + }, + { + "value": "2017-11429", + "type": "cve", + "name": "CVE-2017-11429", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429" + } + ], + "links": [] + }, + { + "category": "dependency_scanning", + "name": "", + "message": "", + "description": "", + "cve": "CWE-2017-11429", + "severity": "Unknown", + "solution": "Upgrade to fixed version.\r\n", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "yarn/yarn.lock", + "dependency": { + "package": { + "name": "io.netty/netty" + }, + "version": "3.9.1.Final" + } + }, + "identifiers": [ + { + "value": "2017-11429", + "type": "cwe", + "name": "CwE-2017-11429", + "url": "https://cwe.mitre.org/cgi-bin/cwename.cgi?name=CWE-2017-11429" + }, + { + "value": "2017-11429", + "type": "other", + "name": "other-2017-11429", + "url": "https://other.mitre.org/cgi-bin/othername.cgi?name=other-2017-11429" + } + ], + "links": [] + }, + { + "category": "dependency_scanning", + "name": "", + "message": "", + "description": "", + "cve": "OTHER-2017-11429", + "severity": "Unknown", + "solution": "Upgrade to fixed version.\r\n", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": { + "file": "yarn/yarn.lock", + "dependency": { + "package": { + "name": "io.netty/netty" + }, + "version": "3.9.1.Final" + } + }, + "identifiers": [ + { + "value": "2017-11429", + "type": "other", + "name": "other-2017-11429", + "url": "https://other.mitre.org/cgi-bin/othername.cgi?name=other-2017-11429" + } + ], + "links": [] + } + ], + "remediations": [], + "dependency_files": [], + "scan": { + "scanner": { + "id": "gemnasium", + "name": "Gemnasium", + "url": "https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven", + "vendor": { + "name": "GitLab" + }, + "version": "2.18.0" + }, + "type": "dependency_scanning", + "start_time": "placeholder-value", + "end_time": "placeholder-value", + "status": "success" + } +} diff --git a/spec/fixtures/security_reports/master/gl-common-scanning-report.json b/spec/fixtures/security_reports/master/gl-common-scanning-report.json new file mode 100644 index 00000000000..cf4c5239b57 --- /dev/null +++ b/spec/fixtures/security_reports/master/gl-common-scanning-report.json @@ -0,0 +1,160 @@ +{ + "vulnerabilities": [ + { + "category": "dependency_scanning", + "name": "Vulnerabilities in libxml2", + "message": "Vulnerabilities in libxml2 in nokogiri", + "description": "", + "cve": "CVE-1020", + "severity": "High", + "solution": "Upgrade to latest version.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": {}, + "identifiers": [ + { + "type": "GitLab", + "name": "Foo vulnerability", + "value": "foo" + } + ], + "links": [ + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020" + } + ], + "details": { + "commit": { + "name": [ + { + "lang": "en", + "value": "The Commit" + } + ], + "description": [ + { + "lang": "en", + "value": "Commit where the vulnerability was identified" + } + ], + "type": "commit", + "value": "41df7b7eb3be2b5be2c406c2f6d28cd6631eeb19" + } + } + }, + { + "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3", + "category": "dependency_scanning", + "name": "Regular Expression Denial of Service", + "message": "Regular Expression Denial of Service in debug", + "description": "", + "cve": "CVE-1030", + "severity": "Unknown", + "solution": "Upgrade to latest versions.", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": {}, + "identifiers": [ + { + "type": "GitLab", + "name": "Bar vulnerability", + "value": "bar" + } + ], + "links": [ + { + "name": "CVE-1030", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030" + } + ] + }, + { + "category": "dependency_scanning", + "name": "Authentication bypass via incorrect DOM traversal and canonicalization", + "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js", + "description": "", + "cve": "yarn/yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98", + "severity": "Unknown", + "solution": "Upgrade to fixed version.\r\n", + "scanner": { + "id": "gemnasium", + "name": "Gemnasium" + }, + "location": {}, + "identifiers": [], + "links": [ + ] + } + ], + "remediations": [ + { + "fixes": [ + { + "cve": "CVE-1020" + } + ], + "summary": "", + "diff": "" + }, + { + "fixes": [ + { + "cve": "CVE", + "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3" + } + ], + "summary": "", + "diff": "" + }, + { + "fixes": [ + { + "cve": "CVE", + "id": "bb2fbeb1b71ea360ce3f86f001d4e84823c3ffe1a1f7d41ba7466b14cfa953d3" + } + ], + "summary": "", + "diff": "" + }, + { + "fixes": [ + { + "id": "2134", + "cve": "CVE-1" + } + ], + "summary": "", + "diff": "" + } + ], + "dependency_files": [], + "scan": { + "analyzer": { + "id": "common-analyzer", + "name": "Common Analyzer", + "url": "https://site.com/analyzer/common", + "version": "2.0.1", + "vendor": { + "name": "Common" + } + }, + "scanner": { + "id": "gemnasium", + "name": "Gemnasium", + "url": "https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium-maven", + "vendor": { + "name": "GitLab" + }, + "version": "2.18.0" + }, + "type": "dependency_scanning", + "start_time": "placeholder-value", + "end_time": "placeholder-value", + "status": "success" + }, + "version": "14.0.2" +} diff --git a/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json b/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json new file mode 100644 index 00000000000..f65580145b4 --- /dev/null +++ b/spec/fixtures/security_reports/master/gl-sast-missing-scanner.json @@ -0,0 +1,802 @@ +{ + "version": "1.2", + "vulnerabilities": [ + { + "category": "sast", + "message": "Probable insecure usage of temp file/directory.", + "cve": "python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108", + "severity": "Medium", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-tmp.py", + "start_line": 1, + "end_line": 1 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B108", + "value": "B108", + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + } + ], + "priority": "Medium", + "file": "python/hardcoded/hardcoded-tmp.py", + "line": 1, + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + }, + { + "category": "sast", + "name": "Predictable pseudorandom number generator", + "message": "Predictable pseudorandom number generator", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM", + "severity": "Medium", + "confidence": "Medium", + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 47, + "end_line": 47, + "class": "com.gitlab.security_products.tests.App", + "method": "generateSecretToken2" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-PREDICTABLE_RANDOM", + "value": "PREDICTABLE_RANDOM", + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 47, + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + }, + { + "category": "sast", + "name": "Predictable pseudorandom number generator", + "message": "Predictable pseudorandom number generator", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM", + "severity": "Medium", + "confidence": "Medium", + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 41, + "end_line": 41, + "class": "com.gitlab.security_products.tests.App", + "method": "generateSecretToken1" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-PREDICTABLE_RANDOM", + "value": "PREDICTABLE_RANDOM", + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 41, + "url": "https://find-sec-bugs.github.io/bugs.htm#PREDICTABLE_RANDOM" + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:cb203b465dffb0cb3a8e8bd8910b84b93b0a5995a938e4b903dbb0cd6ffa1254:B303", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 11, + "end_line": 11 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 11 + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:a7173c43ae66bd07466632d819d450e0071e02dbf782763640d1092981f9631b:B303", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 12, + "end_line": 12 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 12 + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:017017b77deb0b8369b6065947833eeea752a92ec8a700db590fece3e934cf0d:B303", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 13, + "end_line": 13 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 13 + }, + { + "category": "sast", + "message": "Use of insecure MD2, MD4, or MD5 hash function.", + "cve": "python/imports/imports-aliases.py:45fc8c53aea7b84f06bc4e590cc667678d6073c4c8a1d471177ca2146fb22db2:B303", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 14, + "end_line": 14 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B303", + "value": "B303" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 14 + }, + { + "category": "sast", + "message": "Pickle library appears to be in use, possible security issue.", + "cve": "python/imports/imports-aliases.py:5f200d47291e7bbd8352db23019b85453ca048dd98ea0c291260fa7d009963a4:B301", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 15, + "end_line": 15 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B301", + "value": "B301" + } + ], + "priority": "Medium", + "file": "python/imports/imports-aliases.py", + "line": 15 + }, + { + "category": "sast", + "name": "ECB mode is insecure", + "message": "ECB mode is insecure", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:ECB_MODE", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 29, + "end_line": 29, + "class": "com.gitlab.security_products.tests.App", + "method": "insecureCypher" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-ECB_MODE", + "value": "ECB_MODE", + "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 29, + "url": "https://find-sec-bugs.github.io/bugs.htm#ECB_MODE" + }, + { + "category": "sast", + "name": "Cipher with no integrity", + "message": "Cipher with no integrity", + "cve": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:29:CIPHER_INTEGRITY", + "severity": "Medium", + "confidence": "High", + "location": { + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "start_line": 29, + "end_line": 29, + "class": "com.gitlab.security_products.tests.App", + "method": "insecureCypher" + }, + "identifiers": [ + { + "type": "find_sec_bugs_type", + "name": "Find Security Bugs-CIPHER_INTEGRITY", + "value": "CIPHER_INTEGRITY", + "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" + } + ], + "priority": "Medium", + "file": "groovy/src/main/java/com/gitlab/security_products/tests/App.groovy", + "line": 29, + "url": "https://find-sec-bugs.github.io/bugs.htm#CIPHER_INTEGRITY" + }, + { + "category": "sast", + "message": "Probable insecure usage of temp file/directory.", + "cve": "python/hardcoded/hardcoded-tmp.py:63dd4d626855555b816985d82c4614a790462a0a3ada89dc58eb97f9c50f3077:B108", + "severity": "Medium", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-tmp.py", + "start_line": 14, + "end_line": 14 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B108", + "value": "B108", + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + } + ], + "priority": "Medium", + "file": "python/hardcoded/hardcoded-tmp.py", + "line": 14, + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + }, + { + "category": "sast", + "message": "Probable insecure usage of temp file/directory.", + "cve": "python/hardcoded/hardcoded-tmp.py:4ad6d4c40a8c263fc265f3384724014e0a4f8dd6200af83e51ff120420038031:B108", + "severity": "Medium", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-tmp.py", + "start_line": 10, + "end_line": 10 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B108", + "value": "B108", + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + } + ], + "priority": "Medium", + "file": "python/hardcoded/hardcoded-tmp.py", + "line": 10, + "url": "https://docs.openstack.org/bandit/latest/plugins/b108_hardcoded_tmp_directory.html" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with Popen module.", + "cve": "python/imports/imports-aliases.py:2c3e1fa1e54c3c6646e8bcfaee2518153c6799b77587ff8d9a7b0631f6d34785:B404", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 1, + "end_line": 1 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 1 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with pickle module.", + "cve": "python/imports/imports.py:af58d07f6ad519ef5287fcae65bf1a6999448a1a3a8bc1ac2a11daa80d0b96bf:B403", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports.py", + "start_line": 2, + "end_line": 2 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports.py", + "line": 2 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with subprocess module.", + "cve": "python/imports/imports.py:8de9bc98029d212db530785a5f6780cfa663548746ff228ab8fa96c5bb82f089:B404", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports.py", + "start_line": 4, + "end_line": 4 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports.py", + "line": 4 + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'blerg'", + "cve": "python/hardcoded/hardcoded-passwords.py:97c30f1d76d2a88913e3ce9ae74087874d740f87de8af697a9c455f01119f633:B106", + "severity": "Low", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 22, + "end_line": 22 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B106", + "value": "B106", + "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 22, + "url": "https://docs.openstack.org/bandit/latest/plugins/b106_hardcoded_password_funcarg.html" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'root'", + "cve": "python/hardcoded/hardcoded-passwords.py:7431c73a0bc16d94ece2a2e75ef38f302574d42c37ac0c3c38ad0b3bf8a59f10:B105", + "severity": "Low", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 5, + "end_line": 5 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 5, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + }, + { + "category": "sast", + "message": "Possible hardcoded password: ''", + "cve": "python/hardcoded/hardcoded-passwords.py:d2d1857c27caedd49c57bfbcdc23afcc92bd66a22701fcdc632869aab4ca73ee:B105", + "severity": "Low", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 9, + "end_line": 9 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 9, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'ajklawejrkl42348swfgkg'", + "cve": "python/hardcoded/hardcoded-passwords.py:fb3866215a61393a5c9c32a3b60e2058171a23219c353f722cbd3567acab21d2:B105", + "severity": "Low", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 13, + "end_line": 13 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 13, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'blerg'", + "cve": "python/hardcoded/hardcoded-passwords.py:63c62a8b7e1e5224439bd26b28030585ac48741e28ca64561a6071080c560a5f:B105", + "severity": "Low", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 23, + "end_line": 23 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 23, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + }, + { + "category": "sast", + "message": "Possible hardcoded password: 'blerg'", + "cve": "python/hardcoded/hardcoded-passwords.py:4311b06d08df8fa58229b341c531da8e1a31ec4520597bdff920cd5c098d86f9:B105", + "severity": "Low", + "confidence": "Medium", + "location": { + "file": "python/hardcoded/hardcoded-passwords.py", + "start_line": 24, + "end_line": 24 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B105", + "value": "B105", + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + } + ], + "priority": "Low", + "file": "python/hardcoded/hardcoded-passwords.py", + "line": 24, + "url": "https://docs.openstack.org/bandit/latest/plugins/b105_hardcoded_password_string.html" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with subprocess module.", + "cve": "python/imports/imports-function.py:5858400c2f39047787702de44d03361ef8d954c9d14bd54ee1c2bef9e6a7df93:B404", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-function.py", + "start_line": 4, + "end_line": 4 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-function.py", + "line": 4 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with pickle module.", + "cve": "python/imports/imports-function.py:dbda3cf4190279d30e0aad7dd137eca11272b0b225e8af4e8bf39682da67d956:B403", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-function.py", + "start_line": 2, + "end_line": 2 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports-function.py", + "line": 2 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with Popen module.", + "cve": "python/imports/imports-from.py:eb8a0db9cd1a8c1ab39a77e6025021b1261cc2a0b026b2f4a11fca4e0636d8dd:B404", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-from.py", + "start_line": 7, + "end_line": 7 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-from.py", + "line": 7 + }, + { + "category": "sast", + "message": "subprocess call with shell=True seems safe, but may be changed in the future, consider rewriting without shell", + "cve": "python/imports/imports-aliases.py:f99f9721e27537fbcb6699a4cf39c6740d6234d2c6f06cfc2d9ea977313c483d:B602", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 9, + "end_line": 9 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B602", + "value": "B602", + "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 9, + "url": "https://docs.openstack.org/bandit/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html" + }, + { + "category": "sast", + "message": "Consider possible security implications associated with subprocess module.", + "cve": "python/imports/imports-from.py:332a12ab1146698f614a905ce6a6a5401497a12281aef200e80522711c69dcf4:B404", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-from.py", + "start_line": 6, + "end_line": 6 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-from.py", + "line": 6 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with Popen module.", + "cve": "python/imports/imports-from.py:0a48de4a3d5348853a03666cb574697e3982998355e7a095a798bd02a5947276:B404", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-from.py", + "start_line": 1, + "end_line": 2 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B404", + "value": "B404" + } + ], + "priority": "Low", + "file": "python/imports/imports-from.py", + "line": 1 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with pickle module.", + "cve": "python/imports/imports-aliases.py:51b71661dff994bde3529639a727a678c8f5c4c96f00d300913f6d5be1bbdf26:B403", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 7, + "end_line": 8 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 7 + }, + { + "category": "sast", + "message": "Consider possible security implications associated with loads module.", + "cve": "python/imports/imports-aliases.py:6ff02aeb3149c01ab68484d794a94f58d5d3e3bb0d58557ef4153644ea68ea54:B403", + "severity": "Low", + "confidence": "High", + "location": { + "file": "python/imports/imports-aliases.py", + "start_line": 6, + "end_line": 6 + }, + "identifiers": [ + { + "type": "bandit_test_id", + "name": "Bandit Test ID B403", + "value": "B403" + } + ], + "priority": "Low", + "file": "python/imports/imports-aliases.py", + "line": 6 + }, + { + "category": "sast", + "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", + "cve": "c/subdir/utils.c:b466873101951fe96e1332f6728eb7010acbbd5dfc3b65d7d53571d091a06d9e:CWE-119!/CWE-120", + "confidence": "Low", + "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", + "location": { + "file": "c/subdir/utils.c", + "start_line": 4 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - char", + "value": "char" + }, + { + "type": "cwe", + "name": "CWE-119", + "value": "119", + "url": "https://cwe.mitre.org/data/definitions/119.html" + }, + { + "type": "cwe", + "name": "CWE-120", + "value": "120", + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ], + "file": "c/subdir/utils.c", + "line": 4, + "url": "https://cwe.mitre.org/data/definitions/119.html" + }, + { + "category": "sast", + "message": "Check when opening files - can an attacker redirect it (via symlinks), force the opening of special file type (e.g., device files), move things around to create a race condition, control its ancestors, or change its contents? (CWE-362)", + "cve": "c/subdir/utils.c:bab681140fcc8fc3085b6bba74081b44ea145c1c98b5e70cf19ace2417d30770:CWE-362", + "confidence": "Low", + "location": { + "file": "c/subdir/utils.c", + "start_line": 8 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - fopen", + "value": "fopen" + }, + { + "type": "cwe", + "name": "CWE-362", + "value": "362", + "url": "https://cwe.mitre.org/data/definitions/362.html" + } + ], + "file": "c/subdir/utils.c", + "line": 8, + "url": "https://cwe.mitre.org/data/definitions/362.html" + }, + { + "category": "sast", + "message": "Statically-sized arrays can be improperly restricted, leading to potential overflows or other issues (CWE-119!/CWE-120)", + "cve": "cplusplus/src/hello.cpp:c8c6dd0afdae6814194cf0930b719f757ab7b379cf8f261e7f4f9f2f323a818a:CWE-119!/CWE-120", + "confidence": "Low", + "solution": "Perform bounds checking, use functions that limit length, or ensure that the size is larger than the maximum possible length", + "location": { + "file": "cplusplus/src/hello.cpp", + "start_line": 6 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - char", + "value": "char" + }, + { + "type": "cwe", + "name": "CWE-119", + "value": "119", + "url": "https://cwe.mitre.org/data/definitions/119.html" + }, + { + "type": "cwe", + "name": "CWE-120", + "value": "120", + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ], + "file": "cplusplus/src/hello.cpp", + "line": 6, + "url": "https://cwe.mitre.org/data/definitions/119.html" + }, + { + "category": "sast", + "message": "Does not check for buffer overflows when copying to destination [MS-banned] (CWE-120)", + "cve": "cplusplus/src/hello.cpp:331c04062c4fe0c7c486f66f59e82ad146ab33cdd76ae757ca41f392d568cbd0:CWE-120", + "confidence": "Low", + "solution": "Consider using snprintf, strcpy_s, or strlcpy (warning: strncpy easily misused)", + "location": { + "file": "cplusplus/src/hello.cpp", + "start_line": 7 + }, + "identifiers": [ + { + "type": "flawfinder_func_name", + "name": "Flawfinder - strcpy", + "value": "strcpy" + }, + { + "type": "cwe", + "name": "CWE-120", + "value": "120", + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ], + "file": "cplusplus/src/hello.cpp", + "line": 7, + "url": "https://cwe.mitre.org/data/definitions/120.html" + } + ] +} diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js index 267b46d8749..702dc20619d 100644 --- a/spec/frontend/invite_members/components/invite_members_modal_spec.js +++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js @@ -6,6 +6,7 @@ import { GlSprintf, GlLink, GlModal, + GlFormCheckboxGroup, } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import { stubComponent } from 'helpers/stub_component'; @@ -15,7 +16,8 @@ import Api from '~/api'; import ExperimentTracking from '~/experimentation/experiment_tracking'; import InviteMembersModal from '~/invite_members/components/invite_members_modal.vue'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; -import { INVITE_MEMBERS_IN_COMMENT } from '~/invite_members/constants'; +import { INVITE_MEMBERS_IN_COMMENT, MEMBER_AREAS_OF_FOCUS } from '~/invite_members/constants'; +import eventHub from '~/invite_members/event_hub'; import axios from '~/lib/utils/axios_utils'; import httpStatus from '~/lib/utils/http_status'; import { apiPaths, membersApiResponse, invitationsApiResponse } from '../mock_data/api_responses'; @@ -32,7 +34,12 @@ const inviteeType = 'members'; const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 }; const defaultAccessLevel = 10; const inviteSource = 'unknown'; +const noSelectionAreasOfFocus = ['no_selection']; const helpLink = 'https://example.com'; +const areasOfFocusOptions = [ + { text: 'area1', value: 'area1' }, + { text: 'area2', value: 'area2' }, +]; const user1 = { id: 1, name: 'Name One', username: 'one_1', avatar_url: '' }; const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '' }; @@ -58,7 +65,9 @@ const createComponent = (data = {}, props = {}) => { isProject, inviteeType, accessLevels, + areasOfFocusOptions, defaultAccessLevel, + noSelectionAreasOfFocus, helpLink, ...props, }, @@ -119,6 +128,7 @@ describe('InviteMembersModal', () => { const findMembersFormGroup = () => wrapper.findByTestId('members-form-group'); const membersFormGroupInvalidFeedback = () => findMembersFormGroup().props('invalidFeedback'); const findMembersSelect = () => wrapper.findComponent(MembersTokenSelect); + const findAreaofFocusCheckBoxGroup = () => wrapper.findComponent(GlFormCheckboxGroup); describe('rendering the modal', () => { beforeEach(() => { @@ -164,6 +174,21 @@ describe('InviteMembersModal', () => { }); }); + describe('rendering the areas_of_focus', () => { + it('renders the areas_of_focus checkboxes', () => { + createComponent(); + + expect(findAreaofFocusCheckBoxGroup().props('options')).toBe(areasOfFocusOptions); + expect(findAreaofFocusCheckBoxGroup().exists()).toBe(true); + }); + + it('does not render the areas_of_focus checkboxes', () => { + createComponent({}, { areasOfFocusOptions: [] }); + + expect(findAreaofFocusCheckBoxGroup().exists()).toBe(false); + }); + }); + describe('displaying the correct introText', () => { describe('when inviting to a project', () => { describe('when inviting members', () => { @@ -214,6 +239,20 @@ describe('InviteMembersModal', () => { "email 'email@example.com' does not match the allowed domains: example1.org"; const expectedSyntaxError = 'email contains an invalid email address'; + it('calls the API with the expected focus data when an areas_of_focus checkbox is clicked', () => { + const spy = jest.spyOn(Api, 'addGroupMembersByUserId'); + const expectedFocus = [areasOfFocusOptions[0].value]; + createComponent({ newUsersToInvite: [user1] }); + + findAreaofFocusCheckBoxGroup().vm.$emit('input', expectedFocus); + clickInviteButton(); + + expect(spy).toHaveBeenCalledWith( + user1.id.toString(), + expect.objectContaining({ areas_of_focus: expectedFocus }), + ); + }); + describe('when inviting an existing user to group by user ID', () => { const postData = { user_id: '1,2', @@ -221,6 +260,7 @@ describe('InviteMembersModal', () => { expires_at: undefined, invite_source: inviteSource, format: 'json', + areas_of_focus: noSelectionAreasOfFocus, }; describe('when member is added successfully', () => { @@ -230,30 +270,34 @@ describe('InviteMembersModal', () => { wrapper.vm.$toast = { show: jest.fn() }; jest.spyOn(Api, 'addGroupMembersByUserId').mockResolvedValue({ data: postData }); jest.spyOn(wrapper.vm, 'showToastMessageSuccess'); - - clickInviteButton(); }); - it('sets isLoading on the Invite button when it is clicked', () => { - expect(findInviteButton().props('loading')).toBe(true); - }); + it('includes the non-default selected areas of focus', () => { + const focus = ['abc']; + const updatedPostData = { ...postData, areas_of_focus: focus }; + wrapper.setData({ selectedAreasOfFocus: focus }); - it('removes isLoading from the Invite button when request completes', async () => { - await waitForPromises(); + clickInviteButton(); - expect(findInviteButton().props('loading')).toBe(false); + expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, updatedPostData); }); - it('calls Api addGroupMembersByUserId with the correct params', async () => { - await waitForPromises; + describe('when triggered from regular mounting', () => { + beforeEach(() => { + clickInviteButton(); + }); - expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData); - }); + it('sets isLoading on the Invite button when it is clicked', () => { + expect(findInviteButton().props('loading')).toBe(true); + }); - it('displays the successful toastMessage', async () => { - await waitForPromises; + it('calls Api addGroupMembersByUserId with the correct params', () => { + expect(Api.addGroupMembersByUserId).toHaveBeenCalledWith(id, postData); + }); - expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); + it('displays the successful toastMessage', () => { + expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); + }); }); }); @@ -353,6 +397,7 @@ describe('InviteMembersModal', () => { expires_at: undefined, email: 'email@example.com', invite_source: inviteSource, + areas_of_focus: noSelectionAreasOfFocus, format: 'json', }; @@ -363,16 +408,30 @@ describe('InviteMembersModal', () => { wrapper.vm.$toast = { show: jest.fn() }; jest.spyOn(Api, 'inviteGroupMembersByEmail').mockResolvedValue({ data: postData }); jest.spyOn(wrapper.vm, 'showToastMessageSuccess'); + }); + + it('includes the non-default selected areas of focus', () => { + const focus = ['abc']; + const updatedPostData = { ...postData, areas_of_focus: focus }; + wrapper.setData({ selectedAreasOfFocus: focus }); clickInviteButton(); - }); - it('calls Api inviteGroupMembersByEmail with the correct params', () => { - expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, postData); + expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, updatedPostData); }); - it('displays the successful toastMessage', () => { - expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); + describe('when triggered from regular mounting', () => { + beforeEach(() => { + clickInviteButton(); + }); + + it('calls Api inviteGroupMembersByEmail with the correct params', () => { + expect(Api.inviteGroupMembersByEmail).toHaveBeenCalledWith(id, postData); + }); + + it('displays the successful toastMessage', () => { + expect(wrapper.vm.showToastMessageSuccess).toHaveBeenCalled(); + }); }); }); @@ -465,6 +524,7 @@ describe('InviteMembersModal', () => { access_level: defaultAccessLevel, expires_at: undefined, invite_source: inviteSource, + areas_of_focus: noSelectionAreasOfFocus, format: 'json', }; @@ -501,7 +561,7 @@ describe('InviteMembersModal', () => { }); it('calls Apis with the invite source passed through to openModal', () => { - wrapper.vm.openModal({ inviteeType: 'members', source: '_invite_source_' }); + eventHub.$emit('openModal', { inviteeType: 'members', source: '_invite_source_' }); clickInviteButton(); @@ -579,9 +639,7 @@ describe('InviteMembersModal', () => { clickInviteButton(); }); - it('displays the generic error message', async () => { - await waitForPromises(); - + it('displays the generic error message', () => { expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong'); }); }); @@ -596,7 +654,7 @@ describe('InviteMembersModal', () => { }); it('tracks the invite', () => { - wrapper.vm.openModal({ inviteeType: 'members', source: INVITE_MEMBERS_IN_COMMENT }); + eventHub.$emit('openModal', { inviteeType: 'members', source: INVITE_MEMBERS_IN_COMMENT }); clickInviteButton(); @@ -605,19 +663,37 @@ describe('InviteMembersModal', () => { }); it('does not track invite for unknown source', () => { - wrapper.vm.openModal({ inviteeType: 'members', source: 'unknown' }); + eventHub.$emit('openModal', { inviteeType: 'members', source: 'unknown' }); clickInviteButton(); - expect(ExperimentTracking).not.toHaveBeenCalled(); + expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT); }); it('does not track invite undefined source', () => { - wrapper.vm.openModal({ inviteeType: 'members' }); + eventHub.$emit('openModal', { inviteeType: 'members' }); + + clickInviteButton(); + + expect(ExperimentTracking).not.toHaveBeenCalledWith(INVITE_MEMBERS_IN_COMMENT); + }); + + it('tracks the view for areas_of_focus', () => { + eventHub.$emit('openModal', { inviteeType: 'members' }); + + expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name); + expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.view); + }); + + it('tracks the invite for areas_of_focus', () => { + eventHub.$emit('openModal', { inviteeType: 'members' }); clickInviteButton(); - expect(ExperimentTracking).not.toHaveBeenCalled(); + expect(ExperimentTracking).toHaveBeenCalledWith(MEMBER_AREAS_OF_FOCUS.name); + expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith( + MEMBER_AREAS_OF_FOCUS.submit, + ); }); }); }); diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js index 9da370747fc..4867bd99a83 100644 --- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js @@ -12,7 +12,7 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/consta import eventHub from '~/vue_merge_request_widget/event_hub'; import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data'; import mockData from './mock_data'; diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js index 395c74dcba6..71ebe561def 100644 --- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js +++ b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js @@ -13,7 +13,7 @@ import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION, } from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; jest.mock('~/flash'); diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js index bef538e1ff1..4d579fa61df 100644 --- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js +++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js @@ -22,7 +22,7 @@ import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION, } from '~/vue_shared/security_reports/constants'; -import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql'; +import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql'; import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue'; jest.mock('~/flash'); diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb index 3d2adaa5b5d..e08ce09702f 100644 --- a/spec/helpers/invite_members_helper_spec.rb +++ b/spec/helpers/invite_members_helper_spec.rb @@ -14,6 +14,56 @@ RSpec.describe InviteMembersHelper do helper.extend(Gitlab::Experimentation::ControllerConcern) end + describe '#common_invite_modal_dataset' do + context 'when member_areas_of_focus is enabled', :experiment do + context 'with control experience' do + before do + stub_experiments(member_areas_of_focus: :control) + end + + it 'has expected attributes' do + attributes = { + areas_of_focus_options: [], + no_selection_areas_of_focus: [] + } + + expect(helper.common_invite_modal_dataset(project)).to include(attributes) + end + end + + context 'with candidate experience' do + before do + stub_experiments(member_areas_of_focus: :candidate) + end + + it 'has expected attributes', :aggregate_failures do + output = helper.common_invite_modal_dataset(project) + + expect(output[:no_selection_areas_of_focus]).to eq ['no_selection'] + expect(Gitlab::Json.parse(output[:areas_of_focus_options]).first['value']).to eq 'Contribute to the codebase' + end + end + end + + context 'when member_areas_of_focus is disabled' do + before do + stub_feature_flags(member_areas_of_focus: false) + end + + it 'has expected attributes' do + attributes = { + id: project.id, + name: project.name, + default_access_level: Gitlab::Access::GUEST, + areas_of_focus_options: [], + no_selection_areas_of_focus: [] + } + + expect(helper.common_invite_modal_dataset(project)).to match(attributes) + end + end + end + context 'with project' do before do allow(helper).to receive(:current_user) { owner } diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 1dd29872324..2e8ebb2de4b 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -917,4 +917,40 @@ RSpec.describe ProjectsHelper do subject end end + + describe '#project_permissions_panel_data' do + subject { helper.project_permissions_panel_data(project) } + + before do + allow(helper).to receive(:can?) { true } + allow(helper).to receive(:current_user).and_return(user) + end + + it 'includes project_permissions_settings' do + settings = subject.dig(:currentSettings) + + expect(settings).to include( + packagesEnabled: !!project.packages_enabled, + visibilityLevel: project.visibility_level, + requestAccessEnabled: !!project.request_access_enabled, + issuesAccessLevel: project.project_feature.issues_access_level, + repositoryAccessLevel: project.project_feature.repository_access_level, + forkingAccessLevel: project.project_feature.forking_access_level, + mergeRequestsAccessLevel: project.project_feature.merge_requests_access_level, + buildsAccessLevel: project.project_feature.builds_access_level, + wikiAccessLevel: project.project_feature.wiki_access_level, + snippetsAccessLevel: project.project_feature.snippets_access_level, + pagesAccessLevel: project.project_feature.pages_access_level, + analyticsAccessLevel: project.project_feature.analytics_access_level, + containerRegistryEnabled: !!project.container_registry_enabled, + lfsEnabled: !!project.lfs_enabled, + emailsDisabled: project.emails_disabled?, + metricsDashboardAccessLevel: project.project_feature.metrics_dashboard_access_level, + operationsAccessLevel: project.project_feature.operations_access_level, + showDefaultAwardEmojis: project.show_default_award_emojis?, + securityAndComplianceAccessLevel: project.security_and_compliance_access_level, + containerRegistryAccessLevel: project.project_feature.container_registry_access_level + ) + end + end end diff --git a/spec/initializers/0_log_deprecations_spec.rb b/spec/initializers/0_log_deprecations_spec.rb new file mode 100644 index 00000000000..35bceb2f132 --- /dev/null +++ b/spec/initializers/0_log_deprecations_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe '0_log_deprecations' do + def load_initializer + load Rails.root.join('config/initializers/0_log_deprecations.rb') + end + + let(:env_var) { '1' } + + before do + stub_env('GITLAB_LOG_DEPRECATIONS', env_var) + load_initializer + end + + after do + # reset state changed by initializer + Warning.clear + ActiveSupport::Notifications.unsubscribe('deprecation.rails') + end + + context 'for Ruby deprecations' do + context 'when catching deprecations through Kernel#warn' do + it 'also logs them to deprecation logger' do + expect(Gitlab::DeprecationJsonLogger).to receive(:info).with( + message: 'ABC gem is deprecated', + source: 'ruby' + ) + + expect { warn('ABC gem is deprecated') }.to output.to_stderr + end + end + + context 'for other messages from Kernel#warn' do + it 'does not log them to deprecation logger' do + expect(Gitlab::DeprecationJsonLogger).not_to receive(:info) + + expect { warn('Sure is hot today') }.to output.to_stderr + end + end + + context 'when disabled via environment' do + let(:env_var) { '0' } + + it 'does not log them to deprecation logger' do + expect(Gitlab::DeprecationJsonLogger).not_to receive(:info) + + expect { warn('ABC gem is deprecated') }.to output.to_stderr + end + end + end + + context 'for Rails deprecations' do + it 'logs them to deprecation logger' do + expect(Gitlab::DeprecationJsonLogger).to receive(:info).with( + message: match(/^DEPRECATION WARNING: ABC will be removed/), + source: 'rails' + ) + + expect { ActiveSupport::Deprecation.warn('ABC will be removed') }.to output.to_stderr + end + + context 'when disabled via environment' do + let(:env_var) { '0' } + + it 'does not log them to deprecation logger' do + expect(Gitlab::DeprecationJsonLogger).not_to receive(:info) + + expect { ActiveSupport::Deprecation.warn('ABC will be removed') }.to output.to_stderr + end + end + end +end diff --git a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb index 7c6b0cac24b..cba41166be4 100644 --- a/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/alert_reference_filter_spec.rb @@ -220,4 +220,33 @@ RSpec.describe Banzai::Filter::References::AlertReferenceFilter do expect(reference_filter(act, project: nil, group: group).to_html).to eq exp end end + + context 'checking N+1' do + let(:namespace) { create(:namespace) } + let(:project2) { create(:project, :public, namespace: namespace) } + let(:alert2) { create(:alert_management_alert, project: project2) } + let(:alert_reference) { alert.to_reference } + let(:alert2_reference) { alert2.to_reference(full: true) } + + it 'does not have N+1 per multiple references per project', :use_sql_query_cache do + markdown = "#{alert_reference}" + max_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + reference_filter(markdown) + end.count + + expect(max_count).to eq 1 + + markdown = "#{alert_reference} ^alert#2 ^alert#3 ^alert#4 #{alert2_reference}" + + # Since we're not batching alert queries across projects, + # we have to account for that. + # 1 for both projects, 1 for alerts in each project == 3 + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359 + max_count += 2 + + expect do + reference_filter(markdown) + end.not_to exceed_all_query_limit(max_count) + end + end end diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb new file mode 100644 index 00000000000..35eba4cacf4 --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -0,0 +1,350 @@ +# frozen_string_literal: true + +# TODO remove duplication from spec/lib/gitlab/ci/parsers/security/common_spec.rb and spec/lib/gitlab/ci/parsers/security/common_spec.rb +# See https://gitlab.com/gitlab-org/gitlab/-/issues/336589 +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::Common do + describe '#parse!' do + where(vulnerability_finding_signatures_enabled: [true, false]) + with_them do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:artifact) { build(:ci_job_artifact, :common_security_report) } + let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) } + # The path 'yarn.lock' was initially used by DependencyScanning, it is okay for SAST locations to use it, but this could be made better + let(:location) { ::Gitlab::Ci::Reports::Security::Locations::Sast.new(file_path: 'yarn.lock', start_line: 1, end_line: 1) } + let(:tracking_data) { nil } + + before do + allow_next_instance_of(described_class) do |parser| + allow(parser).to receive(:create_location).and_return(location) + allow(parser).to receive(:tracking_data).and_return(tracking_data) + end + + artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) } + end + + describe 'schema validation' do + let(:validator_class) { Gitlab::Ci::Parsers::Security::Validators::SchemaValidator } + let(:parser) { described_class.new('{}', report, vulnerability_finding_signatures_enabled, validate: validate) } + + subject(:parse_report) { parser.parse! } + + before do + allow(validator_class).to receive(:new).and_call_original + end + + context 'when the validate flag is set as `false`' do + let(:validate) { false } + + it 'does not run the validation logic' do + parse_report + + expect(validator_class).not_to have_received(:new) + end + end + + context 'when the validate flag is set as `true`' do + let(:validate) { true } + let(:valid?) { false } + + before do + allow_next_instance_of(validator_class) do |instance| + allow(instance).to receive(:valid?).and_return(valid?) + allow(instance).to receive(:errors).and_return(['foo']) + end + + allow(parser).to receive_messages(create_scanner: true, create_scan: true) + end + + it 'instantiates the validator with correct params' do + parse_report + + expect(validator_class).to have_received(:new).with(report.type, {}) + end + + context 'when the report data is not valid according to the schema' do + it 'adds errors to the report' do + expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }]) + end + + it 'does not try to create report entities' do + parse_report + + expect(parser).not_to have_received(:create_scanner) + expect(parser).not_to have_received(:create_scan) + end + end + + context 'when the report data is valid according to the schema' do + let(:valid?) { true } + + it 'does not add errors to the report' do + expect { parse_report }.not_to change { report.errors }.from([]) + end + + it 'keeps the execution flow as normal' do + parse_report + + expect(parser).to have_received(:create_scanner) + expect(parser).to have_received(:create_scan) + end + end + end + end + + describe 'parsing finding.name' do + let(:artifact) { build(:ci_job_artifact, :common_security_report_with_blank_names) } + + context 'when message is provided' do + it 'sets message from the report as a finding name' do + vulnerability = report.findings.find { |x| x.compare_key == 'CVE-1020' } + expected_name = Gitlab::Json.parse(vulnerability.raw_metadata)['message'] + + expect(vulnerability.name).to eq(expected_name) + end + end + + context 'when message is not provided' do + context 'and name is provided' do + it 'sets name from the report as a name' do + vulnerability = report.findings.find { |x| x.compare_key == 'CVE-1030' } + expected_name = Gitlab::Json.parse(vulnerability.raw_metadata)['name'] + + expect(vulnerability.name).to eq(expected_name) + end + end + + context 'and name is not provided' do + context 'when CVE identifier exists' do + it 'combines identifier with location to create name' do + vulnerability = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' } + expect(vulnerability.name).to eq("CVE-2017-11429 in yarn.lock") + end + end + + context 'when CWE identifier exists' do + it 'combines identifier with location to create name' do + vulnerability = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' } + expect(vulnerability.name).to eq("CWE-2017-11429 in yarn.lock") + end + end + + context 'when neither CVE nor CWE identifier exist' do + it 'combines identifier with location to create name' do + vulnerability = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' } + expect(vulnerability.name).to eq("other-2017-11429 in yarn.lock") + end + end + end + end + end + + describe 'parsing finding.details' do + context 'when details are provided' do + it 'sets details from the report' do + vulnerability = report.findings.find { |x| x.compare_key == 'CVE-1020' } + expected_details = Gitlab::Json.parse(vulnerability.raw_metadata)['details'] + + expect(vulnerability.details).to eq(expected_details) + end + end + + context 'when details are not provided' do + it 'sets empty hash' do + vulnerability = report.findings.find { |x| x.compare_key == 'CVE-1030' } + expect(vulnerability.details).to eq({}) + end + end + end + + describe 'top-level scanner' do + it 'is the primary scanner' do + expect(report.primary_scanner.external_id).to eq('gemnasium') + expect(report.primary_scanner.name).to eq('Gemnasium') + expect(report.primary_scanner.vendor).to eq('GitLab') + expect(report.primary_scanner.version).to eq('2.18.0') + end + + it 'returns nil report has no scanner' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.primary_scanner).to be_nil + end + end + + describe 'parsing scanners' do + subject(:scanner) { report.findings.first.scanner } + + context 'when vendor is not missing in scanner' do + it 'returns scanner with parsed vendor value' do + expect(scanner.vendor).to eq('GitLab') + end + end + end + + describe 'parsing scan' do + it 'returns scan object for each finding' do + scans = report.findings.map(&:scan) + + expect(scans.map(&:status).all?('success')).to be(true) + expect(scans.map(&:start_time).all?('placeholder-value')).to be(true) + expect(scans.map(&:end_time).all?('placeholder-value')).to be(true) + expect(scans.size).to eq(3) + expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan) + end + + it 'returns nil when scan is not a hash' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.scan).to be(nil) + end + end + + describe 'parsing schema version' do + it 'parses the version' do + expect(report.version).to eq('14.0.2') + end + + it 'returns nil when there is no version' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.version).to be_nil + end + end + + describe 'parsing analyzer' do + it 'associates analyzer with report' do + expect(report.analyzer.id).to eq('common-analyzer') + expect(report.analyzer.name).to eq('Common Analyzer') + expect(report.analyzer.version).to eq('2.0.1') + expect(report.analyzer.vendor).to eq('Common') + end + + it 'returns nil when analyzer data is not available' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.analyzer).to be_nil + end + end + + describe 'parsing links' do + it 'returns links object for each finding', :aggregate_failures do + links = report.findings.flat_map(&:links) + + expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030']) + expect(links.map(&:name)).to match_array([nil, 'CVE-1030']) + expect(links.size).to eq(2) + expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link) + end + end + + describe 'setting the uuid' do + let(:finding_uuids) { report.findings.map(&:uuid) } + let(:uuid_1) do + Security::VulnerabilityUUID.generate( + report_type: "sast", + primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint, + location_fingerprint: location.fingerprint, + project_id: pipeline.project_id + ) + end + + let(:uuid_2) do + Security::VulnerabilityUUID.generate( + report_type: "sast", + primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint, + location_fingerprint: location.fingerprint, + project_id: pipeline.project_id + ) + end + + let(:expected_uuids) { [uuid_1, uuid_2, nil] } + + it 'sets the UUIDv5 for findings', :aggregate_failures do + allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report| + allow(report).to receive(:type).and_return('sast') + + expect(finding_uuids).to match_array(expected_uuids) + end + end + end + + describe 'parsing tracking' do + let(:tracking_data) do + { + 'type' => 'source', + 'items' => [ + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' } + ] + ] + } + end + + context 'with valid tracking information' do + it 'creates signatures for each algorithm' do + finding = report.findings.first + expect(finding.signatures.size).to eq(3) + expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset']) + end + end + + context 'with invalid tracking information' do + let(:tracking_data) do + { + 'type' => 'source', + 'items' => [ + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' } + ] + ] + } + end + + it 'ignores invalid algorithm types' do + finding = report.findings.first + expect(finding.signatures.size).to eq(2) + expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location']) + end + end + + context 'with valid tracking information' do + it 'creates signatures for each signature algorithm' do + finding = report.findings.first + expect(finding.signatures.size).to eq(3) + expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset]) + + signatures = finding.signatures.index_by(&:algorithm_type) + expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] } + expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value']) + expect(signatures['location'].signature_value).to eq(expected_values['location']['value']) + expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value']) + end + + it 'sets the uuid according to the higest priority signature' do + finding = report.findings.first + highest_signature = finding.signatures.max_by(&:priority) + + identifiers = if vulnerability_finding_signatures_enabled + "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}" + else + "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}" + end + + expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers)) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb new file mode 100644 index 00000000000..4bc48f6611a --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::Sast do + using RSpec::Parameterized::TableSyntax + + describe '#parse!' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:created_at) { 2.weeks.ago } + + context "when parsing valid reports" do + where(:report_format, :report_version, :scanner_length, :finding_length, :identifier_length, :file_path, :line) do + :sast | '14.0.0' | 1 | 5 | 6 | 'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy' | 47 + :sast_deprecated | '1.2' | 3 | 33 | 17 | 'python/hardcoded/hardcoded-tmp.py' | 1 + end + + with_them do + let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) } + let(:artifact) { create(:ci_job_artifact, report_format) } + + before do + artifact.each_blob { |blob| described_class.parse!(blob, report) } + end + + it "parses all identifiers and findings" do + expect(report.findings.length).to eq(finding_length) + expect(report.identifiers.length).to eq(identifier_length) + expect(report.scanners.length).to eq(scanner_length) + end + + it 'generates expected location' do + location = report.findings.first.location + + expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Sast) + expect(location).to have_attributes( + file_path: file_path, + end_line: line, + start_line: line + ) + end + + it "generates expected metadata_version" do + expect(report.findings.first.metadata_version).to eq(report_version) + end + end + end + + context "when parsing an empty report" do + let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', pipeline, created_at) } + let(:blob) { Gitlab::Json.generate({}) } + + it { expect(described_class.parse!(blob, report)).to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb new file mode 100644 index 00000000000..1d361e16aad --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::SecretDetection do + describe '#parse!' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:created_at) { 2.weeks.ago } + + context "when parsing valid reports" do + where(report_format: %i(secret_detection)) + + with_them do + let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) } + let(:artifact) { create(:ci_job_artifact, report_format) } + + before do + artifact.each_blob { |blob| described_class.parse!(blob, report) } + end + + it "parses all identifiers and findings" do + expect(report.findings.length).to eq(1) + expect(report.identifiers.length).to eq(1) + expect(report.scanners.length).to eq(1) + end + + it 'generates expected location' do + location = report.findings.first.location + + expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::SecretDetection) + expect(location).to have_attributes( + file_path: 'aws-key.py', + start_line: nil, + end_line: nil, + class_name: nil, + method_name: nil + ) + end + + it "generates expected metadata_version" do + expect(report.findings.first.metadata_version).to eq('3.0') + end + end + end + + context "when parsing an empty report" do + let(:report) { Gitlab::Ci::Reports::Security::Report.new('secret_detection', pipeline, created_at) } + let(:blob) { Gitlab::Json.generate({}) } + + it { expect(described_class.parse!(blob, report)).to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb new file mode 100644 index 00000000000..f434ffd12bf --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do + using RSpec::Parameterized::TableSyntax + + where(:report_type, :expected_errors, :valid_data) do + :sast | ['root is missing required keys: vulnerabilities'] | { 'version' => '10.0.0', 'vulnerabilities' => [] } + :secret_detection | ['root is missing required keys: vulnerabilities'] | { 'version' => '10.0.0', 'vulnerabilities' => [] } + end + + with_them do + let(:validator) { described_class.new(report_type, report_data) } + + describe '#valid?' do + subject { validator.valid? } + + context 'when given data is invalid according to the schema' do + let(:report_data) { {} } + + it { is_expected.to be_falsey } + end + + context 'when given data is valid according to the schema' do + let(:report_data) { valid_data } + + it { is_expected.to be_truthy } + end + end + + describe '#errors' do + let(:report_data) { { 'version' => '10.0.0' } } + + subject { validator.errors } + + it { is_expected.to eq(expected_errors) } + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb new file mode 100644 index 00000000000..c56177a6453 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::AggregatedReport do + subject { described_class.new(reports, findings) } + + let(:reports) { build_list(:ci_reports_security_report, 1) } + let(:findings) { build_list(:ci_reports_security_finding, 1) } + + describe '#created_at' do + context 'no reports' do + let(:reports) { [] } + + it 'has no created date' do + expect(subject.created_at).to be_nil + end + end + + context 'report with no created date' do + let(:reports) { build_list(:ci_reports_security_report, 1, created_at: nil) } + + it 'has no created date' do + expect(subject.created_at).to be_nil + end + end + + context 'has reports' do + let(:a_long_time_ago) { 2.months.ago } + let(:a_while_ago) { 2.weeks.ago } + let(:yesterday) { 1.day.ago } + + let(:reports) do + [build(:ci_reports_security_report, created_at: a_while_ago), + build(:ci_reports_security_report, created_at: a_long_time_ago), + build(:ci_reports_security_report, created_at: nil), + build(:ci_reports_security_report, created_at: yesterday)] + end + + it 'has oldest created date' do + expect(subject.created_at).to eq(a_long_time_ago) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb new file mode 100644 index 00000000000..784c1183320 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::FindingKey do + using RSpec::Parameterized::TableSyntax + + describe '#==' do + where(:location_fp_1, :location_fp_2, :identifier_fp_1, :identifier_fp_2, :equals?) do + nil | 'different location fp' | 'identifier fp' | 'different identifier fp' | false + 'location fp' | nil | 'identifier fp' | 'different identifier fp' | false + 'location fp' | 'different location fp' | nil | 'different identifier fp' | false + 'location fp' | 'different location fp' | 'identifier fp' | nil | false + nil | nil | 'identifier fp' | 'identifier fp' | false + 'location fp' | 'location fp' | nil | nil | false + nil | nil | nil | nil | false + 'location fp' | 'different location fp' | 'identifier fp' | 'different identifier fp' | false + 'location fp' | 'different location fp' | 'identifier fp' | 'identifier fp' | false + 'location fp' | 'location fp' | 'identifier fp' | 'different identifier fp' | false + 'location fp' | 'location fp' | 'identifier fp' | 'identifier fp' | true + end + + with_them do + let(:finding_key_1) do + build(:ci_reports_security_finding_key, + location_fingerprint: location_fp_1, + identifier_fingerprint: identifier_fp_1) + end + + let(:finding_key_2) do + build(:ci_reports_security_finding_key, + location_fingerprint: location_fp_2, + identifier_fingerprint: identifier_fp_2) + end + + subject { finding_key_1 == finding_key_2 } + + it { is_expected.to be(equals?) } + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb new file mode 100644 index 00000000000..23e6b40a039 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do + subject { described_class.new(params.with_indifferent_access) } + + let(:params) do + { + algorithm_type: 'hash', + signature_value: 'SIGNATURE' + } + end + + describe '#initialize' do + context 'when a supported algorithm type is given' do + it 'allows itself to be created' do + expect(subject.algorithm_type).to eq(params[:algorithm_type]) + expect(subject.signature_value).to eq(params[:signature_value]) + end + + describe '#valid?' do + it 'returns true' do + expect(subject.valid?).to eq(true) + end + end + end + end + + describe '#valid?' do + context 'when supported algorithm_type is given' do + it 'is valid' do + expect(subject.valid?).to eq(true) + end + end + + context 'when an unsupported algorithm_type is given' do + let(:params) do + { + algorithm_type: 'INVALID', + signature_value: 'SIGNATURE' + } + end + + it 'is not valid' do + expect(subject.valid?).to eq(false) + end + end + end + + describe '#to_hash' do + it 'returns a hash representation of the signature' do + expect(subject.to_hash).to eq( + algorithm_type: params[:algorithm_type], + signature_sha: Digest::SHA1.digest(params[:signature_value]) + ) + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb new file mode 100644 index 00000000000..effa7a60400 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Locations::Sast do + let(:params) do + { + file_path: 'src/main/App.java', + start_line: 29, + end_line: 31, + class_name: 'com.gitlab.security_products.tests.App', + method_name: 'insecureCypher' + } + end + + let(:mandatory_params) { %i[file_path start_line] } + let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') } + let(:expected_fingerprint_path) { 'App.java' } + + it_behaves_like 'vulnerability location' +end diff --git a/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb new file mode 100644 index 00000000000..3b84a548713 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Locations::SecretDetection do + let(:params) do + { + file_path: 'src/main/App.java', + start_line: 29, + end_line: 31, + class_name: 'com.gitlab.security_products.tests.App', + method_name: 'insecureCypher' + } + end + + let(:mandatory_params) { %i[file_path start_line] } + let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') } + let(:expected_fingerprint_path) { 'App.java' } + + it_behaves_like 'vulnerability location' +end diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb new file mode 100644 index 00000000000..5a85c3f19fc --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Report do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:created_at) { 2.weeks.ago } + + subject(:report) { described_class.new('sast', pipeline, created_at) } + + it { expect(report.type).to eq('sast') } + it { is_expected.to delegate_method(:project_id).to(:pipeline) } + + describe '#add_scanner' do + let(:scanner) { create(:ci_reports_security_scanner, external_id: 'find_sec_bugs') } + + subject { report.add_scanner(scanner) } + + it 'stores given scanner params in the map' do + subject + + expect(report.scanners).to eq({ 'find_sec_bugs' => scanner }) + end + + it 'returns the added scanner' do + expect(subject).to eq(scanner) + end + end + + describe '#add_identifier' do + let(:identifier) { create(:ci_reports_security_identifier) } + + subject { report.add_identifier(identifier) } + + it 'stores given identifier params in the map' do + subject + + expect(report.identifiers).to eq({ identifier.fingerprint => identifier }) + end + + it 'returns the added identifier' do + expect(subject).to eq(identifier) + end + end + + describe '#add_finding' do + let(:finding) { create(:ci_reports_security_finding) } + + it 'enriches given finding and stores it in the collection' do + report.add_finding(finding) + + expect(report.findings).to eq([finding]) + end + end + + describe '#clone_as_blank' do + let(:report) do + create( + :ci_reports_security_report, + findings: [create(:ci_reports_security_finding)], + scanners: [create(:ci_reports_security_scanner)], + identifiers: [create(:ci_reports_security_identifier)] + ) + end + + it 'creates a blank report with copied type and pipeline' do + clone = report.clone_as_blank + + expect(clone.type).to eq(report.type) + expect(clone.pipeline).to eq(report.pipeline) + expect(clone.created_at).to eq(report.created_at) + expect(clone.findings).to eq([]) + expect(clone.scanners).to eq({}) + expect(clone.identifiers).to eq({}) + end + end + + describe '#replace_with!' do + let(:report) do + create( + :ci_reports_security_report, + findings: [create(:ci_reports_security_finding)], + scanners: [create(:ci_reports_security_scanner)], + identifiers: [create(:ci_reports_security_identifier)] + ) + end + + let(:other_report) do + create( + :ci_reports_security_report, + findings: [create(:ci_reports_security_finding, compare_key: 'other_finding')], + scanners: [create(:ci_reports_security_scanner, external_id: 'other_scanner', name: 'Other Scanner')], + identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')] + ) + end + + before do + report.replace_with!(other_report) + end + + it 'replaces report contents with other reports contents' do + expect(report.findings).to eq(other_report.findings) + expect(report.scanners).to eq(other_report.scanners) + expect(report.identifiers).to eq(other_report.identifiers) + end + end + + describe '#merge!' do + let(:merged_report) { double('Report') } + + before do + merge_reports_service = double('MergeReportsService') + + allow(::Security::MergeReportsService).to receive(:new).and_return(merge_reports_service) + allow(merge_reports_service).to receive(:execute).and_return(merged_report) + allow(report).to receive(:replace_with!) + end + + subject { report.merge!(described_class.new('sast', pipeline, created_at)) } + + it 'invokes the merge with other report and then replaces this report contents by merge result' do + subject + + expect(report).to have_received(:replace_with!).with(merged_report) + end + end + + describe '#primary_scanner' do + let(:scanner_1) { create(:ci_reports_security_scanner, external_id: 'external_id_1') } + let(:scanner_2) { create(:ci_reports_security_scanner, external_id: 'external_id_2') } + + subject { report.primary_scanner } + + before do + report.add_scanner(scanner_1) + report.add_scanner(scanner_2) + end + + it { is_expected.to eq(scanner_1) } + end + + describe '#add_error' do + context 'when the message is not given' do + it 'adds a new error to report with the generic error message' do + expect { report.add_error('foo') }.to change { report.errors } + .from([]) + .to([{ type: 'foo', message: 'An unexpected error happened!' }]) + end + end + + context 'when the message is given' do + it 'adds a new error to report' do + expect { report.add_error('foo', 'bar') }.to change { report.errors } + .from([]) + .to([{ type: 'foo', message: 'bar' }]) + end + end + end + + describe 'errored?' do + subject { report.errored? } + + context 'when the report does not have any errors' do + it { is_expected.to be_falsey } + end + + context 'when the report has errors' do + before do + report.add_error('foo', 'bar') + end + + it { is_expected.to be_truthy } + end + end + + describe '#primary_scanner_order_to' do + let(:scanner_1) { build(:ci_reports_security_scanner) } + let(:scanner_2) { build(:ci_reports_security_scanner) } + let(:report_1) { described_class.new('sast', pipeline, created_at) } + let(:report_2) { described_class.new('sast', pipeline, created_at) } + + subject(:compare_based_on_primary_scanners) { report_1.primary_scanner_order_to(report_2) } + + context 'when the primary scanner of the receiver is nil' do + context 'when the primary scanner of the other is nil' do + it { is_expected.to be(1) } + end + + context 'when the primary scanner of the other is not nil' do + before do + report_2.add_scanner(scanner_2) + end + + it { is_expected.to be(1) } + end + end + + context 'when the primary scanner of the receiver is not nil' do + before do + report_1.add_scanner(scanner_1) + end + + context 'when the primary scanner of the other is nil' do + let(:scanner_2) { nil } + + it { is_expected.to be(-1) } + end + + context 'when the primary scanner of the other is not nil' do + before do + report_2.add_scanner(scanner_2) + + allow(scanner_1).to receive(:<=>).and_return(0) + end + + it 'compares two scanners' do + expect(compare_based_on_primary_scanners).to be(0) + expect(scanner_1).to have_received(:<=>).with(scanner_2) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb new file mode 100644 index 00000000000..d6a18828120 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Reports do + let_it_be(:pipeline) { create(:ci_pipeline) } + let_it_be(:artifact) { create(:ci_job_artifact, :sast) } + + let(:security_reports) { described_class.new(pipeline) } + + describe '#get_report' do + subject { security_reports.get_report(report_type, artifact) } + + context 'when report type is sast' do + let(:report_type) { 'sast' } + + it { expect(subject.type).to eq('sast') } + it { expect(subject.created_at).to eq(artifact.created_at) } + + it 'initializes a new report and returns it' do + expect(Gitlab::Ci::Reports::Security::Report).to receive(:new) + .with('sast', pipeline, artifact.created_at).and_call_original + + is_expected.to be_a(Gitlab::Ci::Reports::Security::Report) + end + + context 'when report type is already allocated' do + before do + subject + end + + it 'does not initialize a new report' do + expect(Gitlab::Ci::Reports::Security::Report).not_to receive(:new) + + is_expected.to be_a(Gitlab::Ci::Reports::Security::Report) + end + end + end + end + + describe '#findings' do + let(:finding_1) { build(:ci_reports_security_finding, severity: 'low') } + let(:finding_2) { build(:ci_reports_security_finding, severity: 'high') } + let!(:expected_findings) { [finding_1, finding_2] } + + subject { security_reports.findings } + + before do + security_reports.get_report('sast', artifact).add_finding(finding_1) + security_reports.get_report('dependency_scanning', artifact).add_finding(finding_2) + end + + it { is_expected.to match_array(expected_findings) } + end + + describe "#violates_default_policy_against?" do + let(:low_severity_sast) { build(:ci_reports_security_finding, severity: 'low', report_type: :sast) } + let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) } + let(:vulnerabilities_allowed) { 0 } + + subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed) } + + context 'when the target_reports is `nil`' do + let(:target_reports) { nil } + + context "when a report has unsafe vulnerability" do + before do + security_reports.get_report('sast', artifact).add_finding(high_severity_dast) + end + + it { is_expected.to be(true) } + end + + context "when none of the reports have an unsafe vulnerability" do + before do + security_reports.get_report('sast', artifact).add_finding(low_severity_sast) + end + + it { is_expected.to be(false) } + end + end + + context 'when the target_reports is not `nil`' do + let(:target_reports) { described_class.new(pipeline) } + + context "when a report has a new unsafe vulnerability" do + before do + security_reports.get_report('sast', artifact).add_finding(high_severity_dast) + security_reports.get_report('dependency_scanning', artifact).add_finding(low_severity_sast) + target_reports.get_report('dependency_scanning', artifact).add_finding(low_severity_sast) + end + + it { is_expected.to be(true) } + + context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do + let(:vulnerabilities_allowed) { 10000 } + + it { is_expected.to be(false) } + end + end + + context "when none of the reports have a new unsafe vulnerability" do + before do + security_reports.get_report('sast', artifact).add_finding(high_severity_dast) + security_reports.get_report('sast', artifact).add_finding(low_severity_sast) + target_reports.get_report('sast', artifact).add_finding(high_severity_dast) + end + + it { is_expected.to be(false) } + end + end + end +end diff --git a/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb b/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb new file mode 100644 index 00000000000..0a71699971e --- /dev/null +++ b/spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe VulnerabilityFindingSignatureHelpers do + let(:cls) do + Class.new do + include VulnerabilityFindingSignatureHelpers + attr_accessor :algorithm_type + + def initialize(algorithm_type) + @algorithm_type = algorithm_type + end + end + end + + describe '#priority' do + it 'returns numeric values of the priority string' do + expect(cls.new('scope_offset').priority).to eq(3) + expect(cls.new('location').priority).to eq(2) + expect(cls.new('hash').priority).to eq(1) + end + end + + describe '#self.priority' do + it 'returns the numeric value of the provided string' do + expect(cls.priority('scope_offset')).to eq(3) + expect(cls.priority('location')).to eq(2) + expect(cls.priority('hash')).to eq(1) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 87b3aea178c..0aad1257eea 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -89,6 +89,7 @@ RSpec.describe User do it { is_expected.to have_one(:atlassian_identity) } it { is_expected.to have_one(:user_highest_role) } it { is_expected.to have_one(:credit_card_validation) } + it { is_expected.to have_one(:banned_user) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } it { is_expected.to have_many(:project_members) } @@ -1959,6 +1960,42 @@ RSpec.describe User do end end + describe 'banning and unbanning a user', :aggregate_failures do + let(:user) { create(:user) } + + context 'banning a user' do + it 'bans and blocks the user' do + user.ban + + expect(user.banned?).to eq(true) + expect(user.blocked?).to eq(true) + end + + it 'creates a BannedUser record' do + expect { user.ban }.to change { Users::BannedUser.count }.by(1) + expect(Users::BannedUser.last.user_id).to eq(user.id) + end + end + + context 'unbanning a user' do + before do + user.ban! + end + + it 'activates the user' do + user.activate + + expect(user.banned?).to eq(false) + expect(user.active?).to eq(true) + end + + it 'deletes the BannedUser record' do + expect { user.activate }.to change { Users::BannedUser.count }.by(-1) + expect(Users::BannedUser.where(user_id: user.id)).not_to exist + end + end + end + describe '.filter_items' do let(:user) { double } diff --git a/spec/models/users/banned_user_spec.rb b/spec/models/users/banned_user_spec.rb new file mode 100644 index 00000000000..b55c4821d05 --- /dev/null +++ b/spec/models/users/banned_user_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::BannedUser do + describe 'relationships' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + before do + create(:user, :banned) + end + + it { is_expected.to validate_presence_of(:user) } + + it 'validates uniqueness of banned user id' do + is_expected.to validate_uniqueness_of(:user_id).with_message("banned user already exists") + end + end +end diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb index 0e6ac615da5..6f49ee08782 100644 --- a/spec/services/users/ban_service_spec.rb +++ b/spec/services/users/ban_service_spec.rb @@ -3,47 +3,68 @@ require 'spec_helper' RSpec.describe Users::BanService do - let(:current_user) { create(:admin) } + let(:user) { create(:user) } - subject(:service) { described_class.new(current_user) } + let_it_be(:current_user) { create(:admin) } - describe '#execute' do - subject(:operation) { service.execute(user) } + shared_examples 'does not modify the BannedUser record or user state' do + it 'does not modify the BannedUser record or user state' do + expect { ban_user }.not_to change { Users::BannedUser.count } + expect { ban_user }.not_to change { user.state } + end + end - context 'when successful' do - let(:user) { create(:user) } + context 'ban', :aggregate_failures do + subject(:ban_user) { described_class.new(current_user).execute(user) } - it { is_expected.to eq(status: :success) } + context 'when successful', :enable_admin_mode do + it 'returns success status' do + response = ban_user - it "bans the user" do - expect { operation }.to change { user.state }.to('banned') + expect(response[:status]).to eq(:success) end - it "blocks the user" do - expect { operation }.to change { user.blocked? }.from(false).to(true) + it 'bans the user' do + expect { ban_user }.to change { user.state }.from('active').to('banned') end - it 'logs ban in application logs' do - allow(Gitlab::AppLogger).to receive(:info) + it 'creates a BannedUser' do + expect { ban_user }.to change { Users::BannedUser.count }.by(1) + expect(Users::BannedUser.last.user_id).to eq(user.id) + end - operation + it 'logs ban in application logs' do + expect(Gitlab::AppLogger).to receive(:info).with(message: "User ban", user: "#{user.username}", email: "#{user.email}", ban_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") - expect(Gitlab::AppLogger).to have_received(:info).with(message: "User banned", user: "#{user.username}", email: "#{user.email}", banned_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + ban_user end end context 'when failed' do - let(:user) { create(:user, :blocked) } + context 'when user is blocked', :enable_admin_mode do + before do + user.block! + end - it 'returns error result' do - aggregate_failures 'error result' do - expect(operation[:status]).to eq(:error) - expect(operation[:message]).to match(/State cannot transition/) + it 'returns state error message' do + response = ban_user + + expect(response[:status]).to eq(:error) + expect(response[:message]).to match(/State cannot transition/) end + + it_behaves_like 'does not modify the BannedUser record or user state' end - it "does not change the user's state" do - expect { operation }.not_to change { user.state } + context 'when user is not an admin' do + it 'returns permissions error message' do + response = ban_user + + expect(response[:status]).to eq(:error) + expect(response[:message]).to match(/You are not allowed to ban a user/) + end + + it_behaves_like 'does not modify the BannedUser record or user state' end end end diff --git a/spec/services/users/banned_user_base_service_spec.rb b/spec/services/users/banned_user_base_service_spec.rb new file mode 100644 index 00000000000..29a549f0f49 --- /dev/null +++ b/spec/services/users/banned_user_base_service_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::BannedUserBaseService do + let(:admin) { create(:admin) } + let(:base_service) { described_class.new(admin) } + + describe '#initialize' do + it 'sets the current_user instance value' do + expect(base_service.instance_values["current_user"]).to eq(admin) + end + end +end diff --git a/spec/services/users/unban_service_spec.rb b/spec/services/users/unban_service_spec.rb new file mode 100644 index 00000000000..b2b3140ccb3 --- /dev/null +++ b/spec/services/users/unban_service_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::UnbanService do + let(:user) { create(:user) } + + let_it_be(:current_user) { create(:admin) } + + shared_examples 'does not modify the BannedUser record or user state' do + it 'does not modify the BannedUser record or user state' do + expect { unban_user }.not_to change { Users::BannedUser.count } + expect { unban_user }.not_to change { user.state } + end + end + + context 'unban', :aggregate_failures do + subject(:unban_user) { described_class.new(current_user).execute(user) } + + context 'when successful', :enable_admin_mode do + before do + user.ban! + end + + it 'returns success status' do + response = unban_user + + expect(response[:status]).to eq(:success) + end + + it 'unbans the user' do + expect { unban_user }.to change { user.state }.from('banned').to('active') + end + + it 'removes the BannedUser' do + expect { unban_user }.to change { Users::BannedUser.count }.by(-1) + expect(user.reload.banned_user).to be_nil + end + + it 'logs unban in application logs' do + expect(Gitlab::AppLogger).to receive(:info).with(message: "User unban", user: "#{user.username}", email: "#{user.email}", unban_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + + unban_user + end + end + + context 'when failed' do + context 'when user is already active', :enable_admin_mode do + it 'returns state error message' do + response = unban_user + + expect(response[:status]).to eq(:error) + expect(response[:message]).to match(/State cannot transition/) + end + + it_behaves_like 'does not modify the BannedUser record or user state' + end + + context 'when user is not an admin' do + before do + user.ban! + end + + it 'returns permissions error message' do + response = unban_user + + expect(response[:status]).to eq(:error) + expect(response[:message]).to match(/You are not allowed to unban a user/) + end + + it_behaves_like 'does not modify the BannedUser record or user state' + end + end + end +end diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 7b8cd6963c0..69ba20c1ca4 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -5,7 +5,7 @@ module Spec module Helpers module Features module InviteMembersModalHelper - def invite_member(name, role: 'Guest', expires_at: nil) + def invite_member(name, role: 'Guest', expires_at: nil, area_of_focus: false) click_on 'Invite members' page.within '#invite-members-modal' do @@ -14,6 +14,7 @@ module Spec wait_for_requests click_button name choose_options(role, expires_at) + choose_area_of_focus if area_of_focus click_button 'Invite' @@ -41,7 +42,14 @@ module Spec click_button role end - fill_in 'YYYY-MM-DD', with: expires_at.try(:strftime, '%Y-%m-%d') + fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at + end + + def choose_area_of_focus + page.within '[data-testid="area-of-focus-checks"]' do + check 'Contribute to the codebase' + check 'Collaborate on open issues and merge requests' + end end end end diff --git a/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb new file mode 100644 index 00000000000..3aa04a77e57 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'vulnerability location' do + describe '#initialize' do + subject { described_class.new(**params) } + + context 'when all params are given' do + it 'initializes an instance' do + expect { subject }.not_to raise_error + + expect(subject).to have_attributes(**params) + end + end + + where(:param) do + mandatory_params + end + + with_them do + context "when param #{params[:param]} is missing" do + before do + params.delete(param) + end + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError) + end + end + end + end + + describe '#fingerprint' do + subject { described_class.new(**params).fingerprint } + + it "generates expected fingerprint" do + expect(subject).to eq(expected_fingerprint) + end + end + + describe '#fingerprint_path' do + subject { described_class.new(**params).fingerprint_path } + + it "generates expected fingerprint" do + expect(subject).to eq(expected_fingerprint_path) + end + end + + describe '#==' do + let(:location_1) { create(:ci_reports_security_locations_sast) } + let(:location_2) { create(:ci_reports_security_locations_sast) } + + subject { location_1 == location_2 } + + it "returns true when fingerprints are equal" do + allow(location_1).to receive(:fingerprint).and_return('fingerprint') + allow(location_2).to receive(:fingerprint).and_return('fingerprint') + + expect(subject).to eq(true) + end + + it "returns false when fingerprints are different" do + allow(location_1).to receive(:fingerprint).and_return('fingerprint') + allow(location_2).to receive(:fingerprint).and_return('another_fingerprint') + + expect(subject).to eq(false) + end + end +end |