Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-08-02 18:08:56 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-08-02 18:08:56 +0300
commit6f998d352988f93f875db862353e814e95db1fe3 (patch)
tree3596c4b8cbc4d426c3aaf571ea5ba1a57ffe031e
parent3eec6c2511af2b10cd25be64dcd84c4a35a7bcdb (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue61
-rw-r--r--app/assets/javascripts/invite_members/constants.js5
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/fragments/job_artifacts.fragment.graphql13
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql (renamed from app/assets/javascripts/vue_shared/security_reports/queries/security_report_merge_request_download_paths.query.graphql)0
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/graphql/queries/security_report_pipeline_download_paths.query.graphql10
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql18
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue2
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/helpers/invite_members_helper.rb39
-rw-r--r--app/helpers/projects_helper.rb53
-rw-r--r--app/models/concerns/vulnerability_finding_helpers.rb33
-rw-r--r--app/models/concerns/vulnerability_finding_signature_helpers.rb28
-rw-r--r--app/models/user.rb10
-rw-r--r--app/models/users/banned_user.rb12
-rw-r--r--app/services/security/merge_reports_service.rb70
-rw-r--r--app/services/users/ban_service.rb22
-rw-r--r--app/services/users/banned_user_base_service.rb37
-rw-r--r--app/services/users/unban_service.rb15
-rw-r--r--app/views/groups/_invite_members_modal.html.haml7
-rw-r--r--app/views/projects/_invite_members_modal.html.haml7
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml2
-rw-r--r--config/feature_flags/experiment/member_areas_of_focus.yml8
-rw-r--r--config/initializers/0_log_deprecations.rb22
-rw-r--r--db/migrate/20210713211008_create_banned_users.rb20
-rw-r--r--db/schema_migrations/202107132110081
-rw-r--r--db/structure.sql12
-rw-r--r--doc/administration/integration/kroki.md4
-rw-r--r--doc/administration/integration/terminal.md4
-rw-r--r--doc/api/api_resources.md4
-rw-r--r--doc/api/custom_attributes.md4
-rw-r--r--doc/api/graphql/audit_report.md4
-rw-r--r--doc/api/graphql/getting_started.md4
-rw-r--r--doc/api/graphql/index.md4
-rw-r--r--doc/api/graphql/users_example.md4
-rw-r--r--doc/api/index.md4
-rw-r--r--doc/api/openapi/openapi_interactive.md4
-rw-r--r--doc/api/services.md4
-rw-r--r--doc/api/system_hooks.md4
-rw-r--r--doc/api/v3_to_v4.md4
-rw-r--r--doc/api/version.md4
-rw-r--r--doc/architecture/blueprints/consolidating_groups_and_projects/index.md155
-rw-r--r--doc/development/integrations/jenkins.md4
-rw-r--r--doc/development/integrations/jira_connect.md4
-rw-r--r--doc/development/service_ping/metrics_dictionary.md2
-rw-r--r--doc/integration/akismet.md4
-rw-r--r--doc/integration/auth0.md4
-rw-r--r--doc/integration/azure.md4
-rw-r--r--doc/integration/bitbucket.md4
-rw-r--r--doc/integration/cas.md4
-rw-r--r--doc/integration/datadog.md4
-rw-r--r--doc/integration/external-issue-tracker.md4
-rw-r--r--doc/integration/facebook.md4
-rw-r--r--doc/integration/github.md4
-rw-r--r--doc/integration/gitlab.md4
-rw-r--r--doc/integration/gmail_action_buttons_for_gitlab.md4
-rw-r--r--doc/integration/google.md4
-rw-r--r--doc/integration/index.md4
-rw-r--r--doc/integration/jenkins.md4
-rw-r--r--doc/integration/jenkins_deprecated.md4
-rw-r--r--doc/integration/jira/configure.md4
-rw-r--r--doc/integration/jira/connect-app.md4
-rw-r--r--doc/integration/jira/development_panel.md4
-rw-r--r--doc/integration/jira/dvcs.md4
-rw-r--r--doc/integration/jira/index.md4
-rw-r--r--doc/integration/jira/issues.md4
-rw-r--r--doc/integration/jira/jira_cloud_configuration.md4
-rw-r--r--doc/integration/jira/jira_server_configuration.md4
-rw-r--r--doc/integration/oauth2_generic.md4
-rw-r--r--doc/integration/omniauth.md4
-rw-r--r--doc/integration/openid_connect_provider.md4
-rw-r--r--doc/integration/recaptcha.md4
-rw-r--r--doc/integration/salesforce.md4
-rw-r--r--doc/integration/slash_commands.md4
-rw-r--r--doc/integration/trello_power_up.md4
-rw-r--r--doc/integration/twitter.md4
-rw-r--r--doc/user/admin_area/settings/project_integration_management.md4
-rw-r--r--doc/user/project/integrations/asana.md4
-rw-r--r--doc/user/project/integrations/bamboo.md4
-rw-r--r--doc/user/project/integrations/bugzilla.md4
-rw-r--r--doc/user/project/integrations/custom_issue_tracker.md4
-rw-r--r--doc/user/project/integrations/discord_notifications.md4
-rw-r--r--doc/user/project/integrations/emails_on_push.md4
-rw-r--r--doc/user/project/integrations/ewm.md4
-rw-r--r--doc/user/project/integrations/github.md4
-rw-r--r--doc/user/project/integrations/gitlab_slack_application.md4
-rw-r--r--doc/user/project/integrations/hangouts_chat.md4
-rw-r--r--doc/user/project/integrations/index.md4
-rw-r--r--doc/user/project/integrations/irker.md4
-rw-r--r--doc/user/project/integrations/mattermost.md4
-rw-r--r--doc/user/project/integrations/mattermost_slash_commands.md4
-rw-r--r--doc/user/project/integrations/microsoft_teams.md4
-rw-r--r--doc/user/project/integrations/mock_ci.md4
-rw-r--r--doc/user/project/integrations/overview.md4
-rw-r--r--doc/user/project/integrations/pivotal_tracker.md4
-rw-r--r--doc/user/project/integrations/redmine.md4
-rw-r--r--doc/user/project/integrations/servicenow.md4
-rw-r--r--doc/user/project/integrations/slack.md4
-rw-r--r--doc/user/project/integrations/slack_slash_commands.md4
-rw-r--r--doc/user/project/integrations/unify_circuit.md4
-rw-r--r--doc/user/project/integrations/webex_teams.md4
-rw-r--r--doc/user/project/integrations/webhooks.md4
-rw-r--r--doc/user/project/integrations/youtrack.md4
-rw-r--r--lib/gitlab/ci/parsers.rb4
-rw-r--r--lib/gitlab/ci/parsers/security/common.rb266
-rw-r--r--lib/gitlab/ci/parsers/security/concerns/deprecated_syntax.rb36
-rw-r--r--lib/gitlab/ci/parsers/security/sast.rb26
-rw-r--r--lib/gitlab/ci/parsers/security/secret_detection.rb27
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schema_validator.rb68
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/sast.json706
-rw-r--r--lib/gitlab/ci/parsers/security/validators/schemas/secret_detection.json729
-rw-r--r--lib/gitlab/ci/reports/security/aggregated_report.rb24
-rw-r--r--lib/gitlab/ci/reports/security/finding.rb151
-rw-r--r--lib/gitlab/ci/reports/security/finding_key.rb36
-rw-r--r--lib/gitlab/ci/reports/security/finding_signature.rb46
-rw-r--r--lib/gitlab/ci/reports/security/locations/base.rb41
-rw-r--r--lib/gitlab/ci/reports/security/locations/sast.rb33
-rw-r--r--lib/gitlab/ci/reports/security/locations/secret_detection.rb33
-rw-r--r--lib/gitlab/ci/reports/security/report.rb76
-rw-r--r--lib/gitlab/ci/reports/security/reports.rb42
-rw-r--r--lib/gitlab/deprecation_json_logger.rb9
-rw-r--r--locale/gitlab.pot24
-rw-r--r--spec/controllers/admin/users_controller_spec.rb13
-rw-r--r--spec/controllers/projects_controller_spec.rb27
-rw-r--r--spec/factories/ci/job_artifacts.rb70
-rw-r--r--spec/factories/ci/reports/security/aggregated_reports.rb14
-rw-r--r--spec/factories/ci/reports/security/finding_keys.rb18
-rw-r--r--spec/factories/ci/reports/security/findings.rb54
-rw-r--r--spec/factories/ci/reports/security/locations/sast.rb23
-rw-r--r--spec/factories/ci/reports/security/reports.rb28
-rw-r--r--spec/features/groups/members/manage_members_spec.rb54
-rw-r--r--spec/fixtures/security_reports/deprecated/gl-sast-report.json964
-rw-r--r--spec/fixtures/security_reports/feature-branch/gl-sast-report.json177
-rw-r--r--spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json5
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report-names.json168
-rw-r--r--spec/fixtures/security_reports/master/gl-common-scanning-report.json160
-rw-r--r--spec/fixtures/security_reports/master/gl-sast-missing-scanner.json802
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js136
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js2
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js2
-rw-r--r--spec/helpers/invite_members_helper_spec.rb50
-rw-r--r--spec/helpers/projects_helper_spec.rb36
-rw-r--r--spec/initializers/0_log_deprecations_spec.rb74
-rw-r--r--spec/lib/banzai/filter/references/alert_reference_filter_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb350
-rw-r--r--spec/lib/gitlab/ci/parsers/security/sast_spec.rb57
-rw-r--r--spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_key_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb59
-rw-r--r--spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/reports/security/report_spec.rb224
-rw-r--r--spec/lib/gitlab/ci/reports/security/reports_spec.rb113
-rw-r--r--spec/models/concerns/vulnerability_finding_signature_helpers_spec.rb32
-rw-r--r--spec/models/user_spec.rb37
-rw-r--r--spec/models/users/banned_user_spec.rb21
-rw-r--r--spec/services/users/ban_service_spec.rb65
-rw-r--r--spec/services/users/banned_user_base_service_spec.rb14
-rw-r--r--spec/services/users/unban_service_spec.rb75
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb12
-rw-r--r--spec/support/shared_examples/lib/gitlab/ci/reports/security/locations/locations_shared_examples.rb68
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
diff --git a/Gemfile b/Gemfile
index 9b1076baf87..d4126b7614f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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') }
&nbsp;
= 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?
&nbsp;
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