Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-12 15:11:42 +00:00
parent ee772e0c77
commit 1c8734ca5c
84 changed files with 1307 additions and 669 deletions

View File

@ -182,28 +182,6 @@ Layout/HashAlignment:
- 'ee/spec/support/shared_examples/status_page/publish_shared_examples.rb'
- 'ee/spec/support/shared_examples/status_page/reference_links_examples.rb'
- 'ee/spec/workers/scan_security_report_secrets_worker_spec.rb'
- 'lib/api/issue_links.rb'
- 'lib/api/issues.rb'
- 'lib/api/labels.rb'
- 'lib/api/maven_packages.rb'
- 'lib/api/members.rb'
- 'lib/api/merge_requests.rb'
- 'lib/api/metrics/dashboard/annotations.rb'
- 'lib/api/metrics/user_starred_dashboards.rb'
- 'lib/api/milestone_responses.rb'
- 'lib/api/notes.rb'
- 'lib/api/pages_domains.rb'
- 'lib/api/project_packages.rb'
- 'lib/api/project_templates.rb'
- 'lib/api/projects.rb'
- 'lib/api/protected_branches.rb'
- 'lib/api/releases.rb'
- 'lib/api/rubygem_packages.rb'
- 'lib/api/sidekiq_metrics.rb'
- 'lib/api/users.rb'
- 'lib/backup/gitaly_backup.rb'
- 'lib/banzai/filter/references/abstract_reference_filter.rb'
- 'lib/banzai/reference_redactor.rb'
- 'lib/gitlab/abuse.rb'
- 'lib/gitlab/access.rb'
- 'lib/gitlab/application_rate_limiter.rb'

View File

@ -118,6 +118,7 @@ Style/FormatString:
- 'app/models/integrations/mattermost.rb'
- 'app/models/integrations/pipelines_email.rb'
- 'app/models/integrations/pivotaltracker.rb'
- 'app/models/integrations/pumble.rb'
- 'app/models/integrations/pushover.rb'
- 'app/models/integrations/redmine.rb'
- 'app/models/integrations/unify_circuit.rb'

View File

@ -104,15 +104,16 @@ const getAttrsFactory = ({ attributeTransformer, markdown }) =>
function getAttrs(proseMirrorNodeSpec, hastNode, hastParents) {
const { getAttrs: specGetAttrs } = proseMirrorNodeSpec;
const attributes = {
...createSourceMapAttributes(hastNode, markdown),
...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, markdown) : {}),
};
const { transform } = attributeTransformer;
return mapValues(attributes, (value, key) =>
attributeTransformer.attributes.includes(key)
? attributeTransformer.transform(value, key)
: value,
);
return {
...createSourceMapAttributes(hastNode, markdown),
...mapValues(attributes, (attributeValue, attributeName) =>
transform(attributeName, attributeValue, hastNode),
),
};
};
/**

View File

@ -1,4 +1,5 @@
import { render } from '~/lib/gfm';
import { isValidAttribute } from '~/lib/dompurify';
import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter';
const wrappableTags = ['img', 'br', 'code', 'i', 'em', 'b', 'strong', 'a', 'strike', 's', 'del'];
@ -184,28 +185,34 @@ const factorySpecs = {
},
};
const resolveUrl = (url) => {
try {
return new URL(url, window.location.origin).toString();
} catch {
const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url'];
const sanitizeAttribute = (attributeName, attributeValue, hastNode) => {
if (!attributeValue || SANITIZE_ALLOWLIST.includes(attributeName)) {
return attributeValue;
}
/**
* This is a workaround to validate the value of the canonicalSrc
* attribute using DOMPurify without passing the attribute name. canonicalSrc
* is not an allowed attribute in DOMPurify therefore the library will remove
* it regardless of its value.
*
* We want to preserve canonicalSrc, and we also want to make sure that its
* value is sanitized.
*/
const validateAttributeAs = attributeName === 'canonicalSrc' ? 'src' : attributeName;
if (!isValidAttribute(hastNode.tagName, validateAttributeAs, attributeValue)) {
return null;
}
return attributeValue;
};
const attributeTransformer = {
attributes: ['href', 'src'],
transform: (url) => {
if (!url) {
return url;
}
/**
* Resolves a URL if provided. The URL is not resolved against
* the client origin initially to protect the URL protocol
* when it is available, for example, we want to preserve
* mailto and application-specific protocols
*/
return resolveUrl(url);
transform: (attributeName, attributeValue, hastNode) => {
return sanitizeAttribute(attributeName, attributeValue, hastNode);
},
};

View File

@ -93,3 +93,5 @@ addHook('afterSanitizeAttributes', (node) => {
});
export const sanitize = (val, config) => dompurifySanitize(val, { ...defaultConfig, ...config });
export { isValidAttribute } from 'dompurify';

View File

@ -3,12 +3,12 @@
module AcceptsPendingInvitations
extend ActiveSupport::Concern
def accept_pending_invitations
return unless resource.active_for_authentication?
def accept_pending_invitations(user: resource)
return unless user.active_for_authentication?
if resource.pending_invitations.load.any?
resource.accept_pending_invitations!
clear_stored_location_for_resource
if user.pending_invitations.load.any?
user.accept_pending_invitations!
clear_stored_location_for(user: user)
after_pending_invitations_hook
end
end
@ -17,8 +17,8 @@ module AcceptsPendingInvitations
# no-op
end
def clear_stored_location_for_resource
session_key = stored_location_key_for(resource)
def clear_stored_location_for(user:)
session_key = stored_location_key_for(user)
session.delete(session_key)
end

View File

@ -6,6 +6,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
include AuthHelper
include InitializesCurrentUserMode
include KnownSignIn
include AcceptsPendingInvitations
after_action :verify_known_sign_in
@ -159,6 +160,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def sign_in_user_flow(auth_user_class)
auth_user = build_auth_user(auth_user_class)
new_user = auth_user.new?
user = auth_user.find_and_update!
if auth_user.valid_sign_in?
@ -178,6 +180,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
end
accept_pending_invitations(user: user) if new_user
store_after_sign_up_path_for_user if intent_to_register?
sign_in_and_redirect(user, event: :authentication)
end

View File

@ -21,7 +21,7 @@ class Integration < ApplicationRecord
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
# TODO Shimo is temporary disabled on group and instance-levels.

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
module Integrations
class Pumble < BaseChatNotification
def title
'Pumble'
end
def description
s_("PumbleIntegration|Send notifications about project events to Pumble.")
end
def self.to_param
'pumble'
end
def help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/pumble'),
target: '_blank',
rel: 'noopener noreferrer'
)
# rubocop:disable Layout/LineLength
s_("PumbleIntegration|Send notifications about project events to Pumble. %{docs_link}") % { docs_link: docs_link.html_safe }
# rubocop:enable Layout/LineLength
end
def default_channel_placeholder
end
def self.supported_events
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "https://api.pumble.com/workspaces/x/...", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{
type: 'select',
name: 'branches_to_be_notified',
title: s_('Integrations|Branches for which notifications are to be sent'),
choices: self.class.branch_choices
}
]
end
private
def notify(message, opts)
header = { 'Content-Type' => 'application/json' }
response = Gitlab::HTTP.post(webhook, headers: header, body: { text: message.summary }.to_json)
response if response.success?
end
end
end

View File

@ -216,6 +216,7 @@ class Project < ApplicationRecord
has_one :pipelines_email_integration, class_name: 'Integrations::PipelinesEmail'
has_one :pivotaltracker_integration, class_name: 'Integrations::Pivotaltracker'
has_one :prometheus_integration, class_name: 'Integrations::Prometheus', inverse_of: :project
has_one :pumble_integration, class_name: 'Integrations::Pumble'
has_one :pushover_integration, class_name: 'Integrations::Pushover'
has_one :redmine_integration, class_name: 'Integrations::Redmine'
has_one :shimo_integration, class_name: 'Integrations::Shimo'

View File

@ -7,12 +7,13 @@
- return unless branches.any?
.card
.card-header
= render Pajamas::CardComponent.new(card_options: {class: 'gl-mb-5'}, body_options: {class: 'gl-py-0'}, footer_options: {class: 'gl-text-center'}) do |c|
- c.header do
= panel_title
%ul.content-list.all-branches.qa-all-branches
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
- c.body do
%ul.content-list.all-branches.qa-all-branches
- branches.first(overview_max_branches).each do |branch|
= render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch), commit_status: @branch_pipeline_statuses[branch.name], show_commit_status: @branch_pipeline_statuses.any?
- if branches.size > overview_max_branches
.card-footer.text-center
- c.footer do
= link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state }

View File

@ -7,7 +7,7 @@
- else
= sprite_icon('star-o', css_class: 'icon')
%span= s_('ProjectOverview|Star')
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm star-count count' do
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
= @project.star_count
- else
@ -15,5 +15,5 @@
= link_to new_user_session_path, class: 'gl-button btn btn-default btn-sm has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
= sprite_icon('star-o', css_class: 'icon')
%span= s_('ProjectOverview|Star')
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm star-count count' do
= link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
= @project.star_count

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362179
milestone: '15.1'
type: development
group: group::static analysis
default_enabled: false
default_enabled: true

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: counts.groups_inheriting_pumble_active
description: Count of active groups inheriting integrations for Pumble
product_section: dev
product_stage: ecosystem
product_group: integrations
product_category: integrations
value_type: number
status: active
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "15.3"

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: counts.groups_pumble_active
description: Count of groups with active integrations for Pumble
product_section: dev
product_stage: ecosystem
product_group: integrations
product_category: integrations
value_type: number
status: active
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "15.3"

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: counts.instances_pumble_active
description: Count of active instance-level integrations for Pumble
product_section: dev
product_stage: ecosystem
product_group: integrations
product_category: integrations
value_type: number
status: active
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "15.3"

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: counts.projects_inheriting_pumble_active
description: Count of active projects inheriting integrations for Pumble
product_section: dev
product_stage: ecosystem
product_group: integrations
product_category: integrations
value_type: number
status: active
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "15.3"

View File

@ -0,0 +1,21 @@
---
data_category: optional
key_path: counts.projects_pumble_active
description: Count of projects with active integrations for Pumble
product_section: dev
product_stage: ecosystem
product_group: integrations
product_category: integrations
value_type: number
status: active
time_frame: all
data_source: database
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
performance_indicator_type: []
milestone: "15.3"

View File

@ -38,6 +38,7 @@ classes:
- Integrations::PipelinesEmail
- Integrations::Pivotaltracker
- Integrations::Prometheus
- Integrations::Pumble
- Integrations::Pushover
- Integrations::Redmine
- Integrations::Shimo

View File

@ -46,6 +46,7 @@ required number of seconds.
"user",
"pipeline",
"builds",
"total_builds_count",
"namespace"
],
"properties" : {
@ -61,7 +62,9 @@ required number of seconds.
"properties": {
"id": { "type": "integer" },
"path": { "type": "string" },
"created_at": { "type": ["string", "null"], "format": "date-time" }
"created_at": { "type": ["string", "null"], "format": "date-time" },
"shared_runners_enabled": { "type": "boolean" },
"group_runners_enabled": { "type": "boolean" }
}
},
"user": {
@ -121,6 +124,7 @@ required number of seconds.
}
}
},
"total_builds_count": { "type": "integer" },
"namespace": {
"type": "object",
"required": [

View File

@ -1077,6 +1077,9 @@ License.current.trial?
# License ID for lookup on CustomersDot
License.current.license_id
# License data in Base64-encoded ASCII format
License.current.data
```
### Check if a project feature is available on the instance

View File

@ -20380,6 +20380,7 @@ State of a Sentry error.
| <a id="servicetypepipelines_email_service"></a>`PIPELINES_EMAIL_SERVICE` | PipelinesEmailService type. |
| <a id="servicetypepivotaltracker_service"></a>`PIVOTALTRACKER_SERVICE` | PivotaltrackerService type. |
| <a id="servicetypeprometheus_service"></a>`PROMETHEUS_SERVICE` | PrometheusService type. |
| <a id="servicetypepumble_service"></a>`PUMBLE_SERVICE` | PumbleService type. |
| <a id="servicetypepushover_service"></a>`PUSHOVER_SERVICE` | PushoverService type. |
| <a id="servicetyperedmine_service"></a>`REDMINE_SERVICE` | RedmineService type. |
| <a id="servicetypeshimo_service"></a>`SHIMO_SERVICE` | ShimoService type. |
@ -20719,8 +20720,6 @@ Vulnerability sort values.
| ----- | ----------- |
| <a id="vulnerabilitysortdetected_asc"></a>`detected_asc` | Detection timestamp in ascending order. |
| <a id="vulnerabilitysortdetected_desc"></a>`detected_desc` | Detection timestamp in descending order. |
| <a id="vulnerabilitysortreport_type_asc"></a>`report_type_asc` | Report Type in ascending order. |
| <a id="vulnerabilitysortreport_type_desc"></a>`report_type_desc` | Report Type in descending order. |
| <a id="vulnerabilitysortseverity_asc"></a>`severity_asc` | Severity in ascending order. |
| <a id="vulnerabilitysortseverity_desc"></a>`severity_desc` | Severity in descending order. |

View File

@ -383,6 +383,51 @@ Get Unify Circuit integration settings for a project.
GET /projects/:id/integrations/unify-circuit
```
## Pumble
Pumble chat tool.
### Create/Edit Pumble integration
Set Pumble integration for a project.
```plaintext
PUT /projects/:id/integrations/pumble
```
Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `webhook` | string | true | The Pumble webhook. For example, `https://api.pumble.com/workspaces/x/...`. |
| `branches_to_be_notified` | string | false | Branches to send notifications for. Valid options are `all`, `default`, `protected`, and `default_and_protected`. The default is `default`. |
| `confidential_issues_events` | boolean | false | Enable notifications for confidential issue events. |
| `confidential_note_events` | boolean | false | Enable notifications for confidential note events. |
| `issues_events` | boolean | false | Enable notifications for issue events. |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events. |
| `note_events` | boolean | false | Enable notifications for note events. |
| `notify_only_broken_pipelines` | boolean | false | Send notifications for broken pipelines. |
| `pipeline_events` | boolean | false | Enable notifications for pipeline events. |
| `push_events` | boolean | false | Enable notifications for push events. |
| `tag_push_events` | boolean | false | Enable notifications for tag push events. |
| `wiki_page_events` | boolean | false | Enable notifications for wiki page events. |
### Disable Pumble integration
Disable the Pumble integration for a project. Integration settings are preserved.
```plaintext
DELETE /projects/:id/integrations/pumble
```
### Get Pumble integration settings
Get Pumble integration settings for a project.
```plaintext
GET /projects/:id/integrations/pumble
```
## Webex Teams
Webex Teams collaboration tool.

View File

@ -306,9 +306,19 @@ We also need to build a proof of concept for removing data on the PostgreSQL
side (using foreign keys with `ON DELETE CASCADE`) and removing data through
Rails associations, as this might be an important area of uncertainty.
We need to [better understand](https://gitlab.com/gitlab-org/gitlab/-/issues/360148)
how unique constraints we are currently using will perform when using the
partitioned schema.
We [learned](https://gitlab.com/gitlab-org/gitlab/-/issues/360148) that `PostgreSQL`
does not allow to create a single index (unique or otherwise) across all partitions of a table.
One solution to solve this problem is to embed the partitioning key inside the uniqueness constraint.
This might mean prepending the partition ID in a hexadecimal format before the token itself and storing
the concatenated string in a database. To do that we would need to reserve an appropriate number of
leading bytes in a token to accommodate for the maximum number of partitions we may have in the future.
It seems that reserving four characters, what would translate into 16-bits number in base-16,
might be sufficient. The maximum number we can encode this way would be FFFF, what is 65535 in decimal.
This would provide a unique constraint per-partition which
is sufficient for global uniqueness.
We have also designed a query analyzer that makes it possible to detect direct
usage of zero partitions, legacy tables that have been attached as first

View File

@ -269,9 +269,15 @@ Arguments:
#### Ordinary Redis counters
Example of implementation:
Example of implementation: [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb), using Redis methods [`INCR`](https://redis.io/commands/incr) and [`GET`](https://redis.io/commands/get).
Using Redis methods [`INCR`](https://redis.io/commands/incr/), [`GET`](https://redis.io/commands/get/), and [`Gitlab::UsageDataCounters::WikiPageCounter`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters/wiki_page_counter.rb)
Events are handled by counter classes in the `Gitlab::UsageDataCounters` namespace, inheriting from `BaseCounter`, that are either:
1. Listed in [`Gitlab::UsageDataCounters::COUNTERS`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/usage_data_counters.rb#L5) to be then included in `Gitlab::UsageData`.
1. Specified in the metric definition using the `RedisMetric` instrumentation class as a `counter_class` option to be picked up using the [metric instrumentation](metrics_instrumentation.md) framework. Refer to the [Redis metrics](metrics_instrumentation.md#redis-metrics) documentation for an example implementation.
Inheriting classes are expected to override `KNOWN_EVENTS` and `PREFIX` constants to build event names and associated metrics. For example, for prefix `issues` and events array `%w[create, update, delete]`, three metrics will be added to the Service Ping payload: `counts.issues_create`, `counts.issues_update` and `counts.issues_delete`.
##### `UsageData` API

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -4,103 +4,65 @@ group: Optimize
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
---
# Productivity Analytics **(PREMIUM)**
# Productivity analytics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12079) in GitLab 12.3.
You can use productivity analytics to identify:
Track development velocity with Productivity Analytics.
- Your development velocity based on how long it takes for a merge request to merge.
- The most time consuming merge requests and potential causes.
- Authors, labels, or milestones with the longest time to merge, or most changes.
For many companies, the development cycle is a black box and getting an estimate of how
long, on average, it takes to deliver features is an enormous endeavor.
Use productivity analytics to view the following merge request statistics for your groups:
While [Value Stream Analytics](../analytics/value_stream_analytics.md) focuses on the entire
Software Development Life Cycle (SDLC) process, Productivity Analytics provides a way for Engineering Management to drill down in a systematic way to uncover patterns and causes for success or failure at an individual, project, or group level.
- Amount of time between merge request creation and merge.
- Amount of time between commits, comments, and merge.
- Complexity of changes, like number of lines of code per commit and number of files.
Productivity can slow down for many reasons ranging from degrading codebase to quickly growing teams. To investigate, department or team leaders can start by visualizing the time it takes for merge requests to be merged.
To view merge request data for projects, use [Merge request analytics](../analytics/merge_request_analytics.md).
## Visualizations and metrics
## View productivity analytics
With Productivity Analytics, GitLab users can:
Prerequisite:
- Visualize typical merge request (MR) lifetime and statistics. A histogram shows the distribution of the time elapsed between creating and merging merge requests.
- Drill down into the most time consuming merge requests, select outliers, and filter subsequent charts to investigate potential causes.
- Filter by group, project, author, label, milestone, or a specific date range. For example, filter down to the merge requests of a specific author in a group or project during a milestone or specific date range.
- Measure velocity over time. To observe progress, visualize the trends of each metric from the charts over time. Zoom in on a particular date range if you notice outliers.
- You must have at least the Reporter role for the group.
## Metrics charts
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Analytics > Productivity**.
1. Optional. Filter results:
1. Select a project from the dropdown list.
1. To filter results by author, milestone, or label,
select **Filter results...** and enter a value.
1. To adjust the date range:
- In the **From** field, select a start date.
- In the **To** field, select an end date.
To access the charts, navigate to a group's sidebar and select **Analytics > Productivity Analytics**.
Metrics and visualizations of **merged** merge requests are available on a project or group level.
## View time metrics for merge requests
### Time to merge
Use the following charts in productivity analytics to view the velocity of your merge requests:
The **Time to merge** histogram shows the number of merge requests and the number
of days it took to merge after creation. Select a column to filter subsequent charts.
- **Time to merge**: number of days it took for a
merge requests to merge after they were created.
- **Trendline**: number of merge requests that were merged in a specific time period.
![Metrics for number of days merge requests per number of days](img/productivity_analytics_time_to_merge_v14_4.png)
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Analytics > Productivity**.
### Trendline
To filter time metrics:
The **Trendline** scatterplot shows all merge requests on a certain date,
and the days it took to complete the action and a 30 day rolling median. Select the dropdown to view:
1. To filter the **Trendline** chart, in the **Time to merge** chart, select a column.
1. To view a specific merge request, below the charts, select a merge request from the **List**.
- Time from first commit to first comment.
- Time from first comment until last commit.
- Time from last commit to merge.
- Number of commits per merge request.
- Number of lines of code (LOC) per commit.
- Number of files touched.
## View commit statistics
![Metrics for amount of merge requests merged on a certain date](img/productivity_analytics_trendline_v14_4.png)
To view commit statistics for your group:
### Commits and merge request size
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Analytics > Productivity**.
1. Under the **Trendline** scatterplot, view the commit statistics:
- The left histogram shows the number of hours between commits, comments, and merges.
- The right histogram shows the number of commits and changes per merge request.
Under the **Trendline** scatterplot, the left-side histogram shows
the time taken (in hours) between commits and comments until the merge
request is merged. Select the dropdown to view:
To filter commit statistics:
- Time from first commit to first comment.
- Time from first comment until last commit.
- Time from last commit to merge.
The right-side histogram shows the size or complexity of a merge request.
Select the dropdown to view:
- Number of commits per merge request.
- Number of lines of code (LOC) per commit.
- Number of files touched.
![Metrics for amount of commits and complexity of changes per merge request.](img/product_analytics_commits_per_mr_v14_4.png)
### Merge request list
The **List** table shows a list of merge requests with their respective time duration metrics.
Sort metrics by:
- Time from first commit to first comment.
- Time from first comment until last commit.
- Time from last commit to merge.
Filter metrics by:
- Number of commits per merge request.
- Number of lines of code per commit.
- Number of files touched.
## Filter by date range
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13188) in GitLab 12.4.
You can filter analytics based on a date range. To filter results:
1. Select a group.
1. Optional. Select a project.
1. Select a date range by using the available date pickers.
## Permissions
The **Productivity Analytics** dashboard can be accessed only:
- On [GitLab Premium](https://about.gitlab.com/pricing/) and above.
- By users with at least the Reporter role.
1. To view different types of commit data, select the dropdown list next to each histogram.
1. To view a specific merge request, below the charts, select a merge request from the **List**.

View File

@ -149,7 +149,7 @@ base address for Docker images. You can override this for most scanners by setti
The [Container Scanning](container_scanning/index.md) analyzer is an exception, and it
does not use the `SECURE_ANALYZERS_PREFIX` variable. To override its Docker image, see
the instructions for
the instructions for
[Running container scanning in an offline environment](container_scanning/index.md#running-container-scanning-in-an-offline-environment).
## Default behavior of GitLab security scanning tools
@ -390,8 +390,10 @@ Validation depends on the schema version declared in the security report artifac
- If your security report specifies a supported schema version, GitLab uses this version to validate.
- If your security report uses a deprecated version, GitLab attempts validation against that version and adds a deprecation warning to the validation result.
- If your security report uses a version that is not supported, GitLab attempts to validate it against the latest schema version available in GitLab.
- If your security report does not specify a schema version, GitLab attempts to validate it against the lastest schema version available in GitLab. Since the `version` property is required, validation always fails in this case, but other validation errors may also be present.
- If your security report uses a supported MAJOR-MINOR version of the report schema but the PATCH version doesn't match any vendored versions, GitLab attempts to validate it against latest vendored PATCH version of the schema.
- Example: security report uses version 14.1.1 but the latest vendored version is 14.1.0. GitLab would validate against schema version 14.1.0.
- If your security report uses a version that is not supported, GitLab attempts to validate it against the latest schema version available in your installation but doesn't ingest the report.
- If your security report does not specify a schema version, GitLab attempts to validate it against the latest schema version available in GitLab. Because the `version` property is required, validation always fails in this case, but other validation errors may also be present.
You can always find supported and deprecated schema versions in the [source code](https://gitlab.com/gitlab-org/gitlab/blob/master/lib/gitlab/ci/parsers/security/validators/schema_validator.rb).

View File

@ -73,6 +73,7 @@ You can configure the following integrations.
| [Pipelines emails](pipeline_status_emails.md) | Send the pipeline status to a list of recipients by email. | **{dotted-circle}** No |
| [Pivotal Tracker](pivotal_tracker.md) | Add commit messages as comments to Pivotal Tracker stories. | **{dotted-circle}** No |
| [Prometheus](prometheus.md) | Monitor application metrics. | **{dotted-circle}** No |
| [Pumble](pumble.md) | Send event notifications to a Pumble channel. | **{dotted-circle}** No |
| Pushover | Get real-time notifications on your device. | **{dotted-circle}** No |
| [Redmine](redmine.md) | Use Redmine as the issue tracker. | **{dotted-circle}** No |
| [Slack application](gitlab_slack_application.md) | Use Slack's official GitLab application. | **{dotted-circle}** No |

View File

@ -0,0 +1,39 @@
---
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
---
# Pumble **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93623) in GitLab 15.3.
You can configure GitLab to send notifications to a Pumble channel:
1. Create a webhook for the channel.
1. Add the webhook to GitLab.
## Create a webhook for your Pumble channel
1. Follow the steps in [Incoming Webhooks for Pumble](https://pumble.com/help/integrations/custom-apps/incoming-webhooks-for-pumble/) in the Pumble documentation.
1. Copy the webhook URL.
## Configure settings in GitLab
After you have a webhook URL for your Pumble channel, configure GitLab to send
notifications:
1. To enable the integration for your group or project:
1. In your group or project, on the left sidebar, select **Settings > Integrations**.
1. To enable the integration for your instance:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > Integrations**.
1. Select the **Pumble** integration.
1. Ensure that the **Active** toggle is enabled.
1. Select the checkboxes corresponding to the GitLab events you want to receive in Pumble.
1. Paste the **Webhook** URL for the Pumble channel.
1. Configure the remaining options.
1. Optional. To test the integration, select **Test settings**.
1. Select **Save changes**.
The Pumble channel begins to receive all applicable GitLab events.

View File

@ -127,7 +127,7 @@ users were not explicitly listed in the approval rules.
### Group approvers
You can add a group of users as approvers, but those users count as approvers only if
they have direct membership to the group. Group approvers are
they have **direct membership** to the group. Inherited members do not count. Group approvers are
restricted to only groups [with share access to the project](../../members/share_project_with_groups.md).
A user's membership in an approvers group affects their individual ability to

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -678,6 +678,15 @@ module API
desc: 'Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'
}
],
'pumble' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Pumble chat webhook. For example, https://api.pumble.com/workspaces/x/...'
},
chat_notification_events
].flatten,
'pushover' => [
{
required: true,

View File

@ -37,7 +37,7 @@ module API
requires :target_project_id, type: String, desc: 'The ID of the target project'
requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue'
optional :link_type, type: String, values: IssueLink.link_types.keys,
desc: 'The type of the relation'
desc: 'The type of the relation'
end
# rubocop: disable CodeReuse/ActiveRecord
post ':id/issues/:issue_iid/links' do

View File

@ -16,7 +16,7 @@ module API
optional :labels, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, desc: 'Comma-separated list of label names'
optional :milestone, type: String, desc: 'Milestone title'
optional :milestone_id, types: String, values: %w[Any None Upcoming Started],
desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")'
desc: 'Return issues assigned to milestones without the specified timebox value ("Any", "None", "Upcoming" or "Started")'
mutually_exclusive :milestone_id, :milestone
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
@ -27,8 +27,8 @@ module API
optional :assignee_id, type: Integer, desc: 'Return issues which are not assigned to the user with the given ID'
optional :assignee_username, type: Array[String], check_assignees_count: true,
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
desc: 'Return issues which are not assigned to the user with the given username'
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
desc: 'Return issues which are not assigned to the user with the given username'
mutually_exclusive :assignee_id, :assignee_username
use :negatable_issue_filter_params_ee
@ -40,7 +40,7 @@ module API
# 'milestone_id' only accepts wildcard values 'Any', 'None', 'Upcoming', 'Started'
# the param has '_id' in the name to keep consistency (ex. assignee_id accepts id and wildcard values).
optional :milestone_id, types: String, values: %w[Any None Upcoming Started],
desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")'
desc: 'Return issues assigned to milestones with the specified timebox value ("Any", "None", "Upcoming" or "Started")'
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IID array of issues'
optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these'
optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma'
@ -51,10 +51,10 @@ module API
mutually_exclusive :author_id, :author_username
optional :assignee_id, types: [Integer, String], integer_none_any: true,
desc: 'Return issues which are assigned to the user with the given ID'
desc: 'Return issues which are assigned to the user with the given ID'
optional :assignee_username, type: Array[String], check_assignees_count: true,
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
desc: 'Return issues which are assigned to the user with the given username'
coerce_with: Validations::Validators::CheckAssigneesCount.coerce,
desc: 'Return issues which are assigned to the user with the given username'
mutually_exclusive :assignee_id, :assignee_username
optional :created_after, type: DateTime, desc: 'Return issues created after the specified time'
@ -77,13 +77,13 @@ module API
params :issues_params do
optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false
optional :state, type: String, values: %w[opened closed all], default: 'all',
desc: 'Return opened, closed, or all issues'
desc: 'Return opened, closed, or all issues'
optional :order_by, type: String, values: Helpers::IssuesHelpers.sort_options, default: 'created_at',
desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.'
desc: 'Return issues ordered by `created_at`, `due_date`, `label_priority`, `milestone_due`, `popularity`, `priority`, `relative_position`, `title`, or `updated_at` fields.'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return issues sorted in `asc` or `desc` order.'
desc: 'Return issues sorted in `asc` or `desc` order.'
optional :due_date, type: String, values: %w[0 any today tomorrow overdue week month next_month_and_previous_two_weeks] << '',
desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`'
desc: 'Return issues that have no due date (`0`), or whose due date is this week, this month, between two weeks ago and next month, or which are overdue. Accepts: `overdue`, `week`, `month`, `next_month_and_previous_two_weeks`, `0`'
optional :issue_type, type: String, values: WorkItems::Type.allowed_types_for_issues, desc: "The type of the issue. Accepts: #{WorkItems::Type.allowed_types_for_issues.join(', ')}"
use :issues_stats_params

View File

@ -23,11 +23,11 @@ module API
end
params do
optional :with_counts, type: Boolean, default: false,
desc: 'Include issue and merge request counts'
desc: 'Include issue and merge request counts'
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
desc: 'Include ancestor groups'
optional :search, type: String,
desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6'
desc: 'Keyword to filter labels by. This feature was added in GitLab 13.6'
use :pagination
end
get ':id/labels' do
@ -40,7 +40,7 @@ module API
end
params do
optional :include_ancestor_groups, type: Boolean, default: true,
desc: 'Include ancestor groups'
desc: 'Include ancestor groups'
end
get ':id/labels/:name' do
get_label(user_project, Entities::ProjectLabel, declared_params)

View File

@ -283,12 +283,12 @@ module API
''
else
file_params = {
file: params[:file],
size: params['file.size'],
file: params[:file],
size: params['file.size'],
file_name: file_name,
file_type: params['file.type'],
file_sha1: params['file.sha1'],
file_md5: params['file.md5']
file_md5: params['file.md5']
}
::Packages::CreatePackageFileService.new(package, file_params.merge(build: current_authenticated_job)).execute

View File

@ -156,9 +156,9 @@ module API
params do
requires :user_id, type: Integer, desc: 'The user ID of the member'
optional :skip_subresources, type: Boolean, default: false,
desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped'
desc: 'Flag indicating if the deletion of direct memberships of the removed member in subgroups and projects should be skipped'
optional :unassign_issuables, type: Boolean, default: false,
desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
desc: 'Flag indicating if the removed member should be unassigned from any issues or merge requests within given group or project'
end
# rubocop: disable CodeReuse/ActiveRecord
delete ":id/members/:user_id", feature_category: feature_category do

View File

@ -159,7 +159,7 @@ module API
params do
use :merge_requests_params
optional :non_archived, type: Boolean, desc: 'Return merge requests from non archived projects',
default: true
default: true
end
get ":id/merge_requests", feature_category: :code_review, urgency: :low do
validate_anonymous_search_access! if declared_params[:search].present?

View File

@ -20,11 +20,11 @@ module API
resource annotations_source[:resource] do
params do
requires :starting_at, type: DateTime,
desc: 'Date time indicating starting moment to which the annotation relates.'
desc: 'Date time indicating starting moment to which the annotation relates.'
optional :ending_at, type: DateTime,
desc: 'Date time indicating ending moment to which the annotation relates.'
desc: 'Date time indicating ending moment to which the annotation relates.'
requires :dashboard_path, type: String, coerce_with: -> (val) { CGI.unescape(val) },
desc: 'The path to a file defining the dashboard on which the annotation should be added'
desc: 'The path to a file defining the dashboard on which the annotation should be added'
requires :description, type: String, desc: 'The description of the annotation'
end

View File

@ -13,7 +13,7 @@ module API
params do
requires :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
desc: 'Url encoded path to a file defining the dashboard to which the star should be added'
desc: 'Url encoded path to a file defining the dashboard to which the star should be added'
end
post ':id/metrics/user_starred_dashboards' do
@ -30,7 +30,7 @@ module API
params do
optional :dashboard_path, type: String, allow_blank: false, coerce_with: ->(val) { CGI.unescape(val) },
desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
desc: 'Url encoded path to a file defining the dashboard from which the star should be removed'
end
delete ':id/metrics/user_starred_dashboards' do

View File

@ -14,12 +14,12 @@ module API
params :list_params do
optional :state, type: String, values: %w[active closed all], default: 'all',
desc: 'Return "active", "closed", or "all" milestones'
desc: 'Return "active", "closed", or "all" milestones'
optional :iids, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'The IIDs of the milestones'
optional :title, type: String, desc: 'The title of the milestones'
optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
optional :include_parent_milestones, type: Grape::API::Boolean, default: false,
desc: 'Include group milestones from parent and its ancestors'
desc: 'Include group milestones from parent and its ancestors'
use :pagination
end
@ -27,7 +27,7 @@ module API
requires :milestone_id, type: Integer, desc: 'The milestone ID number'
optional :title, type: String, desc: 'The title of the milestone'
optional :state_event, type: String, values: %w[close activate],
desc: 'The state event of the milestone '
desc: 'The state event of the milestone '
use :optional_params
at_least_one_of :title, :description, :start_date, :due_date, :state_event
end

View File

@ -30,7 +30,7 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return notes sorted in `asc` or `desc` order.'
optional :activity_filter, type: String, values: UserPreference::NOTES_FILTERS.stringify_keys.keys, default: 'all_notes',
desc: 'The type of notables which are returned.'
desc: 'The type of notables which are returned.'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord

View File

@ -97,7 +97,7 @@ module API
optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
optional :auto_ssl_enabled, allow_blank: false, type: Boolean, default: false,
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
all_or_none_of :user_provided_certificate, :user_provided_key
end
@ -123,7 +123,7 @@ module API
optional :certificate, types: [File, String], desc: 'The certificate', as: :user_provided_certificate
optional :key, types: [File, String], desc: 'The key', as: :user_provided_key
optional :auto_ssl_enabled, allow_blank: true, type: Boolean,
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
desc: "Enables automatic generation of SSL certificates issued by Let's Encrypt for custom domains."
# rubocop:enable Scalability/FileUploads
end
put ":id/pages/domains/:domain", requirements: PAGES_DOMAINS_ENDPOINT_REQUIREMENTS do

View File

@ -32,9 +32,9 @@ module API
optional :package_name, type: String,
desc: 'Return packages with this name'
optional :include_versionless, type: Boolean,
desc: 'Returns packages without a version'
desc: 'Returns packages without a version'
optional :status, type: String, values: Packages::Package.statuses.keys,
desc: 'Return packages with specified status'
desc: 'Return packages with specified status'
end
get ':id/packages' do
packages = ::Packages::PackagesFinder.new(

View File

@ -37,7 +37,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the template'
optional :source_template_project_id, type: Integer,
desc: 'The project id where a given template is being stored. This is useful when multiple templates from different projects have the same name'
desc: 'The project id where a given template is being stored. This is useful when multiple templates from different projects have the same name'
optional :project, type: String, desc: 'The project name to use when expanding placeholders in the template. Only affects licenses'
optional :fullname, type: String, desc: 'The full name of the copyright holder to use when expanding placeholders in the template. Only affects licenses'
end

View File

@ -688,11 +688,11 @@ module API
optional :search, type: String, desc: 'Return list of groups matching the search criteria'
optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list'
optional :with_shared, type: Boolean, default: false,
desc: 'Include shared groups'
desc: 'Include shared groups'
optional :shared_visible_only, type: Boolean, default: false,
desc: 'Limit to shared groups user has access to'
desc: 'Limit to shared groups user has access to'
optional :shared_min_access_level, type: Integer, values: Gitlab::Access.all_values,
desc: 'Limit returned shared groups by minimum access level to the project'
desc: 'Limit returned shared groups by minimum access level to the project'
use :pagination
end
get ':id/groups', feature_category: :source_code_management do

View File

@ -61,8 +61,8 @@ module API
values: ProtectedBranch::MergeAccessLevel.allowed_access_levels,
desc: 'Access levels allowed to merge (defaults: `40`, maintainer access level)'
optional :allow_force_push, type: Boolean,
default: false,
desc: 'Allow force push for all users with push access.'
default: false,
desc: 'Allow force push for all users with push access.'
use :optional_params_ee
end

View File

@ -23,9 +23,9 @@ module API
params do
requires :id, type: Integer, desc: 'The ID of the group to get releases for'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return projects sorted in ascending and descending order by released_at'
desc: 'Return projects sorted in ascending and descending order by released_at'
optional :simple, type: Boolean, default: false,
desc: 'Return only the ID, URL, name, and path of each project'
desc: 'Return only the ID, URL, name, and path of each project'
use :pagination
end
@ -61,7 +61,7 @@ module API
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return releases sorted in `asc` or `desc` order.'
optional :include_html_description, type: Boolean,
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
end
route_setting :authentication, job_token_allowed: true
get ':id/releases' do
@ -89,7 +89,7 @@ module API
params do
requires :tag_name, type: String, desc: 'The name of the tag', as: :tag
optional :include_html_description, type: Boolean,
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
desc: 'If `true`, a response includes HTML rendered markdown of the release description.'
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do

View File

@ -109,7 +109,7 @@ module API
).execute(:rubygems, name: ::Packages::Rubygems::TEMPORARY_PACKAGE_NAME)
file_params = {
file: params[:file],
file: params[:file],
file_name: PACKAGE_FILENAME
}

View File

@ -22,14 +22,14 @@ module API
def process_metrics
Sidekiq::ProcessSet.new(false).map do |process|
{
hostname: process['hostname'],
pid: process['pid'],
tag: process['tag'],
started_at: Time.at(process['started_at']),
queues: process['queues'],
labels: process['labels'],
hostname: process['hostname'],
pid: process['pid'],
tag: process['tag'],
started_at: Time.at(process['started_at']),
queues: process['queues'],
labels: process['labels'],
concurrency: process['concurrency'],
busy: process['busy']
busy: process['busy']
}
end
end

View File

@ -68,9 +68,9 @@ module API
params :sort_params do
optional :order_by, type: String, values: %w[id name username created_at updated_at],
default: 'id', desc: 'Return users ordered by a field'
default: 'id', desc: 'Return users ordered by a field'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return users sorted in ascending and descending order'
desc: 'Return users sorted in ascending and descending order'
end
end
@ -940,7 +940,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the personal access token'
requires :scopes, type: Array[String], coerce_with: ::API::Validations::Types::CommaSeparatedToArray.coerce, values: ::Gitlab::Auth.all_available_scopes.map(&:to_s),
desc: 'The array of scopes of the personal access token'
desc: 'The array of scopes of the personal access token'
optional :expires_at, type: Date, desc: 'The expiration date in the format YEAR-MONTH-DAY of the personal access token'
end
post feature_category: :authentication_and_authorization do

View File

@ -94,7 +94,7 @@ module Backup
def build_env
{
'SSL_CERT_FILE' => Gitlab::X509::Certificate.default_cert_file,
'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
'SSL_CERT_DIR' => Gitlab::X509::Certificate.default_cert_dir
}.merge(ENV)
end

View File

@ -240,11 +240,11 @@ module Banzai
object_parent_type = parent.is_a?(Group) ? :group : :project
{
original: escape_html_entities(text),
link: link_content,
link_reference: link_reference,
original: escape_html_entities(text),
link: link_content,
link_reference: link_reference,
object_parent_type => parent.id,
object_sym => object.id
object_sym => object.id
}
end

View File

@ -41,8 +41,8 @@ module Banzai
nodes_for_document = entry[:nodes]
doc_data = {
document: entry[:document],
total_reference_count: nodes_for_document.count,
document: entry[:document],
total_reference_count: nodes_for_document.count,
visible_reference_count: nodes_for_document.count
}

View File

@ -38,13 +38,14 @@ module Gitlab
def initialize(report_type, report_version)
@report_type = report_type.to_sym
@report_version = report_version.to_s
@supported_versions = SUPPORTED_VERSIONS[@report_type]
end
delegate :validate, to: :schemer
private
attr_reader :report_type, :report_version
attr_reader :report_type, :report_version, :supported_versions
def schemer
JSONSchemer.schema(pathname)
@ -60,10 +61,24 @@ module Gitlab
report_declared_version = File.join(root_path, report_version, file_name)
return report_declared_version if File.file?(report_declared_version)
if latest_vendored_patch_version
latest_vendored_patch_version_file = File.join(root_path, latest_vendored_patch_version, file_name)
return latest_vendored_patch_version_file if File.file?(latest_vendored_patch_version)
end
earliest_supported_version = SUPPORTED_VERSIONS[report_type].min
File.join(root_path, earliest_supported_version, file_name)
end
def latest_vendored_patch_version
::Security::ReportSchemaVersionMatcher.new(
report_declared_version: report_version,
supported_versions: supported_versions
).call
rescue ArgumentError
nil
end
def file_name
report_type == :api_fuzzing ? "dast-report-format.json" : "#{report_type.to_s.dasherize}-report-format.json"
end
@ -79,16 +94,80 @@ module Gitlab
@warnings = []
@deprecation_warnings = []
populate_errors
populate_warnings
populate_schema_version_errors
populate_validation_errors
populate_deprecation_warnings
end
def valid?
errors.empty?
def populate_schema_version_errors
add_schema_version_errors if add_schema_version_error?
end
def populate_errors
def add_schema_version_errors
if report_version.nil?
template = _("Report version not provided,"\
" %{report_type} report type supports versions: %{supported_schema_versions}."\
" GitLab will attempt to validate this report against the earliest supported versions of this report"\
" type, to show all the errors but will not ingest the report")
message = format(template, report_type: report_type, supported_schema_versions: supported_schema_versions)
else
template = _("Version %{report_version} for report type %{report_type} is unsupported, supported versions"\
" for this report type are: %{supported_schema_versions}."\
" GitLab will attempt to validate this report against the earliest supported versions of this report"\
" type, to show all the errors but will not ingest the report")
message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions)
end
log_warnings(problem_type: 'using_unsupported_schema_version')
add_message_as(level: :error, message: message)
end
def add_schema_version_error?
!report_uses_supported_schema_version? &&
!report_uses_deprecated_schema_version? &&
!report_uses_supported_major_and_minor_schema_version?
end
def report_uses_deprecated_schema_version?
DEPRECATED_VERSIONS[report_type].include?(report_version)
end
def report_uses_supported_schema_version?
SUPPORTED_VERSIONS[report_type].include?(report_version)
end
def report_uses_supported_major_and_minor_schema_version?
if !find_latest_patch_version.nil?
add_supported_major_minor_behavior_warning
true
else
false
end
end
def find_latest_patch_version
::Security::ReportSchemaVersionMatcher.new(
report_declared_version: report_version,
supported_versions: SUPPORTED_VERSIONS[report_type]
).call
rescue ArgumentError
nil
end
def add_supported_major_minor_behavior_warning
template = _("This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\
" any vendored schema version. Validation will be attempted against version"\
" %{find_latest_patch_version}")
message = format(template, find_latest_patch_version: find_latest_patch_version)
add_message_as(
level: :warning,
message: message
)
end
def populate_validation_errors
schema_validation_errors = schema.validate(report_data).map { |error| JSONSchemer::Errors.pretty(error) }
log_warnings(problem_type: 'schema_validation_fails') unless schema_validation_errors.empty?
@ -96,10 +175,6 @@ module Gitlab
@errors += schema_validation_errors
end
def populate_warnings
add_unsupported_report_version_message if !report_uses_supported_schema_version? && !report_uses_deprecated_schema_version?
end
def populate_deprecation_warnings
add_deprecated_report_version_message if report_uses_deprecated_schema_version?
end
@ -107,10 +182,19 @@ module Gitlab
def add_deprecated_report_version_message
log_warnings(problem_type: 'using_deprecated_schema_version')
message = "Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this report type are: #{supported_schema_versions}"
template = _("Version %{report_version} for report type %{report_type} has been deprecated,"\
" supported versions for this report type are: %{supported_schema_versions}."\
" GitLab will attempt to parse and ingest this report if valid.")
message = format(template, report_version: report_version, report_type: report_type, supported_schema_versions: supported_schema_versions)
add_message_as(level: :deprecation_warning, message: message)
end
def valid?
errors.empty?
end
def log_warnings(problem_type:)
Gitlab::AppLogger.info(
message: 'security report schema validation problem',
@ -123,30 +207,6 @@ module Gitlab
)
end
def add_unsupported_report_version_message
log_warnings(problem_type: 'using_unsupported_schema_version')
handle_unsupported_report_version
end
def report_uses_deprecated_schema_version?
DEPRECATED_VERSIONS[report_type].include?(report_version)
end
def report_uses_supported_schema_version?
SUPPORTED_VERSIONS[report_type].include?(report_version)
end
def handle_unsupported_report_version
if report_version.nil?
message = "Report version not provided, #{report_type} report type supports versions: #{supported_schema_versions}"
else
message = "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: #{supported_schema_versions}"
end
add_message_as(level: :error, message: message)
end
def supported_schema_versions
SUPPORTED_VERSIONS[report_type].join(", ")
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Security
class ReportSchemaVersionMatcher
def initialize(report_declared_version:, supported_versions:)
@report_version = Gem::Version.new(report_declared_version)
@supported_versions = supported_versions.sort.map { |version| Gem::Version.new(version) }
end
attr_reader :report_version, :supported_versions
def call
find_matching_versions
end
private
def find_matching_versions
dependency = Gem::Dependency.new('', approximate_version)
matches = supported_versions.map do |supported_version|
exact_version = ['', supported_version.to_s]
[supported_version.to_s, dependency.match?(*exact_version)]
end
matches.to_h.select { |_, matches_dependency| matches_dependency == true }.keys.max
end
def approximate_version
"~> #{generate_patch_version}"
end
def generate_patch_version
# We can't use #approximate_recommendation here because
# for "14.0.32" it would yield "~> 14.0" and according to
# https://www.rubydoc.info/github/rubygems/rubygems/Gem/Version#label-Preventing+Version+Catastrophe-3A
# "~> 3.0" covers [3.0...4.0)
# and version 14.1.0 would fall within that range
#
# Instead we replace the patch number with 0 and get "~> 14.0.0"
# Which will work as we want it to
(report_version.segments[0...2] << 0).join('.')
end
end
end

View File

@ -31894,6 +31894,12 @@ msgstr ""
msgid "Puma is running with a thread count above 1 and the Rugged service is enabled. This may decrease performance in some environments. See our %{link_start}documentation%{link_end} for details of this issue."
msgstr ""
msgid "PumbleIntegration|Send notifications about project events to Pumble."
msgstr ""
msgid "PumbleIntegration|Send notifications about project events to Pumble. %{docs_link}"
msgstr ""
msgid "Purchase more minutes"
msgstr ""
@ -32837,6 +32843,9 @@ msgstr ""
msgid "Report for the scan has been removed from the database."
msgstr ""
msgid "Report version not provided, %{report_type} report type supports versions: %{supported_schema_versions}. GitLab will attempt to validate this report against the earliest supported versions of this report type, to show all the errors but will not ingest the report"
msgstr ""
msgid "Report your license usage data to GitLab"
msgstr ""
@ -40324,6 +40333,9 @@ msgstr ""
msgid "This release was created with a date in the past. Evidence collection at the moment of the release is unavailable."
msgstr ""
msgid "This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match any vendored schema version. Validation will be attempted against version %{find_latest_patch_version}"
msgstr ""
msgid "This repository"
msgstr ""
@ -42970,6 +42982,12 @@ msgstr ""
msgid "Version"
msgstr ""
msgid "Version %{report_version} for report type %{report_type} has been deprecated, supported versions for this report type are: %{supported_schema_versions}. GitLab will attempt to parse and ingest this report if valid."
msgstr ""
msgid "Version %{report_version} for report type %{report_type} is unsupported, supported versions for this report type are: %{supported_schema_versions}. GitLab will attempt to validate this report against the earliest supported versions of this report type, to show all the errors but will not ingest the report"
msgstr ""
msgid "Version %{versionNumber}"
msgstr ""

View File

@ -51,8 +51,8 @@
"@babel/preset-env": "^7.18.2",
"@gitlab/at.js": "1.5.7",
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "2.33.0",
"@gitlab/ui": "42.25.0",
"@gitlab/svgs": "3.1.0",
"@gitlab/ui": "43.5.0",
"@gitlab/visual-review-tools": "1.7.3",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",

View File

@ -182,12 +182,14 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }
it 'signs up and redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
context 'the user signs up for an account with the invitation email address' do
it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
expect(page).to have_content('You have been granted Owner access to group Owned.')
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
expect(page).to have_content('You have been granted Owner access to group Owned.')
end
end
context 'the user sign-up using a different email address' do
@ -227,11 +229,13 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
end
end
it 'signs up and redirects to the group activity page with all the project/groups invitation automatically accepted' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
context 'the user signs up for an account with the invitation email address' do
it 'redirects to the most recent membership activity page with all the projects/groups invitations automatically accepted' do
fill_in_sign_up_form(new_user)
fill_in_welcome_form
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
end
end
context 'the user sign-up using a different email address' do

View File

@ -85,7 +85,46 @@ RSpec.describe 'OAuth Registration', :js, :allow_forgery_protection do
expect(page).to have_content('Please complete your profile with email address')
end
end
context 'when registering via an invitation email' do
let_it_be(:owner) { create(:user) }
let_it_be(:group) { create(:group, name: 'Owned') }
let_it_be(:project) { create(:project, :repository, namespace: group) }
let(:invite_email) { generate(:email) }
let(:extra_params) { { invite_type: Emails::Members::INITIAL_INVITE } }
let(:group_invite) do
create(
:group_member, :invited,
group: group,
invite_email: invite_email,
created_by: owner
)
end
before do
project.add_maintainer(owner)
group.add_owner(owner)
group_invite.generate_invite_token!
mock_auth_hash(provider, uid, invite_email, additional_info: additional_info)
end
it 'redirects to the activity page with all the projects/groups invitations accepted' do
visit invite_path(group_invite.raw_invite_token, extra_params)
click_link_or_button "oauth-login-#{provider}"
fill_in_welcome_form
expect(page).to have_content('You have been granted Owner access to group Owned.')
expect(page).to have_current_path(activity_group_path(group), ignore_query: true)
end
end
end
end
end
def fill_in_welcome_form
select 'Software Developer', from: 'user_role'
click_button 'Get started!'
end
end

View File

@ -87,8 +87,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
expect(findPrimaryButton().attributes('disabled')).toBe('true');
expect(findSecondaryButton().attributes('disabled')).toBe('true');
});
});
@ -105,8 +105,8 @@ describe('Delete user modal', () => {
});
it('has disabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
expect(findPrimaryButton().attributes('disabled')).toBe('true');
expect(findSecondaryButton().attributes('disabled')).toBe('true');
});
});
@ -123,8 +123,8 @@ describe('Delete user modal', () => {
});
it('has enabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeFalsy();
expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
expect(findPrimaryButton().attributes('disabled')).toBeUndefined();
expect(findSecondaryButton().attributes('disabled')).toBeUndefined();
});
describe('when primary action is clicked', () => {

View File

@ -58,7 +58,7 @@ describe('Ci variable modal', () => {
});
it('button is disabled when no key/value pair are present', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
});
});
@ -71,7 +71,7 @@ describe('Ci variable modal', () => {
});
it('button is enabled when key/value pair are present', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
it('Add variable button dispatches addVariable action', () => {
@ -249,7 +249,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
});
it('shows the correct error text', () => {
@ -316,7 +316,7 @@ describe('Ci variable modal', () => {
});
it('does not disable the submit button', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
});
});

View File

@ -261,7 +261,7 @@ describe('Client side Markdown processing', () => {
...source('<img src="bar" alt="foo" />'),
alt: 'foo',
canonicalSrc: 'bar',
src: 'http://test.host/bar',
src: 'bar',
}),
),
),
@ -283,7 +283,7 @@ describe('Client side Markdown processing', () => {
image({
...source('<img src="bar" alt="foo" />'),
alt: 'foo',
src: 'http://test.host/bar',
src: 'bar',
canonicalSrc: 'bar',
}),
),
@ -297,7 +297,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com/',
href: 'https://gitlab.com',
canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
@ -316,7 +316,7 @@ describe('Client side Markdown processing', () => {
link(
{
...source('[GitLab](https://gitlab.com "Go to GitLab")'),
href: 'https://gitlab.com/',
href: 'https://gitlab.com',
canonicalSrc: 'https://gitlab.com',
title: 'Go to GitLab',
},
@ -335,7 +335,7 @@ describe('Client side Markdown processing', () => {
{
...source('www.commonmark.org'),
canonicalSrc: 'http://www.commonmark.org',
href: 'http://www.commonmark.org/',
href: 'http://www.commonmark.org',
},
'www.commonmark.org',
),
@ -389,7 +389,7 @@ describe('Client side Markdown processing', () => {
sourceMapKey: null,
sourceMarkdown: null,
canonicalSrc: 'https://gitlab.com',
href: 'https://gitlab.com/',
href: 'https://gitlab.com',
},
'https://gitlab.com',
),
@ -616,7 +616,7 @@ two
...source('![bar](foo.png)'),
alt: 'bar',
canonicalSrc: 'foo.png',
src: 'http://test.host/foo.png',
src: 'foo.png',
}),
),
),
@ -969,12 +969,12 @@ Paragraph
{
...source('[![moon](moon.jpg)](/uri)'),
canonicalSrc: '/uri',
href: 'http://test.host/uri',
href: '/uri',
},
image({
...source('![moon](moon.jpg)'),
canonicalSrc: 'moon.jpg',
src: 'http://test.host/moon.jpg',
src: 'moon.jpg',
alt: 'moon',
}),
),
@ -1010,7 +1010,7 @@ Paragraph
{
...source('[moon](moon.jpg)'),
canonicalSrc: 'moon.jpg',
href: 'http://test.host/moon.jpg',
href: 'moon.jpg',
},
'moon',
),
@ -1021,7 +1021,7 @@ Paragraph
link(
{
...source('[sun](sun.jpg)'),
href: 'http://test.host/sun.jpg',
href: 'sun.jpg',
canonicalSrc: 'sun.jpg',
},
'sun',
@ -1141,7 +1141,7 @@ _world_.
link(
{
...source('[GitLab][gitlab-url]'),
href: 'https://gitlab.com/',
href: 'https://gitlab.com',
canonicalSrc: 'https://gitlab.com',
title: 'GitLab',
},
@ -1235,4 +1235,72 @@ body {
expect(tiptapEditor.getHTML()).toEqual(expectedHtml);
},
);
describe('attribute sanitization', () => {
// eslint-disable-next-line no-script-url
const protocolBasedInjectionSimpleNoSpaces = "javascript:alert('XSS');";
// eslint-disable-next-line no-script-url
const protocolBasedInjectionSimpleSpacesBefore = "javascript: alert('XSS');";
const docWithImageFactory = (urlInput, urlOutput) => {
const input = `<img src="${urlInput}">`;
return {
input,
expectedDoc: doc(
paragraph(
source(input),
image({
...source(input),
src: urlOutput,
canonicalSrc: urlOutput,
}),
),
),
};
};
const docWithLinkFactory = (urlInput, urlOutput) => {
const input = `<a href="${urlInput}">foo</a>`;
return {
input,
expectedDoc: doc(
paragraph(
source(input),
link({ ...source(input), href: urlOutput, canonicalSrc: urlOutput }, 'foo'),
),
),
};
};
it.each`
desc | urlInput | urlOutput
${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
${'protocol-based JS injection: simple, spaces before'} | ${"javascript :alert('XSS');"} | ${null}
${'protocol-based JS injection: simple, spaces after'} | ${protocolBasedInjectionSimpleSpacesBefore} | ${null}
${'protocol-based JS injection: simple, spaces before and after'} | ${"javascript : alert('XSS');"} | ${null}
${'protocol-based JS injection: UTF-8 encoding'} | ${'javascript&#58;'} | ${null}
${'protocol-based JS injection: long UTF-8 encoding'} | ${'javascript&#0058;'} | ${null}
${'protocol-based JS injection: long UTF-8 encoding without semicolons'} | ${'&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041'} | ${null}
${'protocol-based JS injection: hex encoding'} | ${'javascript&#x3A;'} | ${null}
${'protocol-based JS injection: long hex encoding'} | ${'javascript&#x003A;'} | ${null}
${'protocol-based JS injection: hex encoding without semicolons'} | ${'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29'} | ${null}
${'protocol-based JS injection: Unicode'} | ${"\u0001java\u0003script:alert('XSS')"} | ${null}
${'protocol-based JS injection: spaces and entities'} | ${"&#14; javascript:alert('XSS');"} | ${null}
${'vbscript'} | ${'vbscript:alert(document.domain)'} | ${null}
${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java<76>script:alert('XSS')"}
${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
`('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
const exampleFactories = [docWithImageFactory, docWithLinkFactory];
exampleFactories.forEach(async (exampleFactory) => {
const { input, expectedDoc } = exampleFactory(urlInput, urlOutput);
const document = await deserialize(input);
expect(document.toJSON()).toEqual(expectedDoc.toJSON());
});
});
});
});

View File

@ -1213,47 +1213,47 @@ paragraph
};
it.each`
mark | markdown | modifiedMarkdown | editAction
${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com/">link modified</a>'} | ${defaultEditAction}
${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link [https://www.gitlab.com>](https://www.gitlab.com%3E)'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction}
${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
mark | markdown | modifiedMarkdown | editAction
${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction}
${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction}
${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction}
${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction}
${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction}
${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction}
${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction}
${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction}
${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction}
${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com">link modified</a>'} | ${defaultEditAction}
${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction}
${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction}
${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction}
${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction}
${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link https://www.gitlab.com>'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction}
${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction}
${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link **https://www.gitlab.com\\]**'} | ${prependContentEditAction}
${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction}
${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction}
${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction}
${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction}
${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction}
${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction}
${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction}
${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction}
${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction}
${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction}
${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction}
${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction}
${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction}
${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction}
${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction}
${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction}
`(
'preserves original $mark syntax when sourceMarkdown is available for $markdown',
async ({ markdown, modifiedMarkdown, editAction }) => {

View File

@ -119,7 +119,7 @@ describe('Issuable output', () => {
expect(findEdited().exists()).toBe(true);
expect(findEdited().props('updatedByPath')).toMatch(/\/some_user$/);
expect(findEdited().props('updatedAt')).toBeTruthy();
expect(findEdited().props('updatedAt')).toBe(initialRequest.updated_at);
expect(wrapper.vm.state.lock_version).toBe(initialRequest.lock_version);
})
.then(() => {
@ -133,7 +133,7 @@ describe('Issuable output', () => {
expect(findEdited().exists()).toBe(true);
expect(findEdited().props('updatedByName')).toBe('Other User');
expect(findEdited().props('updatedByPath')).toMatch(/\/other_user$/);
expect(findEdited().props('updatedAt')).toBeTruthy();
expect(findEdited().props('updatedAt')).toBe(secondRequest.updated_at);
});
});

View File

@ -148,7 +148,7 @@ describe('ProjectDropdown', () => {
});
it('emits `error` event', () => {
expect(wrapper.emitted('error')).toBeTruthy();
expect(wrapper.emitted('error')).toHaveLength(1);
});
});

View File

@ -124,7 +124,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
});
it('clicked on link with view', () => {
expect(primaryLink.props('menuItem').view).toBeTruthy();
expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace);
});
it('changes active view', () => {

View File

@ -357,7 +357,7 @@ describe('issue_note', () => {
createWrapper();
updateActions();
wrapper.findComponent(NoteBody).vm.$emit('handleFormUpdate', params);
expect(wrapper.emitted('handleUpdateNote')).toBeTruthy();
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
});
it('does not stringify empty position', () => {

View File

@ -102,7 +102,7 @@ describe('Pipelines filtered search', () => {
it('emits filterPipelines on submit with correct filter', () => {
findFilteredSearch().vm.$emit('submit', mockSearch);
expect(wrapper.emitted('filterPipelines')).toBeTruthy();
expect(wrapper.emitted('filterPipelines')).toHaveLength(1);
expect(wrapper.emitted('filterPipelines')[0]).toEqual([mockSearch]);
});

View File

@ -50,39 +50,33 @@ describe('PrometheusMetrics', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LOADING);
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toEqual(false);
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
expect(
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
it('should show metrics list when called with `list`', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.LIST);
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
expect(
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
it('should show empty state when called with `empty`', () => {
customMetrics.showMonitoringCustomMetricsPanelState(PANEL_STATE.EMPTY);
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsEmpty.hasClass('hidden')).toEqual(false);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
expect(
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toEqual(false);
@ -94,14 +88,12 @@ describe('PrometheusMetrics', () => {
const $metricsListLi = customMetrics.$monitoredCustomMetricsList.find('li');
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toEqual(false);
expect(
customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden'),
).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsNoIntegrationText.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toEqual(false);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
expect($metricsListLi.length).toEqual(metrics.length);
});
@ -114,10 +106,10 @@ describe('PrometheusMetrics', () => {
false,
);
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBeTruthy();
expect(customMetrics.$monitoredCustomMetricsLoading.hasClass('hidden')).toBe(true);
expect(customMetrics.$monitoredCustomMetricsList.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricButton.hasClass('hidden')).toBe(true);
expect(customMetrics.$newCustomMetricText.hasClass('hidden')).toBe(true);
});
});
});

View File

@ -54,25 +54,25 @@ describe('PrometheusMetrics', () => {
it('should show loading state when called with `loading`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
});
it('should show metrics list when called with `list`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
});
it('should show empty state when called with `empty`', () => {
prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(true);
});
});
@ -88,8 +88,8 @@ describe('PrometheusMetrics', () => {
const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li');
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual(
'3 exporters with 12 metrics were found',
@ -102,8 +102,8 @@ describe('PrometheusMetrics', () => {
it('should show missing environment variables list', () => {
prometheusMetrics.populateActiveMetrics(missingVarMetrics);
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBe(false);
expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2');
expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2);
@ -143,12 +143,12 @@ describe('PrometheusMetrics', () => {
prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(false);
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
await waitForPromises();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
});
it('should show empty state if response failed to load', async () => {
@ -158,8 +158,8 @@ describe('PrometheusMetrics', () => {
await waitForPromises();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBe(true);
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBe(false);
});
it('should populate metrics list once response is loaded', async () => {

View File

@ -35,6 +35,7 @@ RSpec.describe GitlabSchema.types['ServiceType'] do
PIPELINES_EMAIL_SERVICE
PIVOTALTRACKER_SERVICE
PROMETHEUS_SERVICE
PUMBLE_SERVICE
PUSHOVER_SERVICE
REDMINE_SERVICE
SHIMO_SERVICE

View File

@ -68,6 +68,49 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
describe '#valid?' do
subject { validator.valid? }
context 'when given a supported MAJOR.MINOR schema version' do
let(:report_type) { :dast }
let(:report_version) do
latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".")
(latest_vendored_version[0...2] << "34").join(".")
end
context 'and the report is valid' do
let(:report_data) do
{
'version' => report_version,
'vulnerabilities' => []
}
end
it { is_expected.to be_truthy }
end
context 'and the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
it { is_expected.to be_falsey }
it 'logs related information' do
expect(Gitlab::AppLogger).to receive(:info).with(
message: "security report schema validation problem",
security_report_type: report_type,
security_report_version: report_version,
project_id: project.id,
security_report_failure: 'schema_validation_fails',
security_report_scanner_id: 'gemnasium',
security_report_scanner_version: '2.1.0'
)
subject
end
end
end
context 'when given a supported schema version' do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }
@ -320,6 +363,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
context 'when given an unsupported schema version' do
let(:report_type) { :dast }
let(:report_version) { "12.37.0" }
let(:expected_unsupported_message) do
"Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: "\
"#{supported_dast_versions}. GitLab will attempt to validate this report against the earliest supported "\
"versions of this report type, to show all the errors but will not ingest the report"
end
context 'and the report is valid' do
let(:report_data) do
@ -331,7 +379,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
"Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}"
expected_unsupported_message
]
end
@ -347,7 +395,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
"Version 12.37.0 for report type dast is unsupported, supported versions for this report type are: #{supported_dast_versions}",
expected_unsupported_message,
"root is missing required keys: vulnerabilities"
]
end
@ -359,6 +407,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
context 'when not given a schema version' do
let(:report_type) { :dast }
let(:report_version) { nil }
let(:expected_missing_version_message) do
"Report version not provided, #{report_type} report type supports versions: #{supported_dast_versions}. GitLab "\
"will attempt to validate this report against the earliest supported versions of this report type, to show all "\
"the errors but will not ingest the report"
end
let(:report_data) do
{
'vulnerabilities' => []
@ -368,7 +422,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
let(:expected_errors) do
[
"root is missing required keys: version",
"Report version not provided, dast report type supports versions: #{supported_dast_versions}"
expected_missing_version_message
]
end
@ -414,9 +468,14 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
end
let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last }
let(:expected_deprecation_message) do
"Version #{report_version} for report type #{report_type} has been deprecated, supported versions for this "\
"report type are: #{supported_dast_versions}. GitLab will attempt to parse and ingest this report if valid."
end
let(:expected_deprecation_warnings) do
[
"Version V2.7.0 for report type dast has been deprecated, supported versions for this report type are: #{supported_dast_versions}"
expected_deprecation_message
]
end
@ -464,6 +523,62 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do
describe '#warnings' do
subject { validator.warnings }
context 'when given a supported MAJOR.MINOR schema version' do
let(:report_type) { :dast }
let(:report_version) do
latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".")
(latest_vendored_version[0...2] << "34").join(".")
end
let(:latest_patch_version) do
::Security::ReportSchemaVersionMatcher.new(
report_declared_version: report_version,
supported_versions: described_class::SUPPORTED_VERSIONS[report_type]
).call
end
let(:message) do
"This report uses a supported MAJOR.MINOR schema version but the PATCH version doesn't match"\
" any vendored schema version. Validation will be attempted against version"\
" #{latest_patch_version}"
end
context 'and the report is valid' do
let(:report_data) do
{
'version' => report_version,
'vulnerabilities' => []
}
end
it { is_expected.to match_array([message]) }
end
context 'and the report is invalid' do
let(:report_data) do
{
'version' => report_version
}
end
it { is_expected.to match_array([message]) }
it 'logs related information' do
expect(Gitlab::AppLogger).to receive(:info).with(
message: "security report schema validation problem",
security_report_type: report_type,
security_report_version: report_version,
project_id: project.id,
security_report_failure: 'schema_validation_fails',
security_report_scanner_id: 'gemnasium',
security_report_scanner_version: '2.1.0'
)
subject
end
end
end
context 'when given a supported schema version' do
let(:report_type) { :dast }
let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last }

View File

@ -597,6 +597,7 @@ project:
- alert_management_alerts
- repository_storage_moves
- freeze_periods
- pumble_integration
- webex_teams_integration
- build_report_results
- vulnerability_statistic

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::ReportSchemaVersionMatcher do
let(:vendored_versions) { %w[14.0.0 14.0.1 14.0.2 14.1.0] }
let(:version_finder) do
described_class.new(
report_declared_version: report_version,
supported_versions: vendored_versions
)
end
describe '#call' do
subject { version_finder.call }
context 'when minor version matches' do
context 'and report schema patch version does not match any vendored schema versions' do
context 'and report version is 14.1.1' do
let(:report_version) { '14.1.1' }
it 'returns 14.1.0' do
expect(subject).to eq('14.1.0')
end
end
context 'and report version is 14.0.32' do
let(:report_version) { '14.0.32' }
it 'returns 14.0.2' do
expect(subject).to eq('14.0.2')
end
end
end
end
context 'when report minor version does not match' do
let(:report_version) { '14.2.1' }
it 'does not return a version' do
expect(subject).to be_nil
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Integrations::Pumble do
it_behaves_like "chat integration", "Pumble" do
let(:client_arguments) { webhook_url }
let(:payload) do
{
text: be_present
}
end
end
end

View File

@ -44,6 +44,7 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to have_one(:mattermost_integration) }
it { is_expected.to have_one(:hangouts_chat_integration) }
it { is_expected.to have_one(:unify_circuit_integration) }
it { is_expected.to have_one(:pumble_integration) }
it { is_expected.to have_one(:webex_teams_integration) }
it { is_expected.to have_one(:packagist_integration) }
it { is_expected.to have_one(:pushover_integration) }

View File

@ -66,6 +66,7 @@ RSpec.describe API::Integrations do
mattermost: %i[deployment_channel labels_to_be_notified],
mock_ci: %i[enable_ssl_verification],
prometheus: %i[manual_configuration],
pumble: %i[branches_to_be_notified notify_only_broken_pipelines],
slack: %i[alert_events alert_channel deployment_channel labels_to_be_notified],
unify_circuit: %i[branches_to_be_notified notify_only_broken_pipelines],
webex_teams: %i[branches_to_be_notified notify_only_broken_pipelines]

View File

@ -8,9 +8,8 @@ Gem::Specification.new do |gem|
gem.description = gem.summary
gem.homepage = "https://github.com/tduehr/omniauth-cas3"
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
gem.files = `git ls-files`.split("\n")
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
gem.files = Dir.glob("lib/**/*.*")
gem.test_files = Dir.glob("spec/**/**/*.*")
gem.name = "omniauth-cas3"
gem.require_paths = ["lib"]
gem.version = Omniauth::Cas3::VERSION

View File

@ -1051,15 +1051,15 @@
stylelint-declaration-strict-value "1.8.0"
stylelint-scss "4.2.0"
"@gitlab/svgs@2.33.0":
version "2.33.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.33.0.tgz#e970ae10ee558e1e2b01116b2fe6ea25161a4609"
integrity sha512-8B5pGmZ6QnywxmWCmqMTkJfPlETbx4R7AK7si8Jf2DyWZ7Agfg9NOdgBq++IuiVjbxBO7VTQcZbVSavxrce6QA==
"@gitlab/svgs@3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.1.0.tgz#0108498a17e2f79d16158015db0be764b406cc09"
integrity sha512-kZ45VTQOgLdwQCLRSj7+aohF+6AUnAaoucR1CFY/6DPDLnNNGeflwsCLN0sFBKwx42HLxFfNwvDmKOMLdSQg5A==
"@gitlab/ui@42.25.0":
version "42.25.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-42.25.0.tgz#d79873347be9868c4d3d3123295ce1f12967f330"
integrity sha512-yxSQeLbhrPD4KKQPCo+glarlhoa4cj46j7mgQtTRbJFw2ZWPcpJ4xuujCb8GoyGPlHpWaS8VJyv3l+hwBQs3qg==
"@gitlab/ui@43.5.0":
version "43.5.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.5.0.tgz#c0652c99cd7ba9c69cef1cdf75c85b9164536e24"
integrity sha512-mbWXKylbnEuCXZuNMVic7K6Dvo8hjwYQkpyVvxdpmTCY+eTOtjxenVHE4HgZ5G/7cjznRnj4WLk0Ot8AquifBQ==
dependencies:
"@popperjs/core" "^2.11.2"
bootstrap-vue "2.20.1"