diff --git a/CHANGELOG.md b/CHANGELOG.md index f834c2b04be..0c0ac790402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 14.0.4 (2021-07-07) + +### Security (1 change) + +- [Disable file and network premailer strategies](gitlab-org/security/gitlab@4af58e3d8ee1b25048f34208db6e685cf0bf1411) ([merge request](gitlab-org/security/gitlab!1544)) + ## 14.0.3 (2021-07-06) ### Fixed (7 changes) @@ -684,6 +690,12 @@ entry. - [Add missing metrics information](gitlab-org/gitlab@89cd7fe3b95323e635b2d73e08549b2e6153dc4d) ([merge request](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61772/edit)) - [Track usage of the resolve UI](gitlab-org/gitlab@35c8e30fce288cecefcf2f7c0077d4608e696519) ([merge request](gitlab-org/gitlab!61654)) +## 13.12.8 (2021-07-07) + +### Security (1 change) + +- [Disable file and network premailer strategies](gitlab-org/security/gitlab@ee69d6d6950bb116cb31523ca805e78af431c25c) ([merge request](gitlab-org/security/gitlab!1545)) + ## 13.12.7 (2021-07-05) ### Fixed (2 changes) @@ -1357,6 +1369,12 @@ entry. - Change wording for design management upload. !61782 +## 13.11.7 (2021-07-07) + +### Security (1 change) + +- [Disable file and network premailer strategies](gitlab-org/security/gitlab@511ed3746b48a26e95c851f76ac6fdcd44c28fd8) ([merge request](gitlab-org/security/gitlab!1546)) + ## 13.11.6 (2021-07-01) ### Added (1 change) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 956378bd4ea..2d6eeaf50fe 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -8cc6bb1ffb6830fa416c8d0e32f9edebf6573730 +ed9300c34897316f98e93d909b964828cb4e5879 diff --git a/app/assets/javascripts/blob/csv/csv_viewer.vue b/app/assets/javascripts/blob/csv/csv_viewer.vue new file mode 100644 index 00000000000..050f2785d9a --- /dev/null +++ b/app/assets/javascripts/blob/csv/csv_viewer.vue @@ -0,0 +1,55 @@ + + + + + + + + + + {{ errorMessage }} + + + + + diff --git a/app/assets/javascripts/blob/csv/index.js b/app/assets/javascripts/blob/csv/index.js new file mode 100644 index 00000000000..4cf6c169c68 --- /dev/null +++ b/app/assets/javascripts/blob/csv/index.js @@ -0,0 +1,17 @@ +import Vue from 'vue'; +import CsvViewer from './csv_viewer.vue'; + +export default () => { + const el = document.getElementById('js-csv-viewer'); + + return new Vue({ + el, + render(createElement) { + return createElement(CsvViewer, { + props: { + csv: el.dataset.data, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/blob/csv_viewer.js b/app/assets/javascripts/blob/csv_viewer.js new file mode 100644 index 00000000000..64d3ba0b390 --- /dev/null +++ b/app/assets/javascripts/blob/csv_viewer.js @@ -0,0 +1,3 @@ +import renderCSV from './csv'; + +export default renderCSV; diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js index 47b82296128..db91d478b39 100644 --- a/app/assets/javascripts/blob/viewer/index.js +++ b/app/assets/javascripts/blob/viewer/index.js @@ -21,6 +21,8 @@ const loadRichBlobViewer = (type) => { return import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'); case 'openapi': return import(/* webpackChunkName: 'openapi_viewer' */ '../openapi_viewer'); + case 'csv': + return import(/* webpackChunkName: 'csv_viewer' */ '../csv_viewer'); case 'pdf': return import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'); case 'sketch': diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 93187a19af1..48d943587d4 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -373,8 +373,6 @@ class ProjectsController < Projects::ApplicationController .new(projects, offset: params[:offset].to_i, filter: event_filter) .to_a .map(&:present) - - Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/models/blob.rb b/app/models/blob.rb index 2185233a1ac..5731d38abe4 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -27,6 +27,7 @@ class Blob < SimpleDelegator # type. LFS pointers to `.stl` files are assumed to always be the binary kind, # and use the `BinarySTL` viewer. RICH_VIEWERS = [ + BlobViewer::CSV, BlobViewer::Markup, BlobViewer::Notebook, BlobViewer::SVG, diff --git a/app/models/blob_viewer/csv.rb b/app/models/blob_viewer/csv.rb new file mode 100644 index 00000000000..633e3bd63d8 --- /dev/null +++ b/app/models/blob_viewer/csv.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module BlobViewer + class CSV < Base + include Rich + include ClientSide + + self.binary = false + self.extensions = %w(csv) + self.partial_name = 'csv' + self.switcher_icon = 'table' + end +end diff --git a/app/views/projects/blob/viewers/_csv.html.haml b/app/views/projects/blob/viewers/_csv.html.haml new file mode 100644 index 00000000000..3a58bc9902c --- /dev/null +++ b/app/views/projects/blob/viewers/_csv.html.haml @@ -0,0 +1 @@ +.file-content#js-csv-viewer{ data: { data: viewer.blob.data } } diff --git a/config/initializers/premailer.rb b/config/initializers/premailer.rb index 77077888af3..b07dc792cab 100644 --- a/config/initializers/premailer.rb +++ b/config/initializers/premailer.rb @@ -7,5 +7,6 @@ Premailer::Rails.config.merge!( remove_comments: true, remove_ids: false, remove_scripts: false, - output_encoding: 'US-ASCII' + output_encoding: 'US-ASCII', + strategies: [:asset_pipeline] ) diff --git a/config/initializers_before_autoloader/004_zeitwerk.rb b/config/initializers_before_autoloader/004_zeitwerk.rb index d576e21f173..18dfa2e9f40 100644 --- a/config/initializers_before_autoloader/004_zeitwerk.rb +++ b/config/initializers_before_autoloader/004_zeitwerk.rb @@ -49,6 +49,7 @@ Rails.autoloaders.each do |autoloader| 'ldap_key' => 'LDAPKey', 'mr_note' => 'MRNote', 'pdf' => 'PDF', + 'csv' => 'CSV', 'rsa_token' => 'RSAToken', 'san_extension' => 'SANExtension', 'sca' => 'SCA', diff --git a/doc/api/packages/helm.md b/doc/api/packages/helm.md new file mode 100644 index 00000000000..39054652908 --- /dev/null +++ b/doc/api/packages/helm.md @@ -0,0 +1,113 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Helm API + +This is the API documentation for [Helm](../../user/packages/helm_repository/index.md). + +WARNING: +This API is used by the Helm-related package clients such as [Helm](https://helm.sh/) +and [`helm-push`](https://github.com/chartmuseum/helm-push/#readme), +and is generally not meant for manual consumption. This API is under development and is not ready +for production use due to limited functionality. + +For instructions on how to upload and install Helm packages from the GitLab +Package Registry, see the [Helm registry documentation](../../user/packages/helm_repository/index.md). + +NOTE: +These endpoints do not adhere to the standard API authentication methods. +See the [Helm registry documentation](../../user/packages/helm_repository/index.md) +for details on which headers and token types are supported. + +## Enable the Helm API + +The Helm API for GitLab is behind a feature flag that is disabled by default. GitLab +administrators with access to the GitLab Rails console can enable this API for your instance. + +To enable it: + +```ruby +Feature.enable(:helm_packages) +``` + +To disable it: + +```ruby +Feature.disable(:helm_packages) +``` + +## Download a chart index + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62757) in GitLab 14.1. + +Download a chart index: + +```plaintext +GET projects/:id/packages/helm/:channel/index.yaml +``` + +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------- | +| `id` | string | yes | The ID or full path of the project. | +| `channel` | string | yes | Helm repository channel. | + +```shell +curl --user : \ + https://gitlab.example.com/api/v4/projects/1/packages/helm/stable/index.yaml +``` + +Write the output to a file: + +```shell +curl --user : \ + https://gitlab.example.com/api/v4/projects/1/packages/helm/stable/index.yaml \ + --remote-name +``` + +## Download a chart + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61014) in GitLab 14.0. + +Download a chart: + +```plaintext +GET projects/:id/packages/helm/:channel/charts/:file_name.tgz +``` + +| Attribute | Type | Required | Description | +| ----------- | ------ | -------- | ----------- | +| `id` | string | yes | The ID or full path of the project. | +| `channel` | string | yes | Helm repository channel. | +| `file_name` | string | yes | Chart file name. | + +```shell +curl --user : \ + https://gitlab.example.com/api/v4/projects/1/packages/helm/stable/charts/mychart.tgz \ + --remote-name +``` + +## Upload a chart + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64814) in GitLab 14.1. + +Upload a chart: + +```plaintext +POST projects/:id/packages/helm/api/:channel/charts +``` + +| Attribute | Type | Required | Description | +| --------- | ------ | -------- | ----------- | +| `id` | string | yes | The ID or full path of the project. | +| `channel` | string | yes | Helm repository channel. | +| `chart` | file | yes | Chart (as `multipart/form-data`). | + +```shell +curl --request POST \ + --form 'chart=@mychart.tgz' \ + --user : \ + https://gitlab.example.com/api/v4/projects/1/packages/helm/api/stable/charts +``` diff --git a/doc/push_rules/push_rules.md b/doc/push_rules/push_rules.md index 34a63f425eb..a19535eb90a 100644 --- a/doc/push_rules/push_rules.md +++ b/doc/push_rules/push_rules.md @@ -39,6 +39,12 @@ Now when a user tries to push a commit with a message `Bugfix`, their push is declined. Only pushing commits with messages like `Bugfix according to JIRA-123` is accepted. +The error message includes the rejected commit's SHA. +To resolve such errors, commit again with a matching message, +[rebase and reword](../topics/git/numerous_undo_possibilities_in_git/index.md#how-to-change-history), +or [amend](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt---amend) +that commit's message locally. + ### Restrict branch names If your company has a strict policy for branch names, you may want the branches to start diff --git a/doc/topics/autodevops/customize.md b/doc/topics/autodevops/customize.md index 011b8b7b949..8f48b1fc57a 100644 --- a/doc/topics/autodevops/customize.md +++ b/doc/topics/autodevops/customize.md @@ -314,6 +314,19 @@ The version of the chart used to provision PostgreSQL: GitLab encourages users to [migrate their database](upgrading_postgresql.md) to the newer PostgreSQL. +### Customize values for PostgreSQL Helm Chart + +> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/issues/113) in auto-deploy-image v2, in GitLab 13.8. + +To set custom values, do one of the following: + +- Add a file named `.gitlab/auto-deploy-postgres-values.yaml` to your repository. If found, this + file is used automatically. This file is used by default for PostgreSQL Helm upgrades. +- Add a file with a different name or path to the repository, and set the + `POSTGRES_HELM_UPGRADE_VALUES_FILE` [environment variable](#database) with the path + and name. +- Set the `POSTGRES_HELM_UPGRADE_EXTRA_ARGS` [environment variable](#database). + ### Using external PostgreSQL database providers While Auto DevOps provides out-of-the-box support for a PostgreSQL container for @@ -408,6 +421,8 @@ The following table lists CI/CD variables related to the database. | `POSTGRES_PASSWORD` | The PostgreSQL password. Defaults to `testing-password`. Set it to use a custom password. | | `POSTGRES_DB` | The PostgreSQL database name. Defaults to the value of [`$CI_ENVIRONMENT_SLUG`](../../ci/variables/index.md#predefined-cicd-variables). Set it to use a custom database name. | | `POSTGRES_VERSION` | Tag for the [`postgres` Docker image](https://hub.docker.com/_/postgres) to use. Defaults to `9.6.16` for tests and deployments as of GitLab 13.0 (previously `9.6.2`). If `AUTO_DEVOPS_POSTGRES_CHANNEL` is set to `1`, deployments uses the default version `9.6.2`. | +| `POSTGRES_HELM_UPGRADE_VALUES_FILE` | In GitLab 13.8 and later, and when using [auto-deploy-image v2](upgrading_auto_deploy_dependencies.md), this variable allows the `helm upgrade` values file for PostgreSQL to be overridden. Defaults to `.gitlab/auto-deploy-postgres-values.yaml`. | +| `POSTGRES_HELM_UPGRADE_EXTRA_ARGS` | In GitLab 13.8 and later, and when using [auto-deploy-image v2](upgrading_auto_deploy_dependencies.md), this variable allows extra PostgreSQL options in `helm upgrade` commands when deploying the application. Note that using quotes doesn't prevent word splitting. | ### Disable jobs diff --git a/doc/user/packages/helm_repository/index.md b/doc/user/packages/helm_repository/index.md new file mode 100644 index 00000000000..b42cd824100 --- /dev/null +++ b/doc/user/packages/helm_repository/index.md @@ -0,0 +1,80 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments +--- + +# Helm charts in the Package Registry **(FREE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18997) in GitLab 14.1. + +WARNING: +The Helm package registry for GitLab is under development and isn't ready for production use due to +limited functionality. + +Publish Helm packages in your project's Package Registry. Then install the +packages whenever you need to use them as a dependency. + +For documentation of the specific API endpoints that Helm package manager +clients use, see the [Helm API documentation](../../../api/packages/helm.md). + +## Enable the Helm repository feature + +Helm repository support is still a work in progress. It's gated behind a feature flag that's +**disabled by default**. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) +can opt to enable it. + +To enable it: + +```ruby +Feature.enable(:helm_packages) +``` + +To disable it: + +```ruby +Feature.disable(:helm_packages) +``` + +## Build a Helm package + +Creating a Helm package is documented [in the Helm documentation](https://helm.sh/docs/intro/using_helm/#creating-your-own-charts). + +## Authenticate to the Helm repository + +To authenticate to the Helm repository, you need either: + +- A [personal access token](../../../api/index.md#personalproject-access-tokens). +- A [deploy token](../../project/deploy_tokens/index.md). +- A [CI/CD job token](../../../api/index.md#gitlab-cicd-job-token). + +## Publish a package + +Once built, a chart can be uploaded to the `stable` channel with `curl` or `helm-push`: + +- With `curl`: + + ```shell + curl --request POST \ + --form 'chart=@mychart.tgz' \ + --user : \ + https://gitlab.example.com/api/v4/projects/1/packages/helm/api/stable/charts + ``` + +- With the [`helm-push`](https://github.com/chartmuseum/helm-push/#readme) plugin: + + ```shell + helm repo add --username --password project-1 https://gitlab.example.com/api/v4/projects/1/packages/helm/stable + helm push mychart.tgz project-1 + ``` + +## Install a package + +To install the latest version of a chart, use the following command: + +```shell +helm repo add project-1 https://gitlab.example.com/api/v4/projects/1/packages/helm/stable +helm install my-release project-1/mychart +``` + +See [Using Helm](https://helm.sh/docs/intro/using_helm/) for more information. diff --git a/lib/gitlab/ci/reports/security/analyzer.rb b/lib/gitlab/ci/reports/security/analyzer.rb new file mode 100644 index 00000000000..b88eaf87cef --- /dev/null +++ b/lib/gitlab/ci/reports/security/analyzer.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Analyzer + attr_reader :id, :name, :version, :vendor + + def initialize(id:, name:, version:, vendor:) + @id = id + @name = name + @version = version + @vendor = vendor + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/concerns/fingerprint_path_from_file.rb b/lib/gitlab/ci/reports/security/concerns/fingerprint_path_from_file.rb new file mode 100644 index 00000000000..ec1d80e11c8 --- /dev/null +++ b/lib/gitlab/ci/reports/security/concerns/fingerprint_path_from_file.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + module Concerns + module FingerprintPathFromFile + extend ActiveSupport::Concern + + def fingerprint_path + File.basename(file_path.to_s) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/identifier.rb b/lib/gitlab/ci/reports/security/identifier.rb new file mode 100644 index 00000000000..4ba943cdcbc --- /dev/null +++ b/lib/gitlab/ci/reports/security/identifier.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Identifier + attr_reader :external_id + attr_reader :external_type + attr_reader :fingerprint + attr_reader :name + attr_reader :url + + def initialize(external_id:, external_type:, name:, url: nil) + @external_id = external_id + @external_type = external_type + @name = name + @url = url + + @fingerprint = generate_fingerprint + end + + def key + fingerprint + end + + def to_hash + %i[ + external_id + external_type + fingerprint + name + url + ].each_with_object({}) do |key, hash| + hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def ==(other) + other.external_type == external_type && + other.external_id == external_id + end + + def type_identifier? + cwe? || wasc? + end + + def cve? + external_type.to_s.casecmp?('cve') + end + + def cwe? + external_type.to_s.casecmp?('cwe') + end + + def wasc? + external_type.to_s.casecmp?('wasc') + end + + private + + def generate_fingerprint + Digest::SHA1.hexdigest("#{external_type}:#{external_id}") + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/link.rb b/lib/gitlab/ci/reports/security/link.rb new file mode 100644 index 00000000000..1c4c05cd9ac --- /dev/null +++ b/lib/gitlab/ci/reports/security/link.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Link + attr_accessor :name, :url + + def initialize(name: nil, url: nil) + @name = name + @url = url + end + + def to_hash + { + name: name, + url: url + }.compact + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/scanned_resource.rb b/lib/gitlab/ci/reports/security/scanned_resource.rb new file mode 100644 index 00000000000..605577eafcd --- /dev/null +++ b/lib/gitlab/ci/reports/security/scanned_resource.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class ScannedResource + include Gitlab::Utils::StrongMemoize + + attr_reader :request_method + attr_reader :request_uri + + delegate :scheme, :host, :port, :path, :query, to: :request_uri, prefix: :url + + def initialize(uri, request_method) + raise ArgumentError unless uri.is_a?(URI) + + @request_method = request_method + @request_uri = uri + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/security/scanner.rb b/lib/gitlab/ci/reports/security/scanner.rb new file mode 100644 index 00000000000..de5c354b5ab --- /dev/null +++ b/lib/gitlab/ci/reports/security/scanner.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Security + class Scanner + ANALYZER_ORDER = { + "bundler_audit" => 1, + "retire.js" => 2, + "gemnasium" => 3, + "gemnasium-maven" => 3, + "gemnasium-python" => 3, + "bandit" => 1, + "semgrep" => 2 + }.freeze + + attr_accessor :external_id, :name, :vendor, :version + + alias_method :key, :external_id + + def initialize(external_id:, name:, vendor:, version:) + @external_id = external_id + @name = name + @vendor = vendor + @version = version + end + + def to_hash + { + external_id: external_id.to_s, + name: name.to_s, + vendor: vendor.presence + }.compact + end + + def ==(other) + other.external_id == external_id + end + + def <=>(other) + sort_keys <=> other.sort_keys + end + + protected + + def sort_keys + @sort_keys ||= [order, external_id, name, vendor] + end + + private + + def order + ANALYZER_ORDER.fetch(external_id, Float::INFINITY) + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 3857f14a223..7890e51d20a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22029,6 +22029,9 @@ msgstr "" msgid "No %{providerTitle} repositories found" msgstr "" +msgid "No CSV data to display." +msgstr "" + msgid "No Epic" msgstr "" diff --git a/package.json b/package.json index fe891f19fef..bc50ae73e7f 100644 --- a/package.json +++ b/package.json @@ -148,6 +148,7 @@ "monaco-editor-webpack-plugin": "^4.0.0", "monaco-yaml": "^2.5.1", "mousetrap": "1.6.5", + "papaparse": "^5.3.1", "pdfjs-dist": "^2.0.943", "pikaday": "^1.8.0", "popper.js": "^1.16.1", diff --git a/spec/factories/ci/reports/security/identifiers.rb b/spec/factories/ci/reports/security/identifiers.rb new file mode 100644 index 00000000000..5211cb5c54a --- /dev/null +++ b/spec/factories/ci/reports/security/identifiers.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_identifier, class: '::Gitlab::Ci::Reports::Security::Identifier' do + external_id { 'PREDICTABLE_RANDOM' } + external_type { 'find_sec_bugs_type' } + name { "#{external_type}-#{external_id}" } + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::Identifier.new(**attributes) + end + end +end diff --git a/spec/factories/ci/reports/security/links.rb b/spec/factories/ci/reports/security/links.rb new file mode 100644 index 00000000000..77af827e7be --- /dev/null +++ b/spec/factories/ci/reports/security/links.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_link, class: '::Gitlab::Ci::Reports::Security::Link' do + name { 'CVE-2020-0202' } + url { 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202' } + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::Link.new(**attributes) + end + end +end diff --git a/spec/factories/ci/reports/security/scanners.rb b/spec/factories/ci/reports/security/scanners.rb new file mode 100644 index 00000000000..8b68ebdb47a --- /dev/null +++ b/spec/factories/ci/reports/security/scanners.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :ci_reports_security_scanner, class: '::Gitlab::Ci::Reports::Security::Scanner' do + external_id { 'find_sec_bugs' } + name { 'Find Security Bugs' } + vendor { 'Security Scanner Vendor' } + version { '1.0.0' } + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::Scanner.new(**attributes) + end + end + + factory :ci_reports_security_scan, class: '::Gitlab::Ci::Reports::Security::Scan' do + status { 'success' } + type { 'sast' } + start_time { 'placeholder' } + end_time { 'placeholder' } + + skip_create + + initialize_with do + ::Gitlab::Ci::Reports::Security::Scan.new(attributes) + end + end +end diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js new file mode 100644 index 00000000000..abb914b8f57 --- /dev/null +++ b/spec/frontend/blob/csv/csv_viewer_spec.js @@ -0,0 +1,75 @@ +import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { getAllByRole } from '@testing-library/dom'; +import { shallowMount, mount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import CSVViewer from '~/blob/csv/csv_viewer.vue'; + +const validCsv = 'one,two,three'; +const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}'; + +describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => { + let wrapper; + + const createComponent = ({ csv = validCsv, mountFunction = shallowMount } = {}) => { + wrapper = mountFunction(CSVViewer, { + propsData: { + csv, + }, + }); + }; + + const findCsvTable = () => wrapper.findComponent(GlTable); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAlert = () => wrapper.findComponent(GlAlert); + + afterEach(() => { + wrapper.destroy(); + }); + + it('should render loading spinner', () => { + createComponent(); + + expect(findLoadingIcon().props()).toMatchObject({ + size: 'lg', + }); + }); + + describe('when the CSV contains errors', () => { + it('should render alert', async () => { + createComponent({ csv: brokenCsv }); + await nextTick; + + expect(findAlert().props()).toMatchObject({ + variant: 'danger', + }); + }); + }); + + describe('when the CSV contains no errors', () => { + it('should not render alert', async () => { + createComponent(); + await nextTick; + + expect(findAlert().exists()).toBe(false); + }); + + it('renders the CSV table with the correct attributes', async () => { + createComponent(); + await nextTick; + + expect(findCsvTable().attributes()).toMatchObject({ + 'empty-text': 'No CSV data to display.', + items: validCsv, + }); + }); + + it('renders the CSV table with the correct content', async () => { + createComponent({ mountFunction: mount }); + await nextTick; + + expect(getAllByRole(wrapper.element, 'row', { name: /One/i })).toHaveLength(1); + expect(getAllByRole(wrapper.element, 'row', { name: /Two/i })).toHaveLength(1); + expect(getAllByRole(wrapper.element, 'row', { name: /Three/i })).toHaveLength(1); + }); + }); +}); diff --git a/spec/lib/gitlab/ci/reports/security/identifier_spec.rb b/spec/lib/gitlab/ci/reports/security/identifier_spec.rb new file mode 100644 index 00000000000..123730b6ee6 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/identifier_spec.rb @@ -0,0 +1,125 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Identifier do + using RSpec::Parameterized::TableSyntax + + describe '#initialize' do + subject { described_class.new(**params) } + + let(:params) do + { + external_type: 'brakeman_warning_code', + external_id: '107', + name: 'Brakeman Warning Code 107', + url: 'https://brakemanscanner.org/docs/warning_types/cross_site_scripting/' + } + end + + context 'when all params are given' do + it 'initializes an instance' do + expect { subject }.not_to raise_error + + expect(subject).to have_attributes( + external_type: 'brakeman_warning_code', + external_id: '107', + fingerprint: 'aa2254904a69148ad14b6ac5db25b355da9c987f', + name: 'Brakeman Warning Code 107', + url: 'https://brakemanscanner.org/docs/warning_types/cross_site_scripting/' + ) + end + end + + %i[external_type external_id name].each do |attribute| + context "when attribute #{attribute} is missing" do + before do + params.delete(attribute) + end + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError) + end + end + end + end + + describe '#key' do + let(:identifier) { create(:ci_reports_security_identifier) } + + subject { identifier.key } + + it 'returns fingerprint' do + is_expected.to eq(identifier.fingerprint) + end + end + + describe '#type_identifier?' do + where(:external_type, :expected_result) do + 'cve' | false + 'foo' | false + 'cwe' | true + 'wasc' | true + end + + with_them do + let(:identifier) { create(:ci_reports_security_identifier, external_type: external_type) } + + subject { identifier.type_identifier? } + + it { is_expected.to be(expected_result) } + end + end + + describe 'external type check methods' do + where(:external_type, :is_cve?, :is_cwe?, :is_wasc?) do + 'Foo' | false | false | false + 'Cve' | true | false | false + 'Cwe' | false | true | false + 'Wasc' | false | false | true + end + + with_them do + let(:identifier) { create(:ci_reports_security_identifier, external_type: external_type) } + + it 'returns correct result for the type check method' do + expect(identifier.cve?).to be(is_cve?) + expect(identifier.cwe?).to be(is_cwe?) + expect(identifier.wasc?).to be(is_wasc?) + end + end + end + + describe '#to_hash' do + let(:identifier) { create(:ci_reports_security_identifier) } + + subject { identifier.to_hash } + + it 'returns expected hash' do + is_expected.to eq({ + external_type: identifier.external_type, + external_id: identifier.external_id, + fingerprint: identifier.fingerprint, + name: identifier.name, + url: identifier.url + }) + end + end + + describe '#==' do + where(:type_1, :id_1, :type_2, :id_2, :equal, :case_name) do + 'CVE' | '2018-1234' | 'CVE' | '2018-1234' | true | 'when external_type and external_id are equal' + 'CVE' | '2018-1234' | 'brakeman_code' | '2018-1234' | false | 'when external_type is different' + 'CVE' | '2018-1234' | 'CVE' | '2019-6789' | false | 'when external_id is different' + end + + with_them do + let(:identifier_1) { create(:ci_reports_security_identifier, external_type: type_1, external_id: id_1) } + let(:identifier_2) { create(:ci_reports_security_identifier, external_type: type_2, external_id: id_2) } + + it "returns #{params[:equal]}" do + expect(identifier_1 == identifier_2).to eq(equal) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/link_spec.rb b/spec/lib/gitlab/ci/reports/security/link_spec.rb new file mode 100644 index 00000000000..7b55af27f4d --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/link_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Link do + subject(:security_link) { described_class.new(name: 'CVE-2020-0202', url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202') } + + describe '#initialize' do + context 'when all params are given' do + it 'initializes an instance' do + expect { subject }.not_to raise_error + + expect(subject).to have_attributes( + name: 'CVE-2020-0202', + url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202' + ) + end + end + + describe '#to_hash' do + it 'returns expected hash' do + expect(security_link.to_hash).to eq( + { + name: 'CVE-2020-0202', + url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-0202' + } + ) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb b/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb new file mode 100644 index 00000000000..e9daa05e8b9 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/scanned_resource_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::ScannedResource do + let(:url) { 'http://example.com:3001/1?foo=bar' } + let(:request_method) { 'GET' } + + context 'when the URI is not a URI' do + subject { ::Gitlab::Ci::Reports::Security::ScannedResource.new(url, request_method) } + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError) + end + end + + context 'when the URL is valid' do + subject { ::Gitlab::Ci::Reports::Security::ScannedResource.new(URI.parse(url), request_method) } + + it 'sets the URL attributes' do + expect(subject.request_method).to eq(request_method) + expect(subject.request_uri.to_s).to eq(url) + expect(subject.url_scheme).to eq('http') + expect(subject.url_host).to eq('example.com') + expect(subject.url_port).to eq(3001) + expect(subject.url_path).to eq('/1') + expect(subject.url_query).to eq('foo=bar') + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/scanner_spec.rb b/spec/lib/gitlab/ci/reports/security/scanner_spec.rb new file mode 100644 index 00000000000..02bb72e2c67 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/scanner_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Scanner do + describe '#initialize' do + subject { described_class.new(**params) } + + let(:params) do + { + external_id: 'brakeman', + name: 'Brakeman', + vendor: 'GitLab', + version: '1.0.1' + } + end + + context 'when all params are given' do + it 'initializes an instance' do + expect { subject }.not_to raise_error + + expect(subject).to have_attributes( + external_id: 'brakeman', + name: 'Brakeman', + vendor: 'GitLab' + ) + end + end + + %i[external_id name].each do |attribute| + context "when attribute #{attribute} is missing" do + before do + params.delete(attribute) + end + + it 'raises an error' do + expect { subject }.to raise_error(ArgumentError) + end + end + end + end + + describe '#key' do + let(:scanner) { create(:ci_reports_security_scanner) } + + subject { scanner.key } + + it 'returns external_id' do + is_expected.to eq(scanner.external_id) + end + end + + describe '#to_hash' do + let(:scanner) { create(:ci_reports_security_scanner) } + + subject { scanner.to_hash } + + it 'returns expected hash' do + is_expected.to eq({ + external_id: scanner.external_id, + name: scanner.name, + vendor: scanner.vendor + }) + end + + context 'when vendor is not defined' do + let(:scanner) { create(:ci_reports_security_scanner, vendor: nil) } + + it 'returns expected hash' do + is_expected.to eq({ + external_id: scanner.external_id, + name: scanner.name + }) + end + end + end + + describe '#==' do + using RSpec::Parameterized::TableSyntax + + where(:id_1, :id_2, :equal, :case_name) do + 'brakeman' | 'brakeman' | true | 'when external_id is equal' + 'brakeman' | 'bandit' | false | 'when external_id is different' + end + + with_them do + let(:scanner_1) { create(:ci_reports_security_scanner, external_id: id_1) } + let(:scanner_2) { create(:ci_reports_security_scanner, external_id: id_2) } + + it "returns #{params[:equal]}" do + expect(scanner_1 == scanner_2).to eq(equal) + end + end + end + + describe '#<=>' do + using RSpec::Parameterized::TableSyntax + + let(:scanner_1) { create(:ci_reports_security_scanner, **scanner_1_attributes) } + let(:scanner_2) { create(:ci_reports_security_scanner, **scanner_2_attributes) } + + subject { scanner_1 <=> scanner_2 } + + context 'when the `external_id` of the scanners are different' do + where(:scanner_1_attributes, :scanner_2_attributes, :expected_comparison_result) do + { external_id: 'bundler_audit', name: 'foo', vendor: 'bar' } | { external_id: 'retire.js', name: 'foo', vendor: 'bar' } | -1 + { external_id: 'retire.js', name: 'foo', vendor: 'bar' } | { external_id: 'gemnasium', name: 'foo', vendor: 'bar' } | -1 + { external_id: 'gemnasium', name: 'foo', vendor: 'bar' } | { external_id: 'gemnasium-maven', name: 'foo', vendor: 'bar' } | -1 + { external_id: 'gemnasium-maven', name: 'foo', vendor: 'bar' } | { external_id: 'gemnasium-python', name: 'foo', vendor: 'bar' } | -1 + { external_id: 'gemnasium-python', name: 'foo', vendor: 'bar' } | { external_id: 'bandit', name: 'foo', vendor: 'bar' } | 1 + { external_id: 'bandit', name: 'foo', vendor: 'bar' } | { external_id: 'semgrep', name: 'foo', vendor: 'bar' } | -1 + { external_id: 'semgrep', name: 'foo', vendor: 'bar' } | { external_id: 'unknown', name: 'foo', vendor: 'bar' } | -1 + end + + with_them do + it { is_expected.to eq(expected_comparison_result) } + end + end + + context 'when the `external_id` of the scanners are equal' do + context 'when the `name` of the scanners are different' do + where(:scanner_1_attributes, :scanner_2_attributes, :expected_comparison_result) do + { external_id: 'gemnasium', name: 'a', vendor: 'bar' } | { external_id: 'gemnasium', name: 'b', vendor: 'bar' } | -1 + { external_id: 'gemnasium', name: 'd', vendor: 'bar' } | { external_id: 'gemnasium', name: 'c', vendor: 'bar' } | 1 + end + + with_them do + it { is_expected.to eq(expected_comparison_result) } + end + end + + context 'when the `name` of the scanners are equal' do + where(:scanner_1_attributes, :scanner_2_attributes, :expected_comparison_result) do + { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | 0 # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | { external_id: 'gemnasium', name: 'foo', vendor: 'b' } | -1 + { external_id: 'gemnasium', name: 'foo', vendor: 'b' } | { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | 1 + end + + with_them do + it { is_expected.to eq(expected_comparison_result) } + end + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 9590307ba6e..630f2a189ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9154,6 +9154,11 @@ pako@~1.0.2, pako@~1.0.5: resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" integrity sha512-lQe48YPsMJAig+yngZ87Lus+NF+3mtu7DVOBu6b/gHO1YpKwIj5AWjZ/TOS7i46HD/UixzWb1zeWDZfGZ3iYcg== +papaparse@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.3.1.tgz#770b7a9124d821d4b2132132b7bd7dce7194b5b1" + integrity sha512-Dbt2yjLJrCwH2sRqKFFJaN5XgIASO9YOFeFP8rIBRG2Ain8mqk5r1M6DkfvqEVozVcz3r3HaUGw253hA1nLIcA== + parallel-transform@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06"