Commit 58730258 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into per-project-pipeline-iid-ee

parents 3bdee5b8 a454614d
......@@ -354,7 +354,7 @@ group :development, :test do
gem 'capybara', '~> 2.15'
gem 'capybara-screenshot', '~> 1.0.0'
gem 'selenium-webdriver', '~> 3.5'
gem 'selenium-webdriver', '~> 3.12'
gem 'spring', '~> 2.0.0'
gem 'spring-commands-rspec', '~> 1.0.4'
......@@ -396,7 +396,7 @@ group :test do
gem 'test-prof', '~> 0.2.5'
end
gem 'octokit', '~> 4.8'
gem 'octokit', '~> 4.9'
gem 'mail_room', '~> 0.9.1'
......
......@@ -123,7 +123,7 @@ GEM
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.6)
childprocess (0.7.0)
childprocess (0.9.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
chronic_duration (0.10.6)
......@@ -546,7 +546,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
octokit (4.8.0)
octokit (4.9.0)
sawyer (~> 0.8.0, >= 0.5.3)
omniauth (1.8.1)
hashie (>= 3.4.6, < 3.6.0)
......@@ -857,9 +857,9 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
selenium-webdriver (3.5.0)
selenium-webdriver (3.12.0)
childprocess (~> 0.5)
rubyzip (~> 1.0)
rubyzip (~> 1.2)
sentry-raven (2.7.2)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
......@@ -1121,7 +1121,7 @@ DEPENDENCIES
net-ssh (~> 4.2.0)
nokogiri (~> 1.8.2)
oauth2 (~> 1.4)
octokit (~> 4.8)
octokit (~> 4.9)
omniauth (~> 1.8)
omniauth-auth0 (~> 2.0.0)
omniauth-authentiq (~> 0.3.3)
......@@ -1191,7 +1191,7 @@ DEPENDENCIES
scss_lint (~> 0.56.0)
seed-fu (~> 2.3.7)
select2-rails (~> 3.5.9)
selenium-webdriver (~> 3.5)
selenium-webdriver (~> 3.12)
sentry-raven (~> 2.7)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
......
......@@ -35,7 +35,12 @@ export default {
</script>
<template>
<div class="hide-collapsed value issuable-show-labels js-value">
<div
class="hide-collapsed value issuable-show-labels js-value"
:class="{
'has-labels':!isEmpty,
}"
>
<span
v-if="isEmpty"
class="text-secondary"
......@@ -50,7 +55,7 @@ export default {
>
<span
v-tooltip
class="label color-label"
class="badge color-label"
data-placement="bottom"
data-container="body"
:style="labelStyle(label)"
......
......@@ -61,8 +61,18 @@ a {
code {
padding: 2px 4px;
color: $red-600;
background-color: $red-100;
border-radius: 3px;
.code & {
background-color: inherit;
padding: unset;
}
}
.code {
padding: 9.5px;
}
table {
......
......@@ -33,16 +33,16 @@ module SearchHelper
"Showing #{from} - #{to} of #{count} #{scope.humanize(capitalize: false)} for \"#{term}\""
end
def find_project_for_result_blob(result)
@project
end
def parse_search_result(result)
if result.is_a?(String)
Gitlab::ProjectSearchResults.parse_search_result(result)
else
Gitlab::Elastic::SearchResults.parse_search_result(result)
end
result
end
def find_project_for_blob(blob)
Project.find(blob['_parent'])
def search_blob_title(project, filename)
filename
end
private
......
......@@ -13,12 +13,12 @@ module Clusters
attr_encrypted :password,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
attr_encrypted :token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
......
......@@ -11,7 +11,7 @@ module Clusters
attr_encrypted :access_token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
......
......@@ -154,10 +154,6 @@ class ProjectWiki
[title, title_array.join("/")]
end
def search_files(query)
repository.search_files_by_content(query, default_branch)
end
def repository
@repository ||= Repository.new(full_path, @project, disk_path: disk_path, is_wiki: true)
end
......
- project = @project || find_project_for_blob(blob)
- if blob.is_a?(Array)
- file_name, blob = blob
- else
- blob = parse_search_result(blob)
- file_name = blob.filename
- project = find_project_for_result_blob(blob)
- file_name, blob = parse_search_result(blob)
- blob_link = project_blob_path(project, tree_join(blob.ref, file_name))
.blob-result
.file-holder
.js-file-title.file-title
= link_to blob_link do
= icon('fa-file')
%strong
- if @project
= file_name
- else
#{project.full_name}:
%i= file_name
- if blob.data
.file-content.code.term
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline, blob_link: blob_link
= render partial: 'search/results/blob_data', locals: { blob: blob, project: project, file_name: file_name, blob_link: blob_link }
.blob-result
.file-holder
.js-file-title.file-title
= link_to blob_link do
%i.fa.fa-file
%strong
= search_blob_title(project, file_name)
- if blob.data
.file-content.code.term
= render 'shared/file_highlight', blob: blob, first_line_number: blob.startline
- project = @project || find_project_for_blob(wiki_blob)
- wiki_blob = parse_search_result(wiki_blob)
- project = find_project_for_result_blob(wiki_blob)
- file_name, wiki_blob = parse_search_result(wiki_blob)
- wiki_blob_link = project_wiki_path(project, wiki_blob.basename)
.blob-result
.file-holder
.js-file-title.file-title
= link_to project_wiki_path(project, wiki_blob.basename) do
%i.fa.fa-file
%strong
- if @project
= wiki_blob.basename
- else
#{project.full_name}:
%i= wiki_blob.basename
.file-content.code.term
= render 'shared/file_highlight', blob: wiki_blob, first_line_number: wiki_blob.startline
= render partial: 'search/results/blob_data', locals: { blob: wiki_blob, project: project, file_name: file_name, blob_link: wiki_blob_link }
---
title: Update selenium-webdriver to 3.12.0
merge_request: 19351
author: Takuya Noguchi
type: other
---
title: Added ability to search by wiki titles
merge_request: 19112
author:
type: added
---
title: Fix attr_encryption key settings
merge_request:
author:
type: fixed
......@@ -110,17 +110,24 @@ class Settings < Settingslogic
File.expand_path(path, Rails.root)
end
# Returns a 256-bit key for attr_encrypted
def attr_encrypted_db_key_base
# Ruby 2.4+ requires passing in the exact required length for OpenSSL keys
# (https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1).
# Previous versions quietly truncated the input.
#
# The default mode for the attr_encrypted gem is to use a 256-bit key.
# We truncate the 128-byte string to 32 bytes.
# Ruby 2.4+ requires passing in the exact required length for OpenSSL keys
# (https://github.com/ruby/ruby/commit/ce635262f53b760284d56bb1027baebaaec175d1).
# Previous versions quietly truncated the input.
#
# Use this when using :per_attribute_iv mode for attr_encrypted.
# We have to truncate the string to 32 bytes for a 256-bit cipher.
def attr_encrypted_db_key_base_truncated
Gitlab::Application.secrets.db_key_base[0..31]
end
# This should be used for :per_attribute_salt_and_iv mode. There is no
# need to truncate the key because the encryptor will use the salt to
# generate a hash of the password:
# https://github.com/attr-encrypted/encryptor/blob/c3a62c4a9e74686dd95e0548f9dc2a361fdc95d1/lib/encryptor.rb#L77
def attr_encrypted_db_key_base
Gitlab::Application.secrets.db_key_base
end
private
def base_url(config)
......
......@@ -48,7 +48,7 @@ class MigrateKubernetesServiceToNewClustersArchitectures < ActiveRecord::Migrati
attr_encrypted :token,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-cbc'
end
......
......@@ -425,6 +425,18 @@ Role | Name | Upstream | Connection String
If the 'Role' column for any node says "FAILED", check the
[Troubleshooting section](#troubleshooting) before proceeding.
Also, check that the check master command works successfully on each node:
```
su - gitlab-consul
gitlab-ctl repmgr-check-master
```
This command relies on exit codes to tell Consul whether a particular node is a master
or secondary. The most important thing here is that this command does not produce errors.
If there are errors it's most likely due to incorrect `gitlab-consul` database user permissions.
Check the [Troubleshooting section](#troubleshooting) before proceeding.
### Configuring the Pgbouncer node
1. Make sure you collect [`CONSUL_SERVER_NODES`](#consul_information), [`CONSUL_PASSWORD_HASH`](#consul_information), and [`PGBOUNCER_PASSWORD_HASH`](#pgbouncer_information) before executing the next step.
......@@ -972,6 +984,21 @@ For PostgreSQL, it is usually safe to restart the master node by default. Automa
On the consul server nodes, it is important to restart the consul service in a controlled fashion. Read our [consul documentation](consul.md#restarting-the-server-cluster) for instructions on how to restart the service.
#### `gitlab-ctl repmgr-check-master` command produces errors
If this command displays errors about database permissions it is likely that something failed during
install, resulting in the `gitlab-consul` database user getting incorrect permissions. Follow these
steps to fix the problem:
1. On the master database node, connect to the database prompt - `gitlab-psql -d template1`
1. Delete the `gitlab-consul` user - `DROP USER "gitlab-consul";`
1. Exit the database prompt - `\q`
1. [Reconfigure GitLab] and the user will be re-added with the proper permissions.
1. Change to the `gitlab-consul` user - `su - gitlab-consul`
1. Try the check command again - `gitlab-ctl repmgr-check-master`.
Now there should not be errors. If errors still occur then there is another problem.
#### PGBouncer error `ERROR: pgbouncer cannot connect to server`
You may get this error when running `gitlab-rake gitlab:db:configure` or you
......
......@@ -83,12 +83,12 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
*.example.io. 1800 IN A 1.1.1.1
*.example.io. 1800 IN A 192.0.2.1
*.example.io. 1800 IN AAAA 2001::1
```
where `example.io` is the domain under which GitLab Pages will be served
and `1.1.1.1` is the IPv4 address of your GitLab instance and `2001::1` is the
and `192.0.2.1` is the IPv4 address of your GitLab instance and `2001::1` is the
IPv6 address. If you don't have IPv6, you can omit the AAAA record.
> **Note:**
......@@ -193,13 +193,13 @@ world. Custom domains are supported, but no TLS.
```shell
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80']
```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
`1.1.1.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
where `192.0.2.1` is the primary IP address that GitLab is listening to and
`192.0.2.2` and `2001::2` are the secondary IPs the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
......@@ -228,16 +228,16 @@ world. Custom domains and TLS are supported.
```shell
pages_external_url "https://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
nginx['listen_addresses'] = ['192.0.2.1']
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
gitlab_pages['external_http'] = ['1.1.1.2:80', '[2001::2]:80']
gitlab_pages['external_https'] = ['1.1.1.2:443', '[2001::2]:443']
gitlab_pages['external_http'] = ['192.0.2.2:80', '[2001::2]:80']
gitlab_pages['external_https'] = ['192.0.2.2:443', '[2001::2]:443']
```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
`1.1.1.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
where `192.0.2.1` is the primary IP address that GitLab is listening to and
`192.0.2.2` and `2001::2` are the secondary IPs where the GitLab Pages daemon
listens on. If you don't have IPv6, you can omit the IPv6 address.
1. [Reconfigure GitLab][reconfigure]
......
......@@ -67,11 +67,11 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
*.example.io. 1800 IN A 1.1.1.1
*.example.io. 1800 IN A 192.0.2.1
```
where `example.io` is the domain under which GitLab Pages will be served
and `1.1.1.1` is the IP address of your GitLab instance.
and `192.0.2.1` is the IP address of your GitLab instance.
> **Note:**
You should not use the GitLab domain to serve user pages. For more information
......@@ -253,7 +253,7 @@ world. Custom domains are supported, but no TLS.
port: 80
https: false
external_http: 1.1.1.2:80
external_http: 192.0.2.2:80
```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
......@@ -263,7 +263,7 @@ world. Custom domains are supported, but no TLS.
```
gitlab_pages_enabled=true
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80"
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
......@@ -274,7 +274,7 @@ world. Custom domains are supported, but no TLS.
```
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
`0.0.0.0` with `192.0.2.1`, where `192.0.2.1` the primary IP where GitLab
listens to.
1. Restart NGINX
1. [Restart GitLab][restart]
......@@ -320,8 +320,8 @@ world. Custom domains and TLS are supported.
port: 443
https: true
external_http: 1.1.1.2:80
external_https: 1.1.1.2:443
external_http: 192.0.2.2:80
external_https: 192.0.2.2:443
```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
......@@ -333,7 +333,7 @@ world. Custom domains and TLS are supported.
```
gitlab_pages_enabled=true
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 192.0.2.2:80 -listen-https 192.0.2.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file:
......@@ -344,7 +344,7 @@ world. Custom domains and TLS are supported.
```
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
`0.0.0.0` with `192.0.2.1`, where `192.0.2.1` the primary IP where GitLab
listens to.
1. Restart NGINX
1. [Restart GitLab][restart]
......
......@@ -144,7 +144,7 @@ helm install --name gitlab -f values.yaml gitlab/gitlab-omnibus
or passing them on the command line:
```bash
helm install --name gitlab --set baseDomain=gitlab.io,baseIP=1.1.1.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus
helm install --name gitlab --set baseDomain=gitlab.io,baseIP=192.0.2.1,gitlab=ee,gitlabEELicense=$LICENSE,legoEmail=email@gitlab.com gitlab/gitlab-omnibus
```
## Updating GitLab using the Helm Chart
......
......@@ -110,7 +110,7 @@ On the sign in page there should now be a GitHub icon below the regular sign in
Click the icon to begin the authentication process. GitHub will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to GitLab and will be signed in.
### GitHub Enterprise with Self-Signed Certificate
## GitHub Enterprise with self-signed Certificate
If you are attempting to import projects from GitHub Enterprise with a self-signed
certificate and the imports are failing, you will need to disable SSL verification.
......
This diff is collapsed.
# GitHub Project Integration
# GitHub project integration **[PREMIUM]**
GitLab provides integration for updating pipeline statuses on GitHub. This is especially useful if using GitLab for CI/CD only.
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/3836) in GitLab Premium 10.6.
This project integration is separate from the [instance wide GitHub integration][gh-integration] and is automatically configured on [GitHub import][gh-import].
GitLab provides an integration for updating the pipeline statuses on GitHub.
This is especially useful if using GitLab for CI/CD only.
This project integration is separate from the [instance wide GitHub integration](../import/github.md#mirroring-and-pipeline-status-sharing)
and is automatically configured on [GitHub import](../../../integration/github.md).
![Pipeline status update on GitHub](img/github_status_check_pipeline_update.png)
......@@ -10,14 +14,14 @@ This project integration is separate from the [instance wide GitHub integration]
### Complete these steps on GitHub
This integration requires a [GitHub API token](https://github.com/settings/tokens) with `repo:status` access granted:
This integration requires a [GitHub API token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)
with `repo:status` access granted:
1. Go to your "Personal access tokens" page at https://github.com/settings/tokens
1. Click "Generate New Token"
1. Ensure that `repo:status` is checked and click "Generate token"
1. Copy the generated token to use on GitLab
### Complete these steps on GitLab
1. Navigate to the project you want to configure.
......@@ -25,10 +29,7 @@ This integration requires a [GitHub API token](https://github.com/settings/token
1. Click "GitHub".
1. Select the "Active" checkbox.
1. Paste the token you've generated on GitHub
1. Enter the path to your project on GitHub, such as "https://github.com/your-name/YourProject/"
1. Enter the path to your project on GitHub, such as `https://github.com/username/repository`
1. Save or optionally click "Test Settings".
![Configure GitHub Project Integration](img/github_configuration.png)
[gh-import]: ../import/github.md#mirroring
[gh-integration]: ../../../integration/github.md
......@@ -28,7 +28,7 @@ The following languages and frameworks are supported.
| Language / framework | Scan tool |
|-----------------------|----------------------------------------------------------------------------------------|
| C/C++ | [Flawfinder](https://www.dwheeler.com/flawfinder/) |
| Python | [bandit](https://github.com/openstack/bandit) |
| Python | [bandit](https://github.com/PyCQA/bandit) |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) |
| Java (Maven & Gradle) | [find-sec-bugs](https://find-sec-bugs.github.io/) |
| Go (experimental) | [Go AST Scanner](https://github.com/GoASTScanner/gas) |
......
# Repository mirroring
Repository Mirroring is a way to mirror repositories from external sources.
Repository mirroring is a way to mirror repositories from external sources.
It can be used to mirror all branches, tags, and commits that you have
in your repository.
......@@ -34,7 +34,7 @@ A few things/limitations to consider:
- The Git LFS objects will not be synced. You'll need to push/pull them
manually.
## Use-cases
## Use cases
- You migrated to GitLab but still need to keep your project in another source.
In that case, you can simply set it up to mirror to GitLab (pull) and all the
......@@ -294,11 +294,12 @@ by using the **Update now** button which is exposed in various places:
## Bidirectional mirroring
> **Warning:** There is no bidirectional support without conflicts. If you
> configure a repository to pull and push to a second remote, there is no
> guarantee that it will update correctly on both remotes. If you configure
> a repository for bidirectional mirroring, you should consider when conflicts
> occur who and how they will be resolved.
CAUTION: **Warning:**
There is no bidirectional support without conflicts. If you
configure a repository to pull and push to a second remote, there is no
guarantee that it will update correctly on both remotes. If you configure
a repository for bidirectional mirroring, you should consider when conflicts
occur who and how they will be resolved.
Rewriting any mirrored commit on either remote will cause conflicts and
mirroring to fail. This can be prevented by [only pulling protected branches](
......@@ -318,10 +319,11 @@ custom Git hooks][hooks] on the GitLab server.
### Mirroring with Perforce via GitFusion
> **Warning:** Bidirectional mirroring should not be used as a permanent
> configuration. There is no bidirectional mirroring without conflicts.
> Refer to [Migrating from Perforce Helix][perforce] for alternative migration
> approaches.
CAUTION: **Warning:**
Bidirectional mirroring should not be used as a permanent
configuration. There is no bidirectional mirroring without conflicts.
Refer to [Migrating from Perforce Helix][perforce] for alternative migration
approaches.
GitFusion provides a Git interface to Perforce which can be used by GitLab to
bidirectionally mirror projects with GitLab. This may be useful in some
......@@ -339,10 +341,11 @@ limitations of GitFusion.
[ee-3326]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3326
[ee-3350]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3350
[ee-3453]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/3453
[ee-4559]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4559
[ce-18715]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18715
[perms]: ../user/permissions.md
[hooks]: ../administration/custom_hooks.html
[hooks]: ../administration/custom_hooks.md
[deploy-key]: ../ssh/README.md#deploy-keys
[webhook]: ../user/project/integrations/webhooks.html#push-events
[pull-api]: ../api/projects.html#start-the-pull-mirroring-process-for-a-project
[perforce]: ../user/project/import/perforce.html
[webhook]: ../user/project/integrations/webhooks.md#push-events
[pull-api]: ../api/projects.md#start-the-pull-mirroring-process-for-a-project
[perforce]: ../user/project/import/perforce.md
......@@ -23,7 +23,10 @@ export default {
<template>
<div
class="flex"
:class="{ 'issue-info-container': !canReorder }"
:class="{
'issue-info-container': !canReorder,
'card-body': canReorder,
}"
>
<div class="block-truncated append-right-10">
<a
......
......@@ -93,10 +93,13 @@ export default {
},
mounted() {
if (this.canReorder) {
this.sortable = Sortable.create(this.$refs.list, Object.assign({}, sortableConfig, {
onStart: this.addDraggingCursor,
onEnd: this.reordered,
}));
this.sortable = Sortable.create(
this.$refs.list,
Object.assign({}, sortableConfig, {
onStart: this.addDraggingCursor,
onEnd: this.reordered,
}),
);
}
},
methods: {
......@@ -225,7 +228,7 @@ issue-count-badge-add-button btn btn-sm btn-default"
:class="{
'user-can-drag': canReorder,
'sortable-row': canReorder,
card: canReorder
'card-slim': canReorder
}"
:data-key="issue.id"
:data-epic-issue-id="issue.epic_issue_id"
......
<script>
/**
* Renders DAST body text
* [priority]: [name]
* [severity] ([confidence]): [name]
*/
import ModalOpenName from './modal_open_name.vue';
......@@ -27,7 +27,7 @@ export default {
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text">
<template v-if="issue.priority">{{ issue.priority }}:</template>
{{ issue.severity }} ({{ issue.confidence }}):
<modal-open-name
:issue="issue"
......
......@@ -35,9 +35,20 @@ export default {
this.dismissIssue();
}
},
isLastValue(index, values) {
return index < values.length - 1;
},
hasValue(field) {
return field.value && field.value.length > 0;
},
hasInstances(field, key) {
return key === 'instances' && field.value && field.value.length > 0;
return key === 'instances' && this.hasValue(field);
},
hasIdentifiers(field, key) {
return key === 'identifiers' && this.hasValue(field);
},
hasLinks(field, key) {
return key === 'links' && this.hasValue(field);
},
},
};
......@@ -51,7 +62,7 @@ export default {
<slot>
<div
v-for="(field, key, index) in modal.data"
v-if="field.value || hasInstances(field, key)"
v-if="field.value"
class="row prepend-top-10 append-bottom-10"
:key="index"
>
......@@ -99,6 +110,42 @@ export default {
</li>
</ul>
</div>
<template v-else-if="hasIdentifiers(field, key)">
<span
v-for="(identifier, i) in field.value"
:key="i"
>
<a
:class="`js-link-${key}`"
v-if="identifier.url"
target="_blank"
:href="identifier.url"
rel="noopener noreferrer"
>
{{ identifier.name }}
</a>
<span v-else>
{{ identifier.name }}
</span>
<span v-if="isLastValue(i, field.value)">,&nbsp;</span>
</span>
</template>
<template v-else-if="hasLinks(field, key)">
<span
v-for="(link, i) in field.value"
:key="i"
>
<a
:class="`js-link-${key}`"
target="_blank"
:href="link.url"
rel="noopener noreferrer"
>
{{ link.value || link.url }}
</a>
<span v-if="isLastValue(i, field.value)">,&nbsp;</span>
</span>
</template>
<template v-else>
<a
:class="`js-link-${key}`"
......
......@@ -22,6 +22,6 @@ export default {
@click="handleIssueClick()"
class="btn-link btn-blank text-left break-link vulnerability-name-button"
>
{{ issue.name }}
{{ issue.title }}
</button>
</template>
......@@ -23,7 +23,7 @@ export default {
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text">
<template v-if="issue.priority">{{ issue.priority }}:</template>
<template v-if="issue.severity">{{ issue.severity }}:</template>
<modal-open-name :issue="issue" />
</div>
......
<script>
/**
* Renders SAST body text
* [priority]: [name] in [link] : [line]
* [severity] ([confidence]): [name] in [link] : [line]
*/
import ReportLink from './report_link.vue';
import ModalOpenName from './modal_open_name.vue';
......@@ -25,7 +25,10 @@ export default {
<template>
<div class="report-block-list-issue-description prepend-top-5 append-bottom-5">
<div class="report-block-list-issue-description-text">
<template v-if="issue.priority">{{ issue.priority }}:</template>
<template v-if="issue.severity && issue.confidence">
{{ issue.severity }} ({{ issue.confidence }}):
</template>
<template v-else-if="issue.priority">{{ issue.priority }}:</template>
<modal-open-name :issue="issue" />
</div>
......
......@@ -248,28 +248,30 @@ export default {
},
[types.SET_ISSUE_MODAL_DATA](state, issue) {
state.modal.title = issue.name;
state.modal.title = issue.title;
state.modal.data.description.value = issue.description;
state.modal.data.file.value = issue.file;
state.modal.data.file.value = issue.location && issue.location.file;
state.modal.data.file.url = issue.urlPath;
state.modal.data.className.value = issue.location && issue.location.class;
state.modal.data.methodName.value = issue.location && issue.location.method;
state.modal.data.namespace.value = issue.namespace;
if (issue.identifiers && issue.identifiers.length > 0) {
state.modal.data.identifiers.value = issue.identifiers;
} else {
// Force a null value for identifiers to avoid showing an empty array
state.modal.data.identifiers.value = null;
}
state.modal.data.severity.value = issue.severity;
state.modal.data.confidence.value = issue.confidence;
state.modal.data.solution.value = issue.solution;
state.modal.data.confidenceLevel.value = issue.confidence;
state.modal.data.source.value = issue.source;
state.modal.data.instances.value = issue.instances;
state.modal.vulnerability = issue;
// Link to CVE-ID for Container Scanning
if (issue.nameLink) {
state.modal.data.identifier.value = issue.name;
state.modal.data.identifier.isLink = true;
state.modal.data.identifier.url = issue.nameLink;
if (issue.links && issue.links.length > 0) {
state.modal.data.links.value = issue.links;
} else {
state.modal.data.identifier.value = issue.identifier;
state.modal.data.identifier.isLink = false;
state.modal.data.identifier.url = null;
// Force a null value for links to avoid showing an empty array
state.modal.data.links.value = null;
}
state.modal.data.instances.value = issue.instances;
state.modal.vulnerability = issue;
// clear previous state
state.modal.error = null;
......
......@@ -77,21 +77,30 @@ export default () => ({
text: s__('ciReport|Description'),
isLink: false,
},
identifiers: {
value: [],
text: s__('ciReport|Identifiers'),
isLink: false,
},
file: {
value: null,
url: null,
text: s__('ciReport|File'),
isLink: true,
},
namespace: {
className: {
value: null,
text: s__('ciReport|Namespace'),
text: s__('ciReport|Class'),
isLink: false,
},
identifier: {
methodName: {
value: null,
url: null,
text: s__('ciReport|Identifier'),
text: s__('ciReport|Method'),
isLink: false,
},
namespace: {
value: null,
text: s__('ciReport|Namespace'),
isLink: false,
},
severity: {
......@@ -99,20 +108,20 @@ export default () => ({
text: s__('ciReport|Severity'),
isLink: false,
},
solution: {
confidence: {
value: null,
text: s__('ciReport|Solution'),
text: s__('ciReport|Confidence'),
isLink: false,
},
confidenceLevel: {
solution: {
value: null,
text: s__('ciReport|Confidence Level'),
text: s__('ciReport|Solution'),
isLink: false,
},
source: {
value: null,
text: s__('ciReport|Source'),
isLink: true,
links: {
value: [],
text: s__('ciReport|Links'),
isLink: false,
},
instances: {
value: [],
......
import sha1 from 'sha1';
import _ from 'underscore';
import { stripHtml } from '~/lib/utils/text_utility';
import { n__, s__, sprintf } from '~/locale';
......@@ -12,25 +13,25 @@ export const findIssueIndex = (issues, issue) =>
/**
* Returns given vulnerability enriched with the corresponding
* feedbacks (`dismissal` or `issue` type)
* feedback (`dismissal` or `issue` type)
* @param {Object} vulnerability
* @param {Array} feedbacks
* @param {Array} feedback
*/
function enrichVulnerabilityWithfeedbacks(vulnerability, feedbacks = []) {
return feedbacks.filter(
feedback => feedback.project_fingerprint === vulnerability.project_fingerprint,
).reduce((vuln, feedback) => {
if (feedback.feedback_type === 'dismissal') {
function enrichVulnerabilityWithfeedback(vulnerability, feedback = []) {
return feedback.filter(
fb => fb.project_fingerprint === vulnerability.project_fingerprint,
).reduce((vuln, fb) => {
if (fb.feedback_type === 'dismissal') {
return {
...vuln,
isDismissed: true,
dismissalFeedback: feedback,
dismissalFeedback: fb,
};
} else if (feedback.feedback_type === 'issue') {
} else if (fb.feedback_type === 'issue') {
return {
...vuln,
hasIssue: true,
issueFeedback: feedback,
issueFeedback: fb,
};
}
return vuln;
......@@ -38,107 +39,192 @@ function enrichVulnerabilityWithfeedbacks(vulnerability, feedbacks = []) {
}
/**
* Maps SAST issues:
* { tool: String, message: String, url: String , cve: String ,
* file: String , solution: String, priority: String }
* to contain:
* { name: String, path: String, line: String, urlPath: String, priority: String }
* Generates url to repository file and highlight section between start and end lines.
*
* @param {Object} location
* @param {String} pathPrefix
* @returns {String}
*/
function fileUrl(location, pathPrefix) {
let lineSuffix = '';
if (!_.isEmpty(location.start_line)) {
lineSuffix += `#L${location.start_line}`;
if (!_.isEmpty(location.end_line)) {
lineSuffix += `-${location.end_line}`;
}
}
return `${pathPrefix}/${location.file}${lineSuffix}`;
}
/**
* Parses issues with deprecated JSON format and adapts it to the new one.
*
* @param {Object} issue
* @returns {Object}
*/
function adaptDeprecatedFormat(issue) {
// Skip issue with new format (old format does not have a location property)
if (issue.location) {
return issue;
}
const adapted = {
...issue,
};
// Add the new links property
const links = [];
if (!_.isEmpty(adapted.url)) {
links.push({ url: adapted.url });
}
Object.assign(adapted, {
// Add the new location property
location: {
file: adapted.file,
start_line: adapted.line,
},
links,
});
return adapted;
}
/**
* Parses SAST results into a common format to allow to use the same Vue component.
*
* @param {Array} issues
* @param {Array} feedback
* @param {String} path
* @returns {Array}
*/
export const parseSastIssues = (issues = [], feedbacks = [], path = '') =>
export const parseSastIssues = (issues = [], feedback = [], path = '') =>
issues.map(issue => {
const parsed = {
...issue,
...adaptDeprecatedFormat(issue),
category: 'sast',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(issue.cve),
name: issue.message,
path: issue.file,
urlPath: issue.line ? `${path}/${issue.file}#L${issue.line}` : `${path}/${issue.file}`,
title: issue.message,
};
return {
...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks),
path: parsed.location.file,
urlPath: fileUrl(parsed.location, path),
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
});
/**
* Maps Dependency scanning issues:
* { tool: String, message: String, url: String , cve: String ,
* file: String , solution: String, priority: String }
* to contain:
* { name: String, path: String, line: String, urlPath: String, priority: String }
* Parses Dependency Scanning results into a common format to allow to use the same Vue component.
*
* @param {Array} issues
* @param {Array} feedback
* @param {String} path
* @returns {Array}
*/
export const parseDependencyScanningIssues = (issues = [], feedbacks = [], path = '') =>
export const parseDependencyScanningIssues = (issues = [], feedback = [], path = '') =>
issues.map(issue => {
const parsed = {
...issue,
...adaptDeprecatedFormat(issue),
category: 'dependency_scanning',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(issue.cve || issue.message),
name: issue.message,
path: issue.file,
urlPath: issue.line ? `${path}/${issue.file}#L${issue.line}` : `${path}/${issue.file}`,
title: issue.message,
};
return {
...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks),
path: parsed.location.file,
urlPath: fileUrl(parsed.location, path),
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
});
/**
* Parses Sast Container results into a common format to allow to use the same Vue component
* And adds an external link
* Parses Container Scanning results into a common format to allow to use the same Vue component.
* Container Scanning report is currently the straigh output from the underlying tool
* (clair scanner) hence the formatting happenning here.
*
* @param {Array} data
* @param {Array} issues
* @param {Array} feedback
* @param {String} path
* @returns {Array}
*/
export const parseSastContainer = (issues = [], feedbacks = []) =>
export const parseSastContainer = (issues = [], feedback = []) =>
issues.map(issue => {
const parsed = {
...issue,
category: 'container_scanning',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`),
name: issue.vulnerability,
priority: issue.severity,
title: issue.vulnerability,
description: !_.isEmpty(issue.description) ? issue.description :
sprintf(s__('ciReport|%{namespace} is affected by %{vulnerability}.'), {
namespace: issue.namespace,
vulnerability: issue.vulnerability,
}),
path: issue.namespace,
// external link to provide better description
nameLink: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`,
identifiers: [{
type: 'CVE',
name: issue.vulnerability,
value: issue.vulnerability,
url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`,
}],
};
// Generate solution
if (!_.isEmpty(issue.fixedby) &&
!_.isEmpty(issue.featurename) &&
!_.isEmpty(issue.featureversion)
) {
Object.assign(parsed, {
solution: sprintf(s__('ciReport|Upgrade %{name} from %{version} to %{fixed}.'), {
name: issue.featurename,
version: issue.featureversion,
fixed: issue.fixedby,
}),
});
}
return {
...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks),
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
});
export const parseDastIssues = (issues = [], feedbacks = []) =>
/**
* Parses DAST into a common format to allow to use the same Vue component.
* DAST report is currently the straigh output from the underlying tool (ZAProxy)
* hence the formatting happenning here.
*
* @param {Array} issues
* @param {Array} feedback
* @returns {Array}
*/
export const parseDastIssues = (issues = [], feedback = []) =>
issues.map(issue => {
const parsed = {
...issue,
category: 'dast',
// TODO: replace with issue.project_fingerprint
project_fingerprint: sha1(issue.pluginid),
parsedDescription: stripHtml(issue.desc, ' '),
priority: issue.riskdesc,
solution: stripHtml(issue.solution, ' '),
title: issue.name,
description: stripHtml(issue.desc, ' '),
solution: stripHtml(issue.solution, ' '),
};
if (issue.cweid && issue.cweid !== '') {
if (!_.isEmpty(issue.cweid)) {
Object.assign(parsed, {
identifier: `CWE-${issue.cweid}`,
identifiers: [{
type: 'CWE',
name: `CWE-${issue.cweid}`,
value: issue.cweid,
url: `https://cwe.mitre.org/data/definitions/${issue.cweid}.html`,
}],
});
}
if (issue.riskdesc && issue.riskdesc !== '') {
// Split 'severity (confidence)'
// Split riskdesc into severity and confidence.
// Riskdesc format is: "severity (confidence)"
const [, severity, confidence] = issue.riskdesc.match(/(.*) \((.*)\)/);
Object.assign(parsed, {
severity,
......@@ -148,7 +234,7 @@ export const parseDastIssues = (issues = [], feedbacks = []) =>
return {
...parsed,
...enrichVulnerabilityWithfeedbacks(parsed, feedbacks),
...enrichVulnerabilityWithfeedback(parsed, feedback),
};
});
......
......@@ -109,9 +109,22 @@ class Projects::VulnerabilityFeedbackController < Projects::ApplicationControlle
method
uri
],
location: %i[
file
start_line
end_line
class
method
],
identifiers: %i[
type
name
value
url
],
links: %i[
name
url
]
]
end
......
......@@ -52,6 +52,8 @@ class EpicsFinder < IssuableFinder
private
def groups_user_can_read_epics(groups)
groups = Gitlab::GroupPlansPreloader.new.preload(groups)
DeclarativePolicy.user_scope do
groups.select { |g| Ability.allowed?(current_user, :read_epic, g) }
end
......
module EE
module SearchHelper
extend ::Gitlab::Utils::Override
def search_filter_input_options(type)
options = super
options[:data][:'multiple-assignees'] = 'true' if search_multiple_assignees?(type)
......@@ -7,6 +9,29 @@ module EE
options
end
override :find_project_for_result_blob
def find_project_for_result_blob(result)
super || ::Project.find(result['_parent'])
end
override :parse_search_result
def parse_search_result(result)
return super if result.is_a?(Array)
blob = ::Gitlab::Elastic::SearchResults.parse_search_result(result)
[blob.filename, blob]
end
override :search_blob_title
def search_blob_title(project, file_name)
if @project
file_name
else
(project.full_name + ': ' + content_tag(:i, file_name)).html_safe
end
end
private
def search_multiple_assignees?(type)
......
......@@ -8,7 +8,8 @@ module EE
'epic_issue_moved' => 'issues',
'issue_added_to_epic' => 'epic',
'issue_removed_from_epic' => 'epic',
'issue_changed_epic' => 'epic'
'issue_changed_epic' => 'epic',
'epic_date_changed' => 'calendar'
}.freeze
override :system_note_icon_name
......
......@@ -70,13 +70,13 @@ module EE
attr_encrypted :external_auth_client_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm',
encode: true
attr_encrypted :external_auth_client_key_pass,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm',
encode: true
end
......
......@@ -155,6 +155,19 @@ module EE
actual_plan&.pipeline_size_limit.to_i
end
def memoized_plans=(plans)
@plans = plans # rubocop: disable Gitlab/ModuleWithInstanceVariables
end
def plans
@plans ||=
if parent_id
Plan.where(id: self_and_ancestors.with_plan.reorder(nil).select(:plan_id))
else
Array(plan)
end
end
private
def validate_plan_name
......@@ -180,14 +193,5 @@ module EE
globally_available
end
end
def plans
@plans ||=
if parent_id
Plan.where(id: self_and_ancestors.with_plan.reorder(nil).select(:plan_id))
else
Array(plan)
end
end
end
end
......@@ -5,7 +5,7 @@ module EE
EE_ICON_TYPES = %w[
weight approved unapproved relate unrelate
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
epic_issue_moved issue_changed_epic
epic_issue_moved issue_changed_epic epic_date_changed
].freeze
override :icon_types
......
......@@ -42,7 +42,7 @@ class GeoNode < ActiveRecord::Base
scope :with_url_prefix, ->(prefix) { where('url LIKE ?', "#{prefix}%") }
attr_encrypted :secret_access_key,
key: Settings.attr_encrypted_db_key_base,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm',
mode: :per_attribute_iv,
encode: true
......
......@@ -8,10 +8,21 @@ module EE
def execute(_issuable, _old_labels)
super
handle_weight_change_note
handle_date_change_note
end
private
def handle_date_change_note
if issuable.previous_changes.include?('start_date')
::SystemNoteService.change_epic_date_note(issuable, current_user, 'start date', issuable['start_date'])
end
if issuable.previous_changes.include?('end_date')
::SystemNoteService.change_epic_date_note(issuable, current_user, 'finish date', issuable['end_date'])
end
end
def handle_weight_change_note
if issuable.previous_changes.include?('weight')
create_weight_change_note
......
......@@ -24,5 +24,27 @@ module EE
body = noteable.weight ? "changed weight to **#{noteable.weight}**," : 'removed the weight'
create_note(NoteSummary.new(noteable, project, author, body, action: 'weight'))
end
# Called when the start or end date of an Issuable is changed
#
# noteable - Noteable object
# author - User performing the change
# date_type - 'start date' or 'finish date'
# date - New date
#
# Example Note text:
#
# "changed start date to FIXME"
#
# Returns the created Note object
def change_epic_date_note(noteable, author, date_type, date)
body = if date
"changed #{date_type} to #{date.strftime('%b %-d, %Y')}"
else
"removed the #{date_type}"
end
create_note(NoteSummary.new(noteable, nil, author, body, action: 'epic_date_changed'))
end
end
end
......@@ -2,14 +2,14 @@ module Issues
class CreateFromVulnerabilityDataService < ::BaseService
def execute
vulnerability = case @params[:category]
when 'sast', 'dependency_scanning'
when 'sast', 'dependency_scanning', 'dast'
Gitlab::Vulnerabilities::StandardVulnerability.new(params)
when 'container_scanning'
Gitlab::Vulnerabilities::ContainerScanningVulnerability.new(params)
when 'dast'
Gitlab::Vulnerabilities::DastVulnerability.new(params)
end
return error('Invalid vulnerability category') unless vulnerability
issue_params = {
title: "Investigate vulnerability: #{vulnerability.title}",
description: render_description(vulnerability)
......
......@@ -7,8 +7,8 @@
- ineligible_approver = issuable.author || current_user
- can_update_approvers = can?(current_user, :update_approvers, issuable)
.form-group
= form.label :approver_ids, class: 'col-form-label' do
.form-group.row
= form.label :approver_ids, class: 'col-form-label col-sm-2' do
Approvers
.col-sm-10
- if can_update_approvers
......@@ -61,7 +61,7 @@
Remove
.col-sm-12
.form-group
.form-group.row
= form.label :approvals_before_merge, class: 'label-light' do
Approvals required
= form.number_field :approvals_before_merge, class: 'form-control', value: issuable.approvals_required, readonly: !can_update_approvers
......
......@@ -19,11 +19,22 @@
### Identifiers:
<% vulnerability.identifiers.each do |identifier| %>
<% if identifier[:link].present? %>
* [<%= identifier[:value] %>](<%= identifier[:link] %>)
<% if identifier[:url].present? %>
* [<%= identifier[:name] %>](<%= identifier[:url] %>)
<% else %>
* <%= identifier[:value] %>
* <%= identifier[:name] %>
<% end %>
<% end %>
<% end %>
<% if vulnerability.links.present? %>
### Links:
<% vulnerability.links.each do |link| %>
<% if link[:name].present? %>
* [<%= link[:name] %>](<%= link[:url] %>)
<% else %>
* <%= link[:url] %>
<% end %>
<% end %>
<% end %>
---
title: Enrich Security Reports with more data
merge_request: 5878
author:
type: changed
---
title: "[Geo] Fix: Deleted project events may be skipped on the secondary when selective
sync is used"
merge_request:
author:
type: fixed
---
title: Create system note on epic date change.
merge_request:
author:
type: added
---
title: Preload Group plans in EpicsFinder
merge_request:
author:
type: performance
module EE
module Gitlab
module SlashCommands
module Command
extend ActiveSupport::Concern
class_methods do
extend ::Gitlab::Utils::Override
override :commands
def commands
super.concat(
[
::Gitlab::SlashCommands::Run
]
)
end
end
end
end
end
end
module EE
module Gitlab
module SlashCommands
module Presenters
module IssueBase
extend ::Gitlab::Utils::Override
override :fields
def fields
super.concat(
[
{
title: "Weight",
value: resource.weight? ? resource.weight : "_None_",
short: true
}
]
)
end
end
end
end
end
end
......@@ -82,6 +82,9 @@ module Gitlab
def can_replay?(event_log)
return true if event_log.project_id.nil?
# Always replay events for deleted projects
return true unless Project.exists?(event_log.project_id)
Gitlab::Geo.current_node&.projects_include?(event_log.project_id)
end
......
# frozen_string_literal: true
module Gitlab
# Preloading of Plans for one or more groups.
#
# This class can be used to efficiently preload the plans of a given list of
# groups, including any plans the groups may have access to based on their
# parent groups.
class GroupPlansPreloader
# Preloads all the plans for the given Groups.
#
# groups - An ActiveRecord::Relation returning a set of Group instances.
#
# Returns an Array containing all the Groups, including their preloaded
# plans.
def preload(groups)
groups_and_ancestors = groups_and_ancestors_for(groups)
# A Hash mapping group IDs to their corresponding Group instances.
groups_map = groups_and_ancestors.each_with_object({}) do |group, hash|
hash[group.id] = group
end
all_plan_ids = Set.new
# A Hash that for every group ID maps _all_ the plan IDs this group has
# access to.
plans_map = groups_and_ancestors
.each_with_object(Hash.new { |h, k| h[k] = [] }) do |group, hash|
current = group
while current
if (plan_id = current.plan_id)
hash[group.id] << plan_id
all_plan_ids << plan_id
end
current = groups_map[current.parent_id]
end
end
# Grab all the plans for all the Groups, using only a single query.
plans = Plan
.where(id: all_plan_ids.to_a)
.each_with_object({}) do |plan, hash|
hash[plan.id] = plan
end
# Assign all the plans to the groups that have access to them.
groups.each do |group|
group.memoized_plans = plans_map[group.id].map { |id| plans[id] }
end
end
# Returns an ActiveRecord::Relation that includes the given groups, and all
# their (recursive) ancestors.
def groups_and_ancestors_for(groups)
Gitlab::GroupHierarchy
.new(groups)
.base_and_ancestors
.select(:id, :parent_id, :plan_id)
end
end
end
......@@ -16,7 +16,7 @@ module Gitlab
end
def commands
Gitlab::SlashCommands::Command::COMMANDS
Gitlab::SlashCommands::Command.commands
end
end
end
......
......@@ -15,23 +15,12 @@ module Gitlab
confidence
solution
identifiers
links
].each do |method_name|
define_method(method_name) do
raise NotImplementedError
end
end
protected
# cve_id must be 'CVE-YYYY-XXXX' (prefix + year + digits)
def cve_link(cve_id)
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=#{cve_id}"
end
# cve_id must be a number only (no 'CWE-' prefix)
def cwe_link(cwe_id)
"https://cwe.mitre.org/data/definitions/#{cwe_id}.html"
end
end
end
end
......@@ -2,24 +2,24 @@ module Gitlab
module Vulnerabilities
class ContainerScanningVulnerability < BaseVulnerability
def title
"#{@data[:name]} in #{@data[:namespace]}"
"#{@data[:vulnerability]} in #{@data[:namespace]}"
end
# Passthrough properties
%i[
confidence
severity
identifiers
links
].each do |method_name|
define_method(method_name) do
@data[method_name]
end
end
def confidence
end
def description
@data[:description].presence ||
"**#{@data[:namespace]}** is affected by #{@data[:name]}"
"**#{@data[:namespace]}** is affected by #{@data[:vulnerability]}"
end
def solution
......@@ -30,13 +30,6 @@ module Gitlab
"Upgrade **#{@data[:featurename]}** from `#{@data[:featureversion]}` to `#{@data[:fixedby]}`"
end
end
def identifiers
[{
value: @data[:name],
link: cve_link(@data[:name])
}]
end
end
end
end
module Gitlab
module Vulnerabilities
class DastVulnerability < BaseVulnerability
def title
@data[:name]
end
# Passthrough properties
%i[
severity
confidence
solution
].each do |method_name|
define_method(method_name) do
@data[method_name]
end
end
def description
@data[:desc]
end
def identifiers
ids = []
if @data[:cweid].present?
ids << {
value: "CWE-#{@data[:cweid]}",
link: cwe_link(@data[:cweid])
}
end
if @data[:wascid].present?
ids << {
value: "WASC-#{@data[:wascid]}"
}
end
ids
end
end
end
end
module Gitlab
module Vulnerabilities
class StandardVulnerability < BaseVulnerability
def title
@data[:name]
end
# Passthrough properties
%i[
title
severity
confidence
solution
identifiers
links
].each do |method_name|
define_method(method_name) do
@data[method_name]
......@@ -17,30 +16,7 @@ module Gitlab
end
def description
@data[:description].presence || @data[:name]
end
def identifiers
return [] unless @data[:identifiers].present?
ids = []
@data[:identifiers].each do |identifier|
# Only show known identifiers
case identifier[:name]
when 'CVE'
ids << {
value: identifier[:value],
link: cve_link(identifier[:value])
}
when 'CWE'
ids << {
value: "CWE-#{identifier[:value]}",
link: cwe_link(identifier[:value])
}
end
end
ids
@data[:description].presence || @data[:title]
end
end
end
......
......@@ -86,18 +86,37 @@ describe Projects::VulnerabilityFeedbackController do
feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'sast',
project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8',
vulnerability_data: {
priority: 'Low', line: '41',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
category: 'sast',
severity: 'Low',
confidence: 'Medium',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator',
title: 'Predictable pseudorandom number generator',
description: 'Description of Predictable pseudorandom number generator',
tool: 'find_sec_bugs'
tool: 'find_sec_bugs',
location: {
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
start_line: '41'
},
identifiers: [{
type: 'CVE',
name: 'CVE-2018-1234',
value: 'CVE-2018-1234',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1234'
}],
links: [{
name: 'Awesome-security blog post',
url: 'https;//example.com/blog-post'
}]
}
}
end
context 'with valid params' do
it 'returns the created list' do
it 'returns the created feedback' do
allow(VulnerabilityFeedbackModule::CreateService)
.to receive(:new).with(project, user, create_params)
.and_call_original
create_feedback user: user, project: project, params: create_params
expect(response).to match_response_schema('vulnerability_feedback', dir: 'ee')
......@@ -142,7 +161,7 @@ describe Projects::VulnerabilityFeedbackController do
def create_feedback(user:, project:, params:)
sign_in(user)
post :create, namespace_id: project.namespace.to_param, project_id: project, vulnerability_feedback: params
post :create, namespace_id: project.namespace.to_param, project_id: project, vulnerability_feedback: params, format: :json
end
end
......
......@@ -53,6 +53,12 @@ describe EpicsFinder do
expect(epics).to contain_exactly(epic1, epic2, epic3)
end
it 'does not execute more than 7 SQL queries' do
amount = ActiveRecord::QueryRecorder.new { epics.to_a }.count
expect(amount).to be <= 7
end
context 'by created_at' do
it 'returns all epics created before the given date' do
expect(epics(created_before: 2.days.ago)).to contain_exactly(epic1, epic2)
......@@ -97,6 +103,24 @@ describe EpicsFinder do
it 'returns all epics that belong to the given group and its subgroups' do
expect(epics).to contain_exactly(epic1, epic2, epic3, subepic1, subepic2)
end
it 'does not execute more than 9 SQL queries' do
amount = ActiveRecord::QueryRecorder.new { epics.to_a }.count
expect(amount).to be <= 9
end
it 'does not execute more than 11 SQL queries when checking namespace plans' do
allow(Gitlab::CurrentSettings)
.to receive(:should_check_namespace_plan?)
.and_return(true)
group.update(plan: create(:gold_plan))
amount = ActiveRecord::QueryRecorder.new { epics.to_a }.count
expect(amount).to be <= 10
end
end
context 'by timeframe' do
......
......@@ -25,7 +25,7 @@ describe SearchHelper do
options: { highlight: true }
)[:blobs][:results][0]
parsed_result = helper.parse_search_result(result)
_, parsed_result = helper.parse_search_result(result)
expect(parsed_result.ref). to eq('b83d6e391c22777fca1ed3012fce84f633d7fed0')
expect(parsed_result.filename).to eq('files/ruby/popen.rb')
......
......@@ -35,6 +35,19 @@ describe Gitlab::Geo::LogCursor::Events::RepositoryDeletedEvent, :postgresql, :c
it 'removes the tracking entry' do
expect { subject.process }.to change(Geo::ProjectRegistry, :count).by(-1)
end
context 'when selective sync is enabled' do
let(:secondary) { create(:geo_node, selective_sync_type: 'namespaces', namespaces: [project.namespace]) }
it 'replays delete events when project does not exist on primary' do
project.delete
expect(::GeoRepositoryDestroyWorker).to receive(:perform_async)
.with(project.id, deleted_project_name, deleted_path, project.repository_storage)
subject.process
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::GroupPlansPreloader do
describe '#preload' do
let!(:plan1) { create(:free_plan, name: 'plan-1') }
let!(:plan2) { create(:free_plan, name: 'plan-2') }
let(:preloaded_groups) do
# We don't use the factory objects here because they might have the plan
# loaded already (as we specify the plan when creating them).
described_class.new.preload(Group.order(id: :asc))
end
before do
group1 = create(:group, name: 'group-1', plan_id: plan1.id)
create(:group, name: 'group-2', plan_id: plan2.id)
create(:group, name: 'group-3', parent: group1)
end
it 'only executes three SQL queries to preload the data' do
amount = ActiveRecord::QueryRecorder
.new { preloaded_groups }
.count
# One query to get the groups and their ancestors, one query to get their
# plans, and one query to _just_ get the groups.
expect(amount).to eq(3)
end
it 'associates the correct plans with the correct groups' do
expect(preloaded_groups[0].plans).to eq([plan1])
expect(preloaded_groups[1].plans).to eq([plan2])
expect(preloaded_groups[2].plans).to eq([plan1])
end
it 'does not execute any queries for preloaded plans' do
preloaded_groups
amount = ActiveRecord::QueryRecorder
.new { preloaded_groups.each(&:plans) }
.count
expect(amount).to be_zero
end
end
end
require 'spec_helper'
describe Gitlab::SlashCommands::Command do
describe '.commands' do
it 'includes EE specific commands' do
expect(described_class.commands).to include(Gitlab::SlashCommands::Run)
end
end
end
require 'spec_helper'
describe Gitlab::SlashCommands::Presenters::IssueShow do
let(:project) { create(:project) }
let(:attachment) { subject[:attachments].first }
subject { described_class.new(issue).present }
context 'issue with issue weight' do
let(:issue) { create(:issue, project: project, weight: 3) }
let(:weight_attachment) { attachment[:fields].find { |a| a[:title] == "Weight" } }
it 'shows the weight' do
expect(weight_attachment).not_to be_nil
expect(weight_attachment[:value]).to be(3)
end
end
end
......@@ -7,5 +7,24 @@ describe Issuable::CommonSystemNotesService do
describe '#execute' do
it_behaves_like 'system note creation', { weight: 5 }, 'changed weight to **5**,'
context 'when issuable is an epic' do
let(:timestamp) { Time.now }
let(:issuable) { create(:epic, end_date: timestamp) }
subject { described_class.new(nil, user).execute(issuable, [])}
before do
issuable.assign_attributes(start_date: timestamp, end_date: nil)
issuable.save
end
it 'creates 2 system notes with the correct content' do
expect { subject }.to change { Note.count }.from(0).to(2)
expect(Note.first.note).to match("changed start date to #{timestamp.strftime('%b %-d, %Y')}")
expect(Note.second.note).to match('removed the finish date')
end
end
end
end
......@@ -29,19 +29,34 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
let(:params) do
{
category: 'sast',
priority: 'Low', line: '41',
severity: 'Low', confidence: 'High',
solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator',
title: 'Predictable pseudorandom number generator',
description: 'Description of Predictable pseudorandom number generator',
tool: 'find_sec_bugs',
identifiers: [
{ name: 'CVE', value: 'CVE-2017-15650' },
{ name: 'CWE', value: '16' },
{ name: 'GAS_RULE_ID', value: 'G105' }
]
identifiers: [{
type: 'CVE',
name: 'CVE-2017-15650',
value: 'CVE-2017-15650',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}, {
type: 'CWE',
name: 'CWE-16',
value: '16',
url: 'https://cwe.mitre.org/data/definitions/16.html'
}, {
type: 'GAS_RULE_ID',
name: 'GAS Rule ID G105',
value: 'G105'
}],
links: [{
name: 'Awesome-security blog post',
url: 'https;//example.com/blog-post'
}, {
url: 'https://example.com/another-link'
}]
}
end
......@@ -63,6 +78,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
* [CWE-16](https://cwe.mitre.org/data/definitions/16.html)
* GAS Rule ID G105
### Links:
* [Awesome-security blog post](https;//example.com/blog-post)
* https://example.com/another-link
DESC
end
......@@ -73,16 +94,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
let(:params) do
{
category: 'sast',
priority: 'Low', line: '41',
severity: 'Low', confidence: 'High',
solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator',
tool: 'find_sec_bugs',
identifiers: [
{ name: 'CVE', value: 'CVE-2017-15650' }
]
title: 'Predictable pseudorandom number generator',
tool: 'find_sec_bugs'
}
end
let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' }
......@@ -98,10 +115,6 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
### Solution:
Please do something!
### Identifiers:
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
DESC
end
......@@ -114,19 +127,34 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
let(:params) do
{
category: 'dependency_scanning',
priority: 'Low', line: '41',
severity: 'Low', confidence: 'High',
solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator',
title: 'Predictable pseudorandom number generator',
description: 'Description of Predictable pseudorandom number generator',
tool: 'find_sec_bugs',
identifiers: [
{ name: 'CVE', value: 'CVE-2017-15650' },
{ name: 'CWE', value: '16' },
{ name: 'GAS_RULE_ID', value: 'G105' }
]
identifiers: [{
type: 'CVE',
name: 'CVE-2017-15650',
value: 'CVE-2017-15650',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}, {
type: 'CWE',
name: 'CWE-16',
value: '16',
url: 'https://cwe.mitre.org/data/definitions/16.html'
}, {
type: 'GAS_RULE_ID',
name: 'GAS Rule ID G105',
value: 'G105'
}],
links: [{
name: 'Awesome-security blog post',
url: 'https;//example.com/blog-post'
}, {
url: 'https://example.com/another-link'
}]
}
end
let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' }
......@@ -147,6 +175,12 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
* [CWE-16](https://cwe.mitre.org/data/definitions/16.html)
* GAS Rule ID G105
### Links:
* [Awesome-security blog post](https;//example.com/blog-post)
* https://example.com/another-link
DESC
end
......@@ -162,11 +196,8 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
solution: 'Please do something!',
file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
name: 'Predictable pseudorandom number generator',
tool: 'find_sec_bugs',
identifiers: [
{ name: 'CVE', value: 'CVE-2017-15650' }
]
title: 'Predictable pseudorandom number generator',
tool: 'find_sec_bugs'
}
end
let(:expected_title) { 'Investigate vulnerability: Predictable pseudorandom number generator' }
......@@ -182,10 +213,6 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
### Solution:
Please do something!
### Identifiers:
* [CVE-2017-15650](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650)
DESC
end
......@@ -204,10 +231,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
featurename: 'musl',
featureversion: '1.1.14-r15',
fixedby: '1.1.14-r16',
name: 'CVE-2017-15650',
title: 'CVE-2017-15650',
vulnerability: 'CVE-2017-15650',
description: 'This is a description for CVE-2017-15650.',
tool: 'find_sec_bugs'
tool: 'find_sec_bugs',
identifiers: [{
type: 'CVE',
name: 'CVE-2017-15650',
value: 'CVE-2017-15650',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}]
}
end
let(:expected_title) { 'Investigate vulnerability: CVE-2017-15650 in alpine:v3.4' }
......@@ -241,10 +274,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
featurename: 'musl',
featureversion: '1.1.14-r15',
fixedby: '1.1.14-r16',
name: 'CVE-2017-15650',
title: 'CVE-2017-15650',
vulnerability: 'CVE-2017-15650',
description: '',
tool: 'find_sec_bugs'
tool: 'find_sec_bugs',
identifiers: [{
type: 'CVE',
name: 'CVE-2017-15650',
value: 'CVE-2017-15650',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15650'
}]
}
end
let(:expected_title) { 'Investigate vulnerability: CVE-2017-15650 in alpine:v3.4' }
......@@ -276,11 +315,22 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
category: 'dast',
priority: 'Low',
severity: 'Low',
name: 'X-Content-Type-Options Header Missing',
desc: 'The Anti-MIME-Sniffing header X-Content-Type-Options was not set to nosniff.',
title: 'X-Content-Type-Options Header Missing',
desc: '<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to nosniff.</p>',
description: 'The Anti-MIME-Sniffing header X-Content-Type-Options was not set to nosniff.',
cweid: '123',
wascid: '456',
solution: 'Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to nosniff for all web pages.'
solution: 'Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to nosniff for all web pages.',
identifiers: [{
type: 'CWE',
name: 'CWE-123',
value: '123',
url: 'https://cwe.mitre.org/data/definitions/123.html'
}, {
type: 'WASC',
name: 'WASC-456',
value: '456'
}]
}
end
let(:expected_title) { 'Investigate vulnerability: X-Content-Type-Options Header Missing' }
......@@ -306,4 +356,16 @@ describe Issues::CreateFromVulnerabilityDataService, '#execute' do
it_behaves_like 'a created issue'
end
end
context 'when params are invalid' do
context 'when category is unknown' do
let(:params) { { category: 'foo' } }
let(:result) { described_class.new(project, user, params).execute }
it 'return expected error' do
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq('Invalid vulnerability category')
end
end
end
end
......@@ -21,20 +21,28 @@ describe SystemNoteService do
expect(subject).to be_system
expect(subject.noteable).to eq expected_noteable
expect(subject.project).to eq project
expect(subject.author).to eq author
expect(subject.system_note_metadata.action).to eq(action)
expect(subject.system_note_metadata.commit_count).to eq(commit_count)
end
end
shared_examples_for 'a project system note' do
it 'has the project attribute set' do
expect(subject.project).to eq project
end
it_behaves_like 'a system note'
end
describe '.change_weight_note' do
context 'when weight changed' do
let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum', weight: 4) }
subject { described_class.change_weight_note(noteable, project, author) }
it_behaves_like 'a system note' do
it_behaves_like 'a project system note' do
let(:action) { 'weight' }
end
......@@ -48,7 +56,7 @@ describe SystemNoteService do
subject { described_class.change_weight_note(noteable, project, author) }
it_behaves_like 'a system note' do
it_behaves_like 'a project system note' do
let(:action) { 'weight' }
end
......@@ -57,4 +65,36 @@ describe SystemNoteService do
end
end
end
describe '.change_epic_date_note' do
let(:timestamp) { Time.now }
context 'when start date was changed' do
let(:noteable) { create(:epic) }
subject { described_class.change_epic_date_note(noteable, author, 'start date', timestamp) }
it_behaves_like 'a system note' do
let(:action) { 'epic_date_changed' }
end
it 'sets the note text' do
expect(subject.note).to eq "changed start date to #{timestamp.strftime('%b %-d, %Y')}"
end
end
context 'when start date was removed' do
let(:noteable) { create(:epic, start_date: timestamp) }
subject { described_class.change_epic_date_note(noteable, author, 'start date', nil) }
it_behaves_like 'a system note' do
let(:action) { 'epic_date_changed' }
end
it 'sets the note text' do
expect(subject.note).to eq 'removed the start date'
end
end
end
end
......@@ -42,9 +42,7 @@ module API
end
case params[:scope]
when 'wiki_blobs'
paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob, user_project) }
when 'blobs'
when 'blobs', 'wiki_blobs'
paginate(results).map { |blob| blob[1] }
else
paginate(results)
......
......@@ -32,17 +32,13 @@ module Gitlab
end
def find_by_filename(query, except: [])
filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
filenames = search_filenames(query, except)
blob_refs = filenames.map { |filename| [ref, filename] }
blobs = Gitlab::Git::Blob.batch(repository, blob_refs, blob_size_limit: 1024)
blobs.map do |blob|
blobs(filenames).map do |blob|
Gitlab::SearchResults::FoundBlob.new(
id: blob.id,
filename: blob.path,
basename: File.basename(blob.path),
basename: File.basename(blob.path, File.extname(blob.path)),
ref: ref,
startline: 1,
data: blob.data,
......@@ -50,5 +46,21 @@ module Gitlab
)
end
end
def search_filenames(query, except)
filenames = repository.search_files_by_name(query, ref).first(BATCH_SIZE)
filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
filenames
end
def blob_refs(filenames)
filenames.map { |filename| [ref, filename] }
end
def blobs(filenames)
Gitlab::Git::Blob.batch(repository, blob_refs(filenames), blob_size_limit: 1024)
end
end
end
......@@ -106,7 +106,8 @@ module Gitlab
project_wiki = ProjectWiki.new(project)
unless project_wiki.empty?
project_wiki.search_files(query)
ref = repository_ref || project.wiki.default_branch
Gitlab::WikiFileFinder.new(project, ref).find(query)
else
[]
end
......
module Gitlab
module SlashCommands
class Command < BaseCommand
COMMANDS = [
Gitlab::SlashCommands::IssueShow,
Gitlab::SlashCommands::IssueNew,
Gitlab::SlashCommands::IssueSearch,
Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::Deploy,
Gitlab::SlashCommands::Run
].freeze
prepend EE::Gitlab::SlashCommands::Command
def self.commands
[
Gitlab::SlashCommands::IssueShow,
Gitlab::SlashCommands::IssueNew,
Gitlab::SlashCommands::IssueSearch,
Gitlab::SlashCommands::IssueMove,
Gitlab::SlashCommands::Deploy
]
end
def execute
command, match = match_command
......@@ -38,7 +41,7 @@ module Gitlab
private
def available_commands
COMMANDS.select do |klass|
self.class.commands.keep_if do |klass|
klass.available?(project)
end
end
......
......@@ -2,6 +2,8 @@ module Gitlab
module SlashCommands
module Presenters
module IssueBase
prepend EE::Gitlab::SlashCommands::Presenters::IssueBase
def color(issuable)
issuable.open? ? '#38ae67' : '#d22852'
end
......@@ -34,11 +36,6 @@ module Gitlab
title: "Labels",
value: resource.labels.any? ? resource.label_names.join(', ') : "_None_",
short: true
},
{
title: "Weight",
value: resource.weight? ? resource.weight : "_None_",
short: true
}
]
end
......
module Gitlab
class WikiFileFinder < FileFinder
attr_reader :repository
def initialize(project, ref)
@project = project
@ref = ref
@repository = project.wiki.repository
end
private
def search_filenames(query, except)
safe_query = Regexp.escape(query.tr(' ', '-'))
safe_query = Regexp.new(safe_query, Regexp::IGNORECASE)
filenames = repository.ls_files(ref)
filenames.delete_if { |filename| except.include?(filename) } unless except.empty?
filenames.grep(safe_query).first(BATCH_SIZE)
end
end
end
......@@ -5922,6 +5922,9 @@ msgstr ""
msgid "ciReport|%{linkStartTag}Learn more about SAST image %{linkEndTag}"
msgstr ""
msgid "ciReport|%{namespace} is affected by %{vulnerability}."
msgstr ""
msgid "ciReport|%{reportName} is loading"
msgstr ""
......@@ -5937,10 +5940,13 @@ msgstr ""
msgid "ciReport|%{type} detected no vulnerabilities"
msgstr ""
msgid "ciReport|Class"
msgstr ""
msgid "ciReport|Code quality"
msgstr ""
msgid "ciReport|Confidence Level"
msgid "ciReport|Confidence"
msgstr ""
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
......@@ -5991,7 +5997,7 @@ msgstr ""
msgid "ciReport|Fixed:"
msgstr ""
msgid "ciReport|Identifier"
msgid "ciReport|Identifiers"
msgstr ""
msgid "ciReport|Instances"
......@@ -6000,9 +6006,15 @@ msgstr ""
msgid "ciReport|Learn more about whitelisting"
msgstr ""
msgid "ciReport|Links"
msgstr ""
msgid "ciReport|Loading %{reportName} report"
msgstr ""
msgid "ciReport|Method"
msgstr ""
msgid "ciReport|Namespace"
msgstr ""
......@@ -6045,9 +6057,6 @@ msgstr ""
msgid "ciReport|Solution"
msgstr ""
msgid "ciReport|Source"
msgstr ""
msgid "ciReport|Static Application Security Testing (SAST) detects known vulnerabilities in your source code."
msgstr ""
......@@ -6069,6 +6078,9 @@ msgstr ""
msgid "ciReport|Unapproved vulnerabilities (red) can be marked as approved."
msgstr ""
msgid "ciReport|Upgrade %{name} from %{version} to %{fixed}."
msgstr ""
msgid "ciReport|no vulnerabilities"
msgstr ""
......
......@@ -82,7 +82,7 @@ describe('DropdownValueComponent', () => {
});
it('renders label element with tooltip and styles based on label details', () => {
const labelEl = vm.$el.querySelector('a span.label.color-label');
const labelEl = vm.$el.querySelector('a span.badge.color-label');
expect(labelEl).not.toBeNull();
expect(labelEl.dataset.placement).toBe('bottom');
expect(labelEl.dataset.container).toBe('body');
......
......@@ -8,15 +8,13 @@ describe('dast issue body', () => {
const Component = Vue.extend(component);
const dastIssue = {
alert: 'X-Content-Type-Options Header Missing',
confidence: '2',
severity: 'Low',
confidence: 'Medium',
count: '17',
cweid: '16',
desc:
'<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". </p>',
name: 'X-Content-Type-Options Header Missing',
parsedDescription:
' The Anti-MIME-Sniffing header X-Content-Type-Options was not set to "nosniff". ',
priority: 'Low (Medium)',
title: 'X-Content-Type-Options Header Missing',
reference:
'<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>',
riskcode: '1',
......@@ -27,34 +25,19 @@ describe('dast issue body', () => {
vm.$destroy();
});
describe('with priority', () => {
it('renders priority key', () => {
describe('severity and confidence ', () => {
it('renders severity and confidence', () => {
vm = mountComponent(Component, {
issue: dastIssue,
issueIndex: 1,
modalTargetId: '#modal-mrwidget-issue',
});
expect(vm.$el.textContent.trim()).toContain(dastIssue.priority);
expect(vm.$el.textContent.trim()).toContain(`${dastIssue.severity} (${dastIssue.confidence})`);
});
});
describe('without priority', () => {
it('does not rendere priority key', () => {
const issueCopy = Object.assign({}, dastIssue);
delete issueCopy.priority;
vm = mountComponent(Component, {
issue: issueCopy,
issueIndex: 1,
modalTargetId: '#modal-mrwidget-issue',
});
expect(vm.$el.textContent.trim()).not.toContain(dastIssue.priority);
});
});
describe('issue name', () => {
describe('issue title', () => {
beforeEach(() => {
vm = mountComponent(Component, {
issue: dastIssue,
......@@ -63,8 +46,8 @@ describe('dast issue body', () => {
});
});
it('renders button with issue name', () => {
expect(vm.$el.textContent.trim()).toContain(dastIssue.name);
it('renders button with issue title', () => {
expect(vm.$el.textContent.trim()).toContain(dastIssue.title);
});
});
});
......@@ -27,7 +27,7 @@ describe('Security Reports modal', () => {
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
isDismissed: true,
......@@ -83,7 +83,7 @@ describe('Security Reports modal', () => {
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
});
......@@ -112,12 +112,10 @@ describe('Security Reports modal', () => {
describe('with instances', () => {
beforeEach(() => {
store.dispatch('setModalData', {
name: 'Absence of Anti-CSRF Tokens',
title: 'Absence of Anti-CSRF Tokens',
riskcode: '1',
riskdesc: 'Low (Medium)',
priority: 'Low (Medium)',
desc: '<p>No Anti-CSRF tokens were found in a HTML submission form.</p>',
parsedDescription: ' No Anti-CSRF tokens were found in a HTML submission form. ',
pluginid: '123',
instances: [
{
......@@ -159,13 +157,17 @@ describe('Security Reports modal', () => {
store.dispatch('setModalData', {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-9999',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
location: {
file: 'Gemfile.lock',
},
links: [{
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
}],
});
vm = mountComponentWithStore(Component, {
......
......@@ -95,7 +95,7 @@ describe('Report issues', () => {
it('should not render location', () => {
vm = mountComponent(ReportIssues, {
issues: [{
name: 'foo',
title: 'foo',
}],
type: 'SAST',
status: 'failed',
......@@ -106,7 +106,7 @@ describe('Report issues', () => {
});
});
describe('for docker issues', () => {
describe('for container scanning issues', () => {
beforeEach(() => {
vm = mountComponent(ReportIssues, {
issues: dockerReportParsed.unapproved,
......@@ -115,16 +115,16 @@ describe('Report issues', () => {
});
});
it('renders priority', () => {
it('renders severity', () => {
expect(
vm.$el.querySelector('.report-block-list li').textContent.trim(),
).toContain(dockerReportParsed.unapproved[0].priority);
).toContain(dockerReportParsed.unapproved[0].severity);
});
it('renders CVE name', () => {
expect(
vm.$el.querySelector('.report-block-list button').textContent.trim(),
).toEqual(dockerReportParsed.unapproved[0].name);
).toEqual(dockerReportParsed.unapproved[0].title);
});
it('renders namespace', () => {
......@@ -148,9 +148,9 @@ describe('Report issues', () => {
});
});
it('renders priority and name', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].name);
expect(vm.$el.textContent).toContain(parsedDast[0].priority);
it('renders severity (confidence) and title', () => {
expect(vm.$el.textContent).toContain(parsedDast[0].title);
expect(vm.$el.textContent).toContain(`${parsedDast[0].severity} (${parsedDast[0].confidence})`);
});
});
});
......@@ -116,7 +116,7 @@ describe('Report section', () => {
cve: 'CVE-2016-9999',
file: 'Gemfile.lock',
message: 'Test Information Leak Vulnerability in Action View',
name: 'Test Information Leak Vulnerability in Action View',
title: 'Test Information Leak Vulnerability in Action View',
path: 'Gemfile.lock',
solution: 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1',
tool: 'bundler_audit',
......@@ -127,7 +127,7 @@ describe('Report section', () => {
cve: 'CVE-2014-7829',
file: 'Gemfile.lock',
message: 'Arbitrary file existence disclosure in Action Pack',
name: 'Arbitrary file existence disclosure in Action Pack',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
tool: 'bundler_audit',
......@@ -138,7 +138,7 @@ describe('Report section', () => {
cve: 'CVE-2016-0752',
file: 'Gemfile.lock',
message: 'Possible Information Leak Vulnerability in Action View',
name: 'Possible Information Leak Vulnerability in Action View',
title: 'Possible Information Leak Vulnerability in Action View',
path: 'Gemfile.lock',
solution: 'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1',
tool: 'bundler_audit',
......
......@@ -8,11 +8,9 @@ describe('sast container issue body', () => {
const Component = Vue.extend(component);
const sastContainerIssue = {
name: 'CVE-2017-11671',
nameLink: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11671',
title: 'CVE-2017-11671',
namespace: 'debian:8',
path: 'debian:8',
priority: 'Low',
severity: 'Low',
vulnerability: 'CVE-2017-11671',
};
......@@ -21,26 +19,26 @@ describe('sast container issue body', () => {
vm.$destroy();
});
describe('with priority', () => {
it('renders priority key', () => {
describe('with severity', () => {
it('renders severity key', () => {
vm = mountComponent(Component, {
issue: sastContainerIssue,
});
expect(vm.$el.textContent.trim()).toContain(sastContainerIssue.priority);
expect(vm.$el.textContent.trim()).toContain(sastContainerIssue.severity);
});
});
describe('without priority', () => {
it('does not rendere priority key', () => {
describe('without severity', () => {
it('does not render severity key', () => {
const issueCopy = Object.assign({}, sastContainerIssue);
delete issueCopy.priority;
delete issueCopy.severity;
vm = mountComponent(Component, {
issue: issueCopy,
});
expect(vm.$el.textContent.trim()).not.toContain(sastContainerIssue.priority);
expect(vm.$el.textContent.trim()).not.toContain(sastContainerIssue.severity);
});
});
......@@ -49,7 +47,7 @@ describe('sast container issue body', () => {
issue: sastContainerIssue,
});
expect(vm.$el.querySelector('button').textContent.trim()).toEqual(sastContainerIssue.name);
expect(vm.$el.querySelector('button').textContent.trim()).toEqual(sastContainerIssue.title);
});
describe('path', () => {
......
......@@ -11,7 +11,7 @@ describe('sast issue body', () => {
cve: 'CVE-2016-9999',
file: 'Gemfile.lock',
message: 'Test Information Leak Vulnerability in Action View',
name: 'Test Information Leak Vulnerability in Action View',
title: 'Test Information Leak Vulnerability in Action View',
path: 'Gemfile.lock',
solution:
'upgrade to >= 5.0.0.beta1.1, >= 4.2.5.1, ~> 4.2.5, >= 4.1.14.1, ~> 4.1.14, ~> 3.2.22.1',
......@@ -19,27 +19,57 @@ describe('sast issue body', () => {
url:
'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
urlPath: '/Gemfile.lock',
priority: 'Low',
severity: 'Medium',
confidence: 'Low',
};
afterEach(() => {
vm.$destroy();
});
describe('with priority', () => {
it('renders priority key', () => {
describe('with severity and confidence (new json format)', () => {
it('renders severity and confidence', () => {
vm = mountComponent(Component, {
issue: sastIssue,
});
expect(vm.$el.textContent.trim()).toContain(sastIssue.priority);
expect(vm.$el.textContent.trim()).toContain(`${sastIssue.severity} (${sastIssue.confidence})`);
});
});
describe('without severity', () => {
it('does not render severity nor confidence', () => {
const issueCopy = Object.assign({}, sastIssue);
delete issueCopy.severity;
vm = mountComponent(Component, {
issue: issueCopy,
});
expect(vm.$el.textContent.trim()).not.toContain(sastIssue.severity);
expect(vm.$el.textContent.trim()).not.toContain(sastIssue.confidence);
});
});
describe('with priority (old json format)', () => {
it('renders priority key', () => {
const issueCopy = Object.assign({}, sastIssue);
delete issueCopy.severity;
delete issueCopy.confidence;
issueCopy.priority = 'Low';
vm = mountComponent(Component, {
issue: issueCopy,
});
expect(vm.$el.textContent.trim()).toContain(issueCopy.priority);
});
});
describe('without priority', () => {
it('does not rendere priority key', () => {
it('does not render priority key', () => {
const issueCopy = Object.assign({}, sastIssue);
delete issueCopy.priority;
delete issueCopy.severity;
delete issueCopy.confidence;
vm = mountComponent(Component, {
issue: issueCopy,
......@@ -51,20 +81,20 @@ describe('sast issue body', () => {
});
});
describe('name', () => {
it('renders name', () => {
describe('title', () => {
it('renders title', () => {
vm = mountComponent(Component, {
issue: sastIssue,
});
expect(vm.$el.textContent.trim()).toContain(
sastIssue.name,
sastIssue.title,
);
});
});
describe('path', () => {
it('renders name', () => {
it('renders path', () => {
vm = mountComponent(Component, {
issue: sastIssue,
});
......
......@@ -316,21 +316,50 @@ describe('security reports mutations', () => {
const issue = {
tool: 'bundler_audit',
message: 'Arbitrary file existence disclosure in Action Pack',
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
cve: 'CVE-2014-7829',
file: 'Gemfile.lock',
solution: 'upgrade to ~> 3.2.21, ~> 4.0.11.1, ~> 4.0.12, ~> 4.1.7.1, >= 4.1.8',
name: 'Arbitrary file existence disclosure in Action Pack',
title: 'Arbitrary file existence disclosure in Action Pack',
path: 'Gemfile.lock',
urlPath: 'path/Gemfile.lock',
namespace: 'debian:8',
location: {
file: 'Gemfile.lock',
class: 'User',
method: 'do_something',
},
links: [{
url: 'https://groups.google.com/forum/#!topic/rubyonrails-security/rMTQy4oRCGk',
}],
identifiers: [{
type: 'CVE',
name: 'CVE-2014-9999',
value: 'CVE-2014-9999',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-9999',
}],
instances: [{
param: 'X-Content-Type-Options',
method: 'GET',
uri: 'http://example.com/some-path',
}],
isDismissed: true,
};
mutations[types.SET_ISSUE_MODAL_DATA](stateCopy, issue);
expect(stateCopy.modal.title).toEqual(issue.name);
expect(stateCopy.modal.data.file.value).toEqual(issue.file);
expect(stateCopy.modal.title).toEqual(issue.title);
expect(stateCopy.modal.data.description.value).toEqual(issue.description);
expect(stateCopy.modal.data.file.value).toEqual(issue.location.file);
expect(stateCopy.modal.data.file.url).toEqual(issue.urlPath);
expect(stateCopy.modal.data.className.value).toEqual(issue.location.class);
expect(stateCopy.modal.data.methodName.value).toEqual(issue.location.method);
expect(stateCopy.modal.data.namespace.value).toEqual(issue.namespace);
expect(stateCopy.modal.data.identifiers.value).toEqual(issue.identifiers);
expect(stateCopy.modal.data.severity.value).toEqual(issue.severity);
expect(stateCopy.modal.data.confidence.value).toEqual(issue.confidence);
expect(stateCopy.modal.data.solution.value).toEqual(issue.solution);
expect(stateCopy.modal.data.links.value).toEqual(issue.links);
expect(stateCopy.modal.data.instances.value).toEqual(issue.instances);
expect(stateCopy.modal.vulnerability).toEqual(issue);
});
});
......
......@@ -11,6 +11,7 @@ import {
statusIcon,
} from 'ee/vue_shared/security_reports/store/utils';
import {
oldSastIssues,
sastIssues,
sastFeedbacks,
dependencyScanningIssues,
......@@ -52,10 +53,17 @@ describe('security reports utils', () => {
});
describe('parseSastIssues', () => {
it('should parse the received issues', () => {
it('should parse the received issues with old JSON format', () => {
const parsed = parseSastIssues(oldSastIssues, [], 'path')[0];
expect(parsed.title).toEqual(sastIssues[0].message);
expect(parsed.path).toEqual(sastIssues[0].location.file);
expect(parsed.project_fingerprint).toEqual(sha1(sastIssues[0].cve));
});
it('should parse the received issues with new JSON format', () => {
const parsed = parseSastIssues(sastIssues, [], 'path')[0];
expect(parsed.name).toEqual(sastIssues[0].message);
expect(parsed.path).toEqual(sastIssues[0].file);
expect(parsed.title).toEqual(sastIssues[0].message);
expect(parsed.path).toEqual(sastIssues[0].location.file);
expect(parsed.project_fingerprint).toEqual(sha1(sastIssues[0].cve));
});
......@@ -75,7 +83,7 @@ describe('security reports utils', () => {
describe('parseDependencyScanningIssues', () => {
it('should parse the received issues', () => {
const parsed = parseDependencyScanningIssues(dependencyScanningIssues, [], 'path')[0];
expect(parsed.name).toEqual(dependencyScanningIssues[0].message);
expect(parsed.title).toEqual(dependencyScanningIssues[0].message);
expect(parsed.path).toEqual(dependencyScanningIssues[0].file);
expect(parsed.project_fingerprint).toEqual(sha1(dependencyScanningIssues[0].cve));
});
......@@ -107,14 +115,14 @@ describe('security reports utils', () => {
const parsed = parseSastContainer(dockerReport.vulnerabilities)[0];
const issue = dockerReport.vulnerabilities[0];
expect(parsed.name).toEqual(dockerReport.vulnerabilities[0].vulnerability);
expect(parsed.priority).toEqual(dockerReport.vulnerabilities[0].severity);
expect(parsed.path).toEqual(dockerReport.vulnerabilities[0].namespace);
expect(parsed.nameLink).toEqual(
`https://cve.mitre.org/cgi-bin/cvename.cgi?name=${
dockerReport.vulnerabilities[0].vulnerability
}`,
);
expect(parsed.title).toEqual(issue.vulnerability);
expect(parsed.path).toEqual(issue.namespace);
expect(parsed.identifiers).toEqual([{
type: 'CVE',
name: issue.vulnerability,
value: issue.vulnerability,
url: `https://cve.mitre.org/cgi-bin/cvename.cgi?name=${issue.vulnerability}`,
}]);
expect(parsed.project_fingerprint).toEqual(
sha1(`${issue.namespace}:${issue.vulnerability}:${issue.featurename}:${issue.featureversion}`));
});
......
......@@ -3,27 +3,11 @@ require 'spec_helper'
describe Gitlab::FileFinder do
describe '#find' do
let(:project) { create(:project, :public, :repository) }
let(:finder) { described_class.new(project, project.default_branch) }
it 'finds by name' do
results = finder.find('files')
filename, blob = results.find { |_, blob| blob.filename == 'files/images/wm.svg' }
expect(filename).to eq('files/images/wm.svg')
expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
expect(blob.ref).to eq(finder.ref)
expect(blob.data).not_to be_empty
end
it 'finds by content' do
results = finder.find('files')
filename, blob = results.find { |_, blob| blob.filename == 'CHANGELOG' }
expect(filename).to eq('CHANGELOG')
expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
expect(blob.ref).to eq(finder.ref)
expect(blob.data).not_to be_empty
it_behaves_like 'file finder' do
subject { described_class.new(project, project.default_branch) }
let(:expected_file_by_name) { 'files/images/wm.svg' }
let(:expected_file_by_content) { 'CHANGELOG' }
end
end
end
......@@ -22,47 +22,57 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.query).to eq('hello world') }
end
describe 'blob search' do
let(:project) { create(:project, :public, :repository) }
subject(:results) { described_class.new(user, project, 'files').objects('blobs') }
context 'when repository is disabled' do
let(:project) { create(:project, :public, :repository, :repository_disabled) }
shared_examples 'general blob search' do |entity_type, blob_kind|
let(:query) { 'files' }
subject(:results) { described_class.new(user, project, query).objects(blob_type) }
it 'hides blobs from members' do
context "when #{entity_type} is disabled" do
let(:project) { disabled_project }
it "hides #{blob_kind} from members" do
project.add_reporter(user)
is_expected.to be_empty
end
it 'hides blobs from non-members' do
it "hides #{blob_kind} from non-members" do
is_expected.to be_empty
end
end
context 'when repository is internal' do
let(:project) { create(:project, :public, :repository, :repository_private) }
context "when #{entity_type} is internal" do
let(:project) { private_project }
it 'finds blobs for members' do
it "finds #{blob_kind} for members" do
project.add_reporter(user)
is_expected.not_to be_empty
end
it 'hides blobs from non-members' do
it "hides #{blob_kind} from non-members" do
is_expected.to be_empty
end
end
it 'finds by name' do
expect(results.map(&:first)).to include('files/images/wm.svg')
expect(results.map(&:first)).to include(expected_file_by_name)
end
it 'finds by content' do
blob = results.select { |result| result.first == 'CHANGELOG' }.flatten.last
blob = results.select { |result| result.first == expected_file_by_content }.flatten.last
expect(blob.filename).to eq("CHANGELOG")
expect(blob.filename).to eq(expected_file_by_content)
end
end
describe 'blob search' do
let(:project) { create(:project, :public, :repository) }
it_behaves_like 'general blob search', 'repository', 'blobs' do
let(:blob_type) { 'blobs' }
let(:disabled_project) { create(:project, :public, :repository, :repository_disabled) }
let(:private_project) { create(:project, :public, :repository, :repository_private) }
let(:expected_file_by_name) { 'files/images/wm.svg' }
let(:expected_file_by_content) { 'CHANGELOG' }
end
describe 'parsing results' do
......@@ -189,40 +199,18 @@ describe Gitlab::ProjectSearchResults do
describe 'wiki search' do
let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
let!(:wiki_page) { wiki.create_page('Title', 'Content') }
subject(:results) { described_class.new(user, project, 'Content').objects('wiki_blobs') }
context 'when wiki is disabled' do
let(:project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
it 'hides wiki blobs from members' do
project.add_reporter(user)
is_expected.to be_empty
end
it 'hides wiki blobs from non-members' do
is_expected.to be_empty
end
end
context 'when wiki is internal' do
let(:project) { create(:project, :public, :wiki_repo, :wiki_private) }
it 'finds wiki blobs for guest' do
project.add_guest(user)
is_expected.not_to be_empty
end
it 'hides wiki blobs from non-members' do
is_expected.to be_empty
end
before do
wiki.create_page('Files/Title', 'Content')
wiki.create_page('CHANGELOG', 'Files example')
end
it 'finds by content' do
expect(results).to include("master:Title.md\x001\x00Content\n")
it_behaves_like 'general blob search', 'wiki', 'wiki blobs' do
let(:blob_type) { 'wiki_blobs' }
let(:disabled_project) { create(:project, :public, :wiki_repo, :wiki_disabled) }
let(:private_project) { create(:project, :public, :wiki_repo, :wiki_private) }
let(:expected_file_by_name) { 'Files/Title.md' }
let(:expected_file_by_content) { 'CHANGELOG.md' }
end
end
......
......@@ -49,14 +49,4 @@ describe Gitlab::SlashCommands::Presenters::IssueShow do
expect(attachment[:text]).to start_with("**Open**")
end
end
context 'issue with issue weight' do
let(:issue) { create(:issue, project: project, weight: 3) }
let(:weight_attachment) { subject[:attachments].first[:fields].find { |a| a[:title] == "Weight" } }
it 'shows the weight' do
expect(weight_attachment).not_to be_nil
expect(weight_attachment[:value]).to be(3)
end
end
end
require 'spec_helper'
describe Gitlab::WikiFileFinder do
describe '#find' do
let(:project) { create(:project, :public, :wiki_repo) }
let(:wiki) { build(:project_wiki, project: project) }
before do
wiki.create_page('Files/Title', 'Content')
wiki.create_page('CHANGELOG', 'Files example')
end
it_behaves_like 'file finder' do
subject { described_class.new(project, project.wiki.default_branch) }
let(:expected_file_by_name) { 'Files/Title.md' }
let(:expected_file_by_content) { 'CHANGELOG.md' }
end
end
end
shared_examples 'file finder' do
let(:query) { 'files' }
let(:search_results) { subject.find(query) }
it 'finds by name' do
filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_name }
expect(filename).to eq(expected_file_by_name)
expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
it 'finds by content' do
filename, blob = search_results.find { |_, blob| blob.filename == expected_file_by_content }
expect(filename).to eq(expected_file_by_content)
expect(blob).to be_a(Gitlab::SearchResults::FoundBlob)
expect(blob.ref).to eq(subject.ref)
expect(blob.data).not_to be_empty
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment