Commit 3c1d1601 authored by Jonathan Schafer's avatar Jonathan Schafer Committed by Steve Abrams

Add entity columns to vulnerability occurrences

Add the following columns to vulerability_occurrences table:
- descriuption
- message
- solution
- cve
- location
parent 483b6276
---
title: Add entity columns to vulnerability occurrences
merge_request: 51739
author:
type: changed
# frozen_string_literal: true
class AddEntityColumnsToVulnerabilityOccurrences < ActiveRecord::Migration[6.0]
DOWNTIME = false
# rubocop:disable Migration/AddLimitToTextColumns
# limit is added in 20200501000002_add_text_limit_to_sprints_extended_title
def change
add_column :vulnerability_occurrences, :description, :text
add_column :vulnerability_occurrences, :message, :text
add_column :vulnerability_occurrences, :solution, :text
add_column :vulnerability_occurrences, :cve, :text
add_column :vulnerability_occurrences, :location, :jsonb
end
# rubocop:enable Migration/AddLimitToTextColumns
end
# frozen_string_literal: true
class AddTextLimitToVulnerabilityOccurrencesEntityColumns < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_text_limit :vulnerability_occurrences, :description, 15000
add_text_limit :vulnerability_occurrences, :message, 3000
add_text_limit :vulnerability_occurrences, :solution, 7000
add_text_limit :vulnerability_occurrences, :cve, 48400
end
def down
remove_text_limit :vulnerability_occurrences, :description
remove_text_limit :vulnerability_occurrences, :message
remove_text_limit :vulnerability_occurrences, :solution
remove_text_limit :vulnerability_occurrences, :cve
end
end
7a252c5d76c1e71421c3aa3e01584cdeeec6a5002ba6ef0824674c64f92e2764
\ No newline at end of file
9327676097c49bb1a221d79dd351ad8c57a434f19e32f49951c0d6d655c2fa4e
\ No newline at end of file
...@@ -18009,7 +18009,16 @@ CREATE TABLE vulnerability_occurrences ( ...@@ -18009,7 +18009,16 @@ CREATE TABLE vulnerability_occurrences (
metadata_version character varying NOT NULL, metadata_version character varying NOT NULL,
raw_metadata text NOT NULL, raw_metadata text NOT NULL,
vulnerability_id bigint, vulnerability_id bigint,
details jsonb DEFAULT '{}'::jsonb NOT NULL details jsonb DEFAULT '{}'::jsonb NOT NULL,
description text,
message text,
solution text,
cve text,
location jsonb,
CONSTRAINT check_4a3a60f2ba CHECK ((char_length(solution) <= 7000)),
CONSTRAINT check_ade261da6b CHECK ((char_length(description) <= 15000)),
CONSTRAINT check_df6dd20219 CHECK ((char_length(message) <= 3000)),
CONSTRAINT check_f602da68dd CHECK ((char_length(cve) <= 48400))
); );
CREATE SEQUENCE vulnerability_occurrences_id_seq CREATE SEQUENCE vulnerability_occurrences_id_seq
......
...@@ -62,6 +62,11 @@ module Vulnerabilities ...@@ -62,6 +62,11 @@ module Vulnerabilities
validates :raw_metadata, presence: true validates :raw_metadata, presence: true
validates :details, json_schema: { filename: 'vulnerability_finding_details', draft: 7 } validates :details, json_schema: { filename: 'vulnerability_finding_details', draft: 7 }
validates :description, length: { maximum: 15000 }
validates :message, length: { maximum: 3000 }
validates :solution, length: { maximum: 7000 }
validates :cve, length: { maximum: 48400 }
delegate :name, :external_id, to: :scanner, prefix: true, allow_nil: true delegate :name, :external_id, to: :scanner, prefix: true, allow_nil: true
scope :report_type, -> (type) { where(report_type: report_types[type]) } scope :report_type, -> (type) { where(report_type: report_types[type]) }
...@@ -217,15 +222,15 @@ module Vulnerabilities ...@@ -217,15 +222,15 @@ module Vulnerabilities
end end
def description def description
metadata.dig('description') super.presence || metadata.dig('description')
end end
def solution def solution
metadata.dig('solution') || remediations&.first&.dig('summary') super.presence || metadata.dig('solution') || remediations&.first&.dig('summary')
end end
def location def location
metadata.fetch('location', {}) super.presence || metadata.fetch('location', {})
end end
def file def file
...@@ -309,11 +314,11 @@ module Vulnerabilities ...@@ -309,11 +314,11 @@ module Vulnerabilities
end end
def message def message
metadata.dig('message') super.presence || metadata.dig('message')
end end
def cve_value def cve_value
identifiers.find(&:cve?)&.name cve || identifiers.find(&:cve?)&.name
end end
def cwe_value def cwe_value
......
...@@ -50,7 +50,8 @@ module Security ...@@ -50,7 +50,8 @@ module Security
end end
vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan, :links) vulnerability_params = finding.to_hash.except(:compare_key, :identifiers, :location, :scanner, :scan, :links)
vulnerability_finding = create_or_find_vulnerability_finding(finding, vulnerability_params) entity_params = Gitlab::Json.parse(vulnerability_params&.dig(:raw_metadata)).slice('description', 'message', 'solution', 'cve', 'location')
vulnerability_finding = create_or_find_vulnerability_finding(finding, vulnerability_params.merge(entity_params))
update_vulnerability_scanner(finding) update_vulnerability_scanner(finding)
......
...@@ -37,6 +37,11 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -37,6 +37,11 @@ RSpec.describe Vulnerabilities::Finding do
it { is_expected.to validate_presence_of(:severity) } it { is_expected.to validate_presence_of(:severity) }
it { is_expected.to validate_presence_of(:confidence) } it { is_expected.to validate_presence_of(:confidence) }
it { is_expected.to validate_length_of(:description).is_at_most(15000) }
it { is_expected.to validate_length_of(:message).is_at_most(3000) }
it { is_expected.to validate_length_of(:solution).is_at_most(7000) }
it { is_expected.to validate_length_of(:cve).is_at_most(48400) }
context 'when value for details field is valid' do context 'when value for details field is valid' do
it 'is valid' do it 'is valid' do
finding.details = {} finding.details = {}
...@@ -453,26 +458,34 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -453,26 +458,34 @@ RSpec.describe Vulnerabilities::Finding do
end end
describe '#remediations' do describe '#remediations' do
let(:raw_remediation) { { summary: 'foo', diff: 'bar' }.stringify_keys } let_it_be(:project) { create_default(:project) }
let(:raw_metadata) { { remediations: [raw_remediation] }.to_json } let_it_be(:finding, refind: true) { create(:vulnerabilities_finding) }
let(:finding) { create(:vulnerabilities_finding, raw_metadata: raw_metadata) }
subject { finding.remediations } subject { finding.remediations }
context 'when the finding has associated remediation records' do context 'when the finding has associated remediation records' do
let!(:persisted_remediation) { create(:vulnerabilities_remediation, findings: [finding]) } let_it_be(:persisted_remediation) { create(:vulnerabilities_remediation, findings: [finding]) }
let(:remediation_hash) { { 'summary' => persisted_remediation.summary, 'diff' => persisted_remediation.diff } } let_it_be(:remediation_hash) { { 'summary' => persisted_remediation.summary, 'diff' => persisted_remediation.diff } }
it { is_expected.to eq([remediation_hash]) } it { is_expected.to eq([remediation_hash]) }
end end
context 'when the finding does not have associated remediation records' do context 'when the finding does not have associated remediation records' do
context 'when the finding has remediations in `raw_metadata`' do context 'when the finding has remediations in `raw_metadata`' do
let(:raw_remediation) { { summary: 'foo', diff: 'bar' }.stringify_keys }
before do
raw_metadata = { remediations: [raw_remediation] }.to_json
finding.update!(raw_metadata: raw_metadata)
end
it { is_expected.to eq([raw_remediation]) } it { is_expected.to eq([raw_remediation]) }
end end
context 'when the finding does not have remediations in `raw_metadata`' do context 'when the finding does not have remediations in `raw_metadata`' do
let(:raw_metadata) { {}.to_json } before do
finding.update!(raw_metadata: {}.to_json)
end
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
...@@ -689,6 +702,23 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -689,6 +702,23 @@ RSpec.describe Vulnerabilities::Finding do
it { is_expected.to eq(vulnerabilities_finding.scanner.name) } it { is_expected.to eq(vulnerabilities_finding.scanner.name) }
end end
describe '#description' do
let(:finding) { build(:vulnerabilities_finding) }
let(:expected_description) { finding.metadata['description'] }
subject { finding.description }
context 'when description metadata key is present' do
it { is_expected.to eql(expected_description) }
end
context 'when description data is present' do
let(:finding) { build(:vulnerabilities_finding, description: 'Vulnerability description') }
it { is_expected.to eq('Vulnerability description') }
end
end
describe '#solution' do describe '#solution' do
subject { vulnerabilities_finding.solution } subject { vulnerabilities_finding.solution }
...@@ -698,13 +728,37 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -698,13 +728,37 @@ RSpec.describe Vulnerabilities::Finding do
it { is_expected.to eq(vulnerabilities_finding.metadata['solution']) } it { is_expected.to eq(vulnerabilities_finding.metadata['solution']) }
end end
context 'when remediations key is present' do context 'when remediations key is present in finding' do
let(:vulnerabilities_finding) do let(:vulnerabilities_finding) do
build(:vulnerabilities_finding_with_remediation, summary: "Test remediation") build(:vulnerabilities_finding_with_remediation, summary: "Test remediation")
end end
it { is_expected.to eq(vulnerabilities_finding.remediations.dig(0, 'summary')) } it { is_expected.to eq(vulnerabilities_finding.remediations.dig(0, 'summary')) }
end end
context 'when solution data is present' do
let(:vulnerabilities_finding) { build(:vulnerabilities_finding, solution: 'Vulnerability solution') }
it { is_expected.to eq('Vulnerability solution') }
end
end
describe '#location' do
let(:finding) { build(:vulnerabilities_finding) }
let(:expected_location) { finding.metadata['location'] }
subject { finding.location }
context 'when location metadata key is present' do
it { is_expected.to eql(expected_location) }
end
context 'when location data is present' do
let(:location) { { 'class' => 'class', 'end_line' => 3, 'file' => 'test_file.rb', 'start_line' => 1 } }
let(:finding) { build(:vulnerabilities_finding, location: location) }
it { is_expected.to eq(location) }
end
end end
describe '#evidence' do describe '#evidence' do
...@@ -810,9 +864,17 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -810,9 +864,17 @@ RSpec.describe Vulnerabilities::Finding do
subject { finding.message } subject { finding.message }
context 'when message metadata key is present' do
it { is_expected.to eql(expected_message) } it { is_expected.to eql(expected_message) }
end end
context 'when message data is present' do
let(:finding) { build(:vulnerabilities_finding, message: 'Vulnerability message') }
it { is_expected.to eq('Vulnerability message') }
end
end
describe '#cve_value' do describe '#cve_value' do
let(:finding) { build(:vulnerabilities_finding) } let(:finding) { build(:vulnerabilities_finding) }
let(:expected_cve) { 'CVE-2020-0000' } let(:expected_cve) { 'CVE-2020-0000' }
...@@ -823,9 +885,17 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -823,9 +885,17 @@ RSpec.describe Vulnerabilities::Finding do
finding.identifiers << build(:vulnerabilities_identifier, external_type: 'cve', name: expected_cve) finding.identifiers << build(:vulnerabilities_identifier, external_type: 'cve', name: expected_cve)
end end
context 'when cve metadata key is present' do
it { is_expected.to eql(expected_cve) } it { is_expected.to eql(expected_cve) }
end end
context 'when cve data is present' do
let(:finding) { build(:vulnerabilities_finding, cve: 'Vulnerability cve') }
it { is_expected.to eq('Vulnerability cve') }
end
end
describe '#cwe_value' do describe '#cwe_value' do
let(:finding) { build(:vulnerabilities_finding) } let(:finding) { build(:vulnerabilities_finding) }
let(:expected_cwe) { 'CWE-0000' } let(:expected_cwe) { 'CWE-0000' }
......
...@@ -67,6 +67,24 @@ RSpec.describe Security::StoreReportService, '#execute' do ...@@ -67,6 +67,24 @@ RSpec.describe Security::StoreReportService, '#execute' do
end end
end end
context 'when report data includes all raw_metadata' do
let(:trait) { :dependency_scanning_remediation }
it 'inserts top level finding data', :aggregate_failures do
subject
finding = Vulnerabilities::Finding.last
finding.raw_metadata = nil
expect(finding.metadata).to be_blank
expect(finding.cve).not_to be_nil
expect(finding.description).not_to be_nil
expect(finding.location).not_to be_nil
expect(finding.message).not_to be_nil
expect(finding.solution).not_to be_nil
end
end
context 'invalid data' do context 'invalid data' do
let(:artifact) { create(:ee_ci_job_artifact, :sast) } let(:artifact) { create(:ee_ci_job_artifact, :sast) }
let(:finding_without_name) { build(:ci_reports_security_finding, name: nil) } let(:finding_without_name) { build(:ci_reports_security_finding, name: nil) }
......
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