Commit 8ad1881c authored by Victor Zagorodny's avatar Victor Zagorodny Committed by Stan Hu

Add DB schema migrations for Vulnerability

This is the DB layout for first-class
vulnerabilities backend MVC. Added
vulnerabilities table, FK to it from
vulnerability_occurrences table, add all
required foreign keys for associations
between Vulnerability and other models
parent 3cc73052
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# #
# Contains common functionality shared between Issues and MergeRequests # Contains common functionality shared between Issues and MergeRequests
# #
# Used by Issue, MergeRequest # Used by Issue, MergeRequest, Epic
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
...@@ -26,6 +26,11 @@ module Issuable ...@@ -26,6 +26,11 @@ module Issuable
include IssuableStates include IssuableStates
include ClosedAtFilterable include ClosedAtFilterable
TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800
DESCRIPTION_LENGTH_MAX = 16000
DESCRIPTION_HTML_LENGTH_MAX = 48000
# This object is used to gather issuable meta data for displaying # This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests # upvotes, downvotes, notes and closing merge requests count for issues and merge requests
# lists avoiding n+1 queries and improving performance. # lists avoiding n+1 queries and improving performance.
...@@ -72,10 +77,15 @@ module Issuable ...@@ -72,10 +77,15 @@ module Issuable
prefix: true prefix: true
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: TITLE_LENGTH_MAX }
validates :description, length: { maximum: Gitlab::Database::MAX_TEXT_SIZE_LIMIT }, allow_blank: true # we validate the description against DESCRIPTION_LENGTH_MAX only for Issuables being created
# to avoid breaking the existing Issuables which may have their descriptions longer
validates :description, length: { maximum: DESCRIPTION_LENGTH_MAX }, allow_blank: true, on: :create
validate :description_max_length_for_new_records_is_valid, on: :update
validate :milestone_is_valid validate :milestone_is_valid
before_validation :truncate_description_on_import!
scope :authored, ->(user) { where(author_id: user) } scope :authored, ->(user) { where(author_id: user) }
scope :recent, -> { reorder(id: :desc) } scope :recent, -> { reorder(id: :desc) }
scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_projects, ->(ids) { where(project_id: ids) }
...@@ -138,6 +148,16 @@ module Issuable ...@@ -138,6 +148,16 @@ module Issuable
def milestone_is_valid def milestone_is_valid
errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available? errors.add(:milestone_id, message: "is invalid") if milestone_id.present? && !milestone_available?
end end
def description_max_length_for_new_records_is_valid
if new_record? && description.length > Issuable::DESCRIPTION_LENGTH_MAX
errors.add(:description, :too_long, count: Issuable::DESCRIPTION_LENGTH_MAX)
end
end
def truncate_description_on_import!
self.description = description&.slice(0, Issuable::DESCRIPTION_LENGTH_MAX) if importing?
end
end end
class_methods do class_methods do
......
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class CreateVulnerabilities < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
create_table :vulnerabilities do |t|
t.bigint "milestone_id"
t.bigint "epic_id"
t.bigint "project_id", null: false
t.bigint "author_id", null: false
t.bigint "updated_by_id"
t.bigint "last_edited_by_id"
t.bigint "start_date_sourcing_milestone_id"
t.bigint "due_date_sourcing_milestone_id"
t.bigint "closed_by_id"
t.datetime_with_timezone "last_edited_at"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.datetime_with_timezone "closed_at"
t.date "start_date"
t.date "due_date"
t.integer "state", limit: 2, default: 1, null: false # initially: open, closed
t.integer "severity", limit: 2, null: false # auto-calculated as highest-severity finding, but overrideable
t.integer "confidence", limit: 2, null: false # auto-calculated as lowest-confidence finding, but overrideable
t.boolean "severity_overridden", default: false
t.boolean "confidence_overridden", default: false
t.string "title", limit: 255, null: false
t.text "title_html", null: false
t.text "description"
t.text "description_html"
end
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddReferenceFromVulnerabilityOccurrencesToOccurrences < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :vulnerability_occurrences, :vulnerability_id, :bigint
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddForeignKeysAndIndexesToVulnerabilities < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :vulnerabilities, :milestone_id
add_concurrent_foreign_key :vulnerabilities, :milestones, column: :milestone_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :epic_id
add_concurrent_foreign_key :vulnerabilities, :epics, column: :epic_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :project_id
add_concurrent_foreign_key :vulnerabilities, :projects, column: :project_id
add_concurrent_index :vulnerabilities, :author_id
add_concurrent_foreign_key :vulnerabilities, :users, column: :author_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :updated_by_id
add_concurrent_foreign_key :vulnerabilities, :users, column: :updated_by_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :last_edited_by_id
add_concurrent_foreign_key :vulnerabilities, :users, column: :last_edited_by_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :closed_by_id
add_concurrent_foreign_key :vulnerabilities, :users, column: :closed_by_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :start_date_sourcing_milestone_id
add_concurrent_foreign_key :vulnerabilities, :milestones, column: :start_date_sourcing_milestone_id, on_delete: :nullify
add_concurrent_index :vulnerabilities, :due_date_sourcing_milestone_id
add_concurrent_foreign_key :vulnerabilities, :milestones, column: :due_date_sourcing_milestone_id, on_delete: :nullify
add_concurrent_index :vulnerability_occurrences, :vulnerability_id
add_concurrent_foreign_key :vulnerability_occurrences, :vulnerabilities, column: :vulnerability_id, on_delete: :nullify
end
def down
remove_foreign_key :vulnerability_occurrences, :vulnerabilities
remove_concurrent_index :vulnerability_occurrences, :vulnerability_id
remove_foreign_key :vulnerabilities, column: :due_date_sourcing_milestone_id
remove_concurrent_index :vulnerabilities, :due_date_sourcing_milestone_id
remove_foreign_key :vulnerabilities, column: :start_date_sourcing_milestone_id
remove_concurrent_index :vulnerabilities, :start_date_sourcing_milestone_id
remove_foreign_key :vulnerabilities, column: :closed_by_id
remove_concurrent_index :vulnerabilities, :closed_by_id
remove_foreign_key :vulnerabilities, column: :last_edited_by_id
remove_concurrent_index :vulnerabilities, :last_edited_by_id
remove_foreign_key :vulnerabilities, column: :updated_by_id
remove_concurrent_index :vulnerabilities, :updated_by_id
remove_foreign_key :vulnerabilities, column: :author_id
remove_concurrent_index :vulnerabilities, :author_id
remove_foreign_key :vulnerabilities, :projects
remove_concurrent_index :vulnerabilities, :project_id
remove_foreign_key :vulnerabilities, :epics
remove_concurrent_index :vulnerabilities, :epic_id
remove_foreign_key :vulnerabilities, :milestones
remove_concurrent_index :vulnerabilities, :milestone_id
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_09_27_074328) do ActiveRecord::Schema.define(version: 2019_09_29_180827) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -3705,6 +3705,42 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do ...@@ -3705,6 +3705,42 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do
t.index ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true t.index ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true
end end
create_table "vulnerabilities", force: :cascade do |t|
t.bigint "milestone_id"
t.bigint "epic_id"
t.bigint "project_id", null: false
t.bigint "author_id", null: false
t.bigint "updated_by_id"
t.bigint "last_edited_by_id"
t.date "start_date"
t.date "due_date"
t.datetime_with_timezone "last_edited_at"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "title", limit: 255, null: false
t.text "title_html", null: false
t.text "description"
t.text "description_html"
t.bigint "start_date_sourcing_milestone_id"
t.bigint "due_date_sourcing_milestone_id"
t.bigint "closed_by_id"
t.datetime_with_timezone "closed_at"
t.integer "state", limit: 2, default: 1, null: false
t.integer "severity", limit: 2, null: false
t.boolean "severity_overridden", default: false
t.integer "confidence", limit: 2, null: false
t.boolean "confidence_overridden", default: false
t.index ["author_id"], name: "index_vulnerabilities_on_author_id"
t.index ["closed_by_id"], name: "index_vulnerabilities_on_closed_by_id"
t.index ["due_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_due_date_sourcing_milestone_id"
t.index ["epic_id"], name: "index_vulnerabilities_on_epic_id"
t.index ["last_edited_by_id"], name: "index_vulnerabilities_on_last_edited_by_id"
t.index ["milestone_id"], name: "index_vulnerabilities_on_milestone_id"
t.index ["project_id"], name: "index_vulnerabilities_on_project_id"
t.index ["start_date_sourcing_milestone_id"], name: "index_vulnerabilities_on_start_date_sourcing_milestone_id"
t.index ["updated_by_id"], name: "index_vulnerabilities_on_updated_by_id"
end
create_table "vulnerability_feedback", id: :serial, force: :cascade do |t| create_table "vulnerability_feedback", id: :serial, force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
...@@ -3772,10 +3808,12 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do ...@@ -3772,10 +3808,12 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do
t.string "name", null: false t.string "name", null: false
t.string "metadata_version", null: false t.string "metadata_version", null: false
t.text "raw_metadata", null: false t.text "raw_metadata", null: false
t.bigint "vulnerability_id"
t.index ["primary_identifier_id"], name: "index_vulnerability_occurrences_on_primary_identifier_id" t.index ["primary_identifier_id"], name: "index_vulnerability_occurrences_on_primary_identifier_id"
t.index ["project_id", "primary_identifier_id", "location_fingerprint", "scanner_id"], name: "index_vulnerability_occurrences_on_unique_keys", unique: true t.index ["project_id", "primary_identifier_id", "location_fingerprint", "scanner_id"], name: "index_vulnerability_occurrences_on_unique_keys", unique: true
t.index ["scanner_id"], name: "index_vulnerability_occurrences_on_scanner_id" t.index ["scanner_id"], name: "index_vulnerability_occurrences_on_scanner_id"
t.index ["uuid"], name: "index_vulnerability_occurrences_on_uuid", unique: true t.index ["uuid"], name: "index_vulnerability_occurrences_on_uuid", unique: true
t.index ["vulnerability_id"], name: "index_vulnerability_occurrences_on_vulnerability_id"
end end
create_table "vulnerability_scanners", force: :cascade do |t| create_table "vulnerability_scanners", force: :cascade do |t|
...@@ -4202,6 +4240,15 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do ...@@ -4202,6 +4240,15 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do
add_foreign_key "users_ops_dashboard_projects", "projects", on_delete: :cascade add_foreign_key "users_ops_dashboard_projects", "projects", on_delete: :cascade
add_foreign_key "users_ops_dashboard_projects", "users", on_delete: :cascade add_foreign_key "users_ops_dashboard_projects", "users", on_delete: :cascade
add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade
add_foreign_key "vulnerabilities", "epics", name: "fk_1d37cddf91", on_delete: :nullify
add_foreign_key "vulnerabilities", "milestones", column: "due_date_sourcing_milestone_id", name: "fk_7c5bb22a22", on_delete: :nullify
add_foreign_key "vulnerabilities", "milestones", column: "start_date_sourcing_milestone_id", name: "fk_88b4d546ef", on_delete: :nullify
add_foreign_key "vulnerabilities", "milestones", name: "fk_131d289c65", on_delete: :nullify
add_foreign_key "vulnerabilities", "projects", name: "fk_efb96ab1e2", on_delete: :cascade
add_foreign_key "vulnerabilities", "users", column: "author_id", name: "fk_b1de915a15", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "closed_by_id", name: "fk_cf5c60acbf", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "last_edited_by_id", name: "fk_1302949740", on_delete: :nullify
add_foreign_key "vulnerabilities", "users", column: "updated_by_id", name: "fk_7ac31eacb9", on_delete: :nullify
add_foreign_key "vulnerability_feedback", "ci_pipelines", column: "pipeline_id", on_delete: :nullify add_foreign_key "vulnerability_feedback", "ci_pipelines", column: "pipeline_id", on_delete: :nullify
add_foreign_key "vulnerability_feedback", "issues", on_delete: :nullify add_foreign_key "vulnerability_feedback", "issues", on_delete: :nullify
add_foreign_key "vulnerability_feedback", "merge_requests", name: "fk_563ff1912e", on_delete: :nullify add_foreign_key "vulnerability_feedback", "merge_requests", name: "fk_563ff1912e", on_delete: :nullify
...@@ -4214,6 +4261,7 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do ...@@ -4214,6 +4261,7 @@ ActiveRecord::Schema.define(version: 2019_09_27_074328) do
add_foreign_key "vulnerability_occurrence_pipelines", "ci_pipelines", column: "pipeline_id", on_delete: :cascade add_foreign_key "vulnerability_occurrence_pipelines", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
add_foreign_key "vulnerability_occurrence_pipelines", "vulnerability_occurrences", column: "occurrence_id", on_delete: :cascade add_foreign_key "vulnerability_occurrence_pipelines", "vulnerability_occurrences", column: "occurrence_id", on_delete: :cascade
add_foreign_key "vulnerability_occurrences", "projects", on_delete: :cascade add_foreign_key "vulnerability_occurrences", "projects", on_delete: :cascade
add_foreign_key "vulnerability_occurrences", "vulnerabilities", name: "fk_97ffe77653", on_delete: :nullify
add_foreign_key "vulnerability_occurrences", "vulnerability_identifiers", column: "primary_identifier_id", on_delete: :cascade add_foreign_key "vulnerability_occurrences", "vulnerability_identifiers", column: "primary_identifier_id", on_delete: :cascade
add_foreign_key "vulnerability_occurrences", "vulnerability_scanners", column: "scanner_id", on_delete: :cascade add_foreign_key "vulnerability_occurrences", "vulnerability_scanners", column: "scanner_id", on_delete: :cascade
add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade add_foreign_key "vulnerability_scanners", "projects", on_delete: :cascade
......
...@@ -34,6 +34,7 @@ description: 'Learn how to contribute to GitLab.' ...@@ -34,6 +34,7 @@ description: 'Learn how to contribute to GitLab.'
## Backend guides ## Backend guides
- [GitLab utilities](utilities.md) - [GitLab utilities](utilities.md)
- [Issuable-like Rails models](issuable-like-models.md)
- [Logging](logging.md) - [Logging](logging.md)
- [API styleguide](api_styleguide.md) Use this styleguide if you are - [API styleguide](api_styleguide.md) Use this styleguide if you are
contributing to the API contributing to the API
......
# Issuable-like Rails models utilities
GitLab Rails codebase contains several models that hold common functionality and behave similarly to an [Issue]. Other
examples of `Issuable`s are [Merge Requests] and [Epics].
This guide accumulates guidelines on working with such Rails models.
## Important text fields
There are max length constraints for the most important text fields for `Issuable`s:
- `title`: 255 chars
- `title_html`: 800 chars
- `description`: 16000 chars
- `description_html`: 48000 chars
[Issue]: https://docs.gitlab.com/ee/user/project/issues
[Merge Requests]: https://docs.gitlab.com/ee/user/project/merge_requests
[Epics]: https://docs.gitlab.com/ee/user/group/epics
...@@ -17,7 +17,7 @@ module EE ...@@ -17,7 +17,7 @@ module EE
has_many :job_artifacts, through: :builds has_many :job_artifacts, through: :builds
has_many :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::OccurrencePipeline' has_many :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::OccurrencePipeline'
has_many :vulnerabilities, source: :occurrence, through: :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::Occurrence' has_many :vulnerability_findings, source: :occurrence, through: :vulnerabilities_occurrence_pipelines, class_name: 'Vulnerabilities::Occurrence'
has_one :source_pipeline, class_name: "::Ci::Sources::Pipeline", inverse_of: :pipeline has_one :source_pipeline, class_name: "::Ci::Sources::Pipeline", inverse_of: :pipeline
has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_pipeline_id has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline", foreign_key: :source_pipeline_id
......
...@@ -61,10 +61,15 @@ module EE ...@@ -61,10 +61,15 @@ module EE
has_many :audit_events, as: :entity has_many :audit_events, as: :entity
has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design' has_many :designs, inverse_of: :project, class_name: 'DesignManagement::Design'
has_many :path_locks has_many :path_locks
# the rationale behind vulnerabilities and vulnerability_findings can be found here:
# https://gitlab.com/gitlab-org/gitlab/issues/10252#terminology
has_many :vulnerabilities
has_many :vulnerability_feedback, class_name: 'Vulnerabilities::Feedback' has_many :vulnerability_feedback, class_name: 'Vulnerabilities::Feedback'
has_many :vulnerabilities, class_name: 'Vulnerabilities::Occurrence' has_many :vulnerability_findings, class_name: 'Vulnerabilities::Occurrence'
has_many :vulnerability_identifiers, class_name: 'Vulnerabilities::Identifier' has_many :vulnerability_identifiers, class_name: 'Vulnerabilities::Identifier'
has_many :vulnerability_scanners, class_name: 'Vulnerabilities::Scanner' has_many :vulnerability_scanners, class_name: 'Vulnerabilities::Scanner'
has_many :protected_environments has_many :protected_environments
has_many :software_license_policies, inverse_of: :project, class_name: 'SoftwareLicensePolicy' has_many :software_license_policies, inverse_of: :project, class_name: 'SoftwareLicensePolicy'
accepts_nested_attributes_for :software_license_policies, allow_destroy: true accepts_nested_attributes_for :software_license_policies, allow_destroy: true
......
# frozen_string_literal: true
class Vulnerability < ApplicationRecord
belongs_to :project
belongs_to :milestone
belongs_to :epic
belongs_to :author, class_name: 'User'
belongs_to :updated_by, class_name: 'User'
belongs_to :last_edited_by, class_name: 'User'
belongs_to :closed_by, class_name: 'User'
enum state: { opened: 1, closed: 2 }
enum severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS, _prefix: :severity
enum confidence: Vulnerabilities::Occurrence::CONFIDENCE_LEVELS, _prefix: :confidence
validates :project, :author, :title, :title_html, :severity, :confidence, presence: true
# at this stage Vulnerability is not an Issuable, has some important attributes (and their constraints) in common
validates :title, length: { maximum: Issuable::TITLE_LENGTH_MAX }
validates :title_html, length: { maximum: Issuable::TITLE_HTML_LENGTH_MAX }, allow_blank: true
validates :description, length: { maximum: Issuable::DESCRIPTION_LENGTH_MAX }, allow_blank: true
validates :description_html, length: { maximum: Issuable::DESCRIPTION_HTML_LENGTH_MAX }, allow_blank: true
end
...@@ -26,17 +26,17 @@ module Security ...@@ -26,17 +26,17 @@ module Security
private private
def executed? def executed?
pipeline.vulnerabilities.report_type(@report.type).any? pipeline.vulnerability_findings.report_type(@report.type).any?
end end
def create_all_vulnerabilities! def create_all_vulnerabilities!
@report.occurrences.each do |occurrence| @report.occurrences.each do |occurrence|
create_vulnerability(occurrence) create_vulnerability_finding(occurrence)
end end
end end
def create_vulnerability(occurrence) def create_vulnerability_finding(occurrence)
vulnerability = create_or_find_vulnerability_object(occurrence) vulnerability = create_or_find_vulnerability_finding(occurrence)
occurrence.identifiers.map do |identifier| occurrence.identifiers.map do |identifier|
create_vulnerability_identifier_object(vulnerability, identifier) create_vulnerability_identifier_object(vulnerability, identifier)
...@@ -46,7 +46,7 @@ module Security ...@@ -46,7 +46,7 @@ module Security
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def create_or_find_vulnerability_object(occurrence) def create_or_find_vulnerability_finding(occurrence)
find_params = { find_params = {
scanner: scanners_objects[occurrence.scanner.key], scanner: scanners_objects[occurrence.scanner.key],
primary_identifier: identifiers_objects[occurrence.primary_identifier.key], primary_identifier: identifiers_objects[occurrence.primary_identifier.key],
...@@ -57,11 +57,12 @@ module Security ...@@ -57,11 +57,12 @@ module Security
.except(:compare_key, :identifiers, :location, :scanner) .except(:compare_key, :identifiers, :location, :scanner)
begin begin
project.vulnerabilities project
.vulnerability_findings
.create_with(create_params) .create_with(create_params)
.find_or_create_by!(find_params) .find_or_create_by!(find_params)
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
project.vulnerabilities.find_by!(find_params) project.vulnerability_findings.find_by!(find_params)
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -35,7 +35,7 @@ class Gitlab::Seeder::Vulnerabilities ...@@ -35,7 +35,7 @@ class Gitlab::Seeder::Vulnerabilities
private private
def create_occurrence(rank, primary_identifier) def create_occurrence(rank, primary_identifier)
project.vulnerabilities.create!( project.vulnerability_findings.create!(
uuid: random_uuid, uuid: random_uuid,
name: 'Cipher with no integrity', name: 'Cipher with no integrity',
report_type: :sast, report_type: :sast,
......
# frozen_string_literal: true
FactoryBot.define do
factory :vulnerability do
project
author
title { generate(:title) }
title_html { "<h2>#{title}</h2>" }
severity { :high }
confidence { :medium }
trait :opened do
state { :opened }
end
trait :closed do
state { :closed }
closed_at { Time.now }
end
end
end
...@@ -16,7 +16,7 @@ describe Ci::Pipeline do ...@@ -16,7 +16,7 @@ describe Ci::Pipeline do
it { is_expected.to have_many(:triggered_pipelines) } it { is_expected.to have_many(:triggered_pipelines) }
it { is_expected.to have_many(:downstream_bridges) } it { is_expected.to have_many(:downstream_bridges) }
it { is_expected.to have_many(:job_artifacts).through(:builds) } it { is_expected.to have_many(:job_artifacts).through(:builds) }
it { is_expected.to have_many(:vulnerabilities).through(:vulnerabilities_occurrence_pipelines).class_name('Vulnerabilities::Occurrence') } it { is_expected.to have_many(:vulnerability_findings).through(:vulnerabilities_occurrence_pipelines).class_name('Vulnerabilities::Occurrence') }
it { is_expected.to have_many(:vulnerabilities_occurrence_pipelines).class_name('Vulnerabilities::OccurrencePipeline') } it { is_expected.to have_many(:vulnerabilities_occurrence_pipelines).class_name('Vulnerabilities::OccurrencePipeline') }
describe '.failure_reasons' do describe '.failure_reasons' do
......
...@@ -15,7 +15,10 @@ describe EE::Issuable do ...@@ -15,7 +15,10 @@ describe EE::Issuable do
it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(255) } it { is_expected.to validate_length_of(:title).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1_000_000) } it { is_expected.to validate_length_of(:description).is_at_most(16_000).on(:create) }
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'truncates the description to its allowed maximum length on import'
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Vulnerability do
let(:severity_values) do
{ undefined: 0,
info: 1,
unknown: 2,
low: 4,
medium: 5,
high: 6,
critical: 7 }
end
let(:confidence_values) do
{ undefined: 0,
ignore: 1,
unknown: 2,
experimental: 3,
low: 4,
medium: 5,
high: 6,
confirmed: 7 }
end
it { is_expected.to define_enum_for(:state) }
it { is_expected.to define_enum_for(:severity).with_values(severity_values).with_prefix(:severity) }
it { is_expected.to define_enum_for(:confidence).with_values(confidence_values).with_prefix(:confidence) }
describe 'associations' do
subject { build(:vulnerability) }
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:milestone) }
it { is_expected.to belong_to(:epic) }
it { is_expected.to belong_to(:author).class_name('User') }
it { is_expected.to belong_to(:updated_by).class_name('User') }
it { is_expected.to belong_to(:last_edited_by).class_name('User') }
it { is_expected.to belong_to(:closed_by).class_name('User') }
end
describe 'validations' do
subject { build(:vulnerability) }
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:title_html) }
it { is_expected.to validate_presence_of(:severity) }
it { is_expected.to validate_presence_of(:confidence) }
it { is_expected.to validate_length_of(:title).is_at_most(255) }
it { is_expected.to validate_length_of(:title_html).is_at_most(800) }
it { is_expected.to validate_length_of(:description).is_at_most(16_000).allow_nil }
it { is_expected.to validate_length_of(:description_html).is_at_most(48_000).allow_nil }
end
end
This diff is collapsed.
...@@ -172,7 +172,7 @@ ci_pipelines: ...@@ -172,7 +172,7 @@ ci_pipelines:
- downstream_bridges - downstream_bridges
- job_artifacts - job_artifacts
- vulnerabilities_occurrence_pipelines - vulnerabilities_occurrence_pipelines
- vulnerabilities - vulnerability_findings
pipeline_variables: pipeline_variables:
- pipeline - pipeline
stages: stages:
...@@ -389,6 +389,7 @@ project: ...@@ -389,6 +389,7 @@ project:
- sourced_pipelines - sourced_pipelines
- prometheus_metrics - prometheus_metrics
- vulnerabilities - vulnerabilities
- vulnerability_findings
- vulnerability_feedback - vulnerability_feedback
- vulnerability_identifiers - vulnerability_identifiers
- vulnerability_scanners - vulnerability_scanners
......
...@@ -46,7 +46,10 @@ describe Issuable do ...@@ -46,7 +46,10 @@ describe Issuable do
it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:author) }
it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_length_of(:title).is_at_most(255) } it { is_expected.to validate_length_of(:title).is_at_most(255) }
it { is_expected.to validate_length_of(:description).is_at_most(1_000_000) } it { is_expected.to validate_length_of(:description).is_at_most(16_000).on(:create) }
it_behaves_like 'validates description length with custom validation'
it_behaves_like 'truncates the description to its allowed maximum length on import'
end end
describe 'milestone' do describe 'milestone' do
......
# frozen_string_literal: true
shared_examples_for 'matches_cross_reference_regex? fails fast' do shared_examples_for 'matches_cross_reference_regex? fails fast' do
it 'fails fast for long strings' do it 'fails fast for long strings' do
# took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823 # took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823
...@@ -6,3 +8,59 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do ...@@ -6,3 +8,59 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do
end.not_to raise_error end.not_to raise_error
end end
end end
shared_examples_for 'validates description length with custom validation' do
let(:issuable) { build(:issue, description: 'x' * 16_001) }
let(:context) { :update }
subject { issuable.validate(context) }
context 'when Issuable is a new record' do
it 'validates the maximum description length' do
subject
expect(issuable.errors[:description]).to eq(["is too long (maximum is 16000 characters)"])
end
context 'on create' do
let(:context) { :create }
it 'does not validate the maximum description length' do
allow(issuable).to receive(:description_max_length_for_new_records_is_valid).and_call_original
subject
expect(issuable).not_to have_received(:description_max_length_for_new_records_is_valid)
end
end
end
context 'when Issuable is an existing record' do
before do
allow(issuable).to receive(:expire_etag_cache) # to skip the expire_etag_cache callback
issuable.save!(validate: false)
end
it 'does not validate the maximum description length' do
subject
expect(issuable.errors).not_to have_key(:description)
end
end
end
shared_examples_for 'truncates the description to its allowed maximum length on import' do
before do
allow(issuable).to receive(:importing?).and_return(true)
end
let(:issuable) { build(:issue, description: 'x' * 16_001) }
subject { issuable.validate(:create) }
it 'truncates the description to its allowed maximum length' do
subject
expect(issuable.description).to eq('x' * 16_000)
expect(issuable.errors[:description]).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