debian-mirror-gitlab/spec/lib/gitlab/ci/parsers/security/common_spec.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

482 lines
19 KiB
Ruby
Raw Normal View History

2021-10-27 15:23:28 +05:30
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Security::Common do
describe '#parse!' do
2022-06-21 17:19:12 +05:30
let_it_be(:scanner_data) do
{
scan: {
scanner: {
id: "gemnasium",
name: "Gemnasium",
version: "2.1.0"
}
}
}
end
2021-10-27 15:23:28 +05:30
where(vulnerability_finding_signatures_enabled: [true, false])
with_them do
let_it_be(:pipeline) { create(:ci_pipeline) }
let(:artifact) { build(:ci_job_artifact, :common_security_report) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) }
# The path 'yarn.lock' was initially used by DependencyScanning, it is okay for SAST locations to use it, but this could be made better
let(:location) { ::Gitlab::Ci::Reports::Security::Locations::Sast.new(file_path: 'yarn.lock', start_line: 1, end_line: 1) }
let(:tracking_data) { nil }
2021-11-11 11:23:49 +05:30
let(:vulnerability_flags_data) do
[
::Gitlab::Ci::Reports::Security::Flag.new(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'),
::Gitlab::Ci::Reports::Security::Flag.new(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
]
end
2021-10-27 15:23:28 +05:30
before do
allow_next_instance_of(described_class) do |parser|
allow(parser).to receive(:create_location).and_return(location)
allow(parser).to receive(:tracking_data).and_return(tracking_data)
2021-11-11 11:23:49 +05:30
allow(parser).to receive(:create_flags).and_return(vulnerability_flags_data)
2021-10-27 15:23:28 +05:30
end
end
describe 'schema validation' do
let(:validator_class) { Gitlab::Ci::Parsers::Security::Validators::SchemaValidator }
2022-06-21 17:19:12 +05:30
let(:data) { {}.merge(scanner_data) }
let(:json_data) { data.to_json }
let(:parser) { described_class.new(json_data, report, vulnerability_finding_signatures_enabled, validate: validate) }
2021-10-27 15:23:28 +05:30
subject(:parse_report) { parser.parse! }
before do
allow(validator_class).to receive(:new).and_call_original
end
2022-06-21 17:19:12 +05:30
context 'when the validate flag is set to `false`' do
let(:validate) { false }
let(:valid?) { false }
let(:errors) { ['foo'] }
let(:warnings) { ['bar'] }
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
before do
allow_next_instance_of(validator_class) do |instance|
allow(instance).to receive(:valid?).and_return(valid?)
allow(instance).to receive(:errors).and_return(errors)
allow(instance).to receive(:warnings).and_return(warnings)
2022-05-07 20:08:51 +05:30
end
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
allow(parser).to receive_messages(create_scanner: true, create_scan: true)
2022-05-07 20:08:51 +05:30
end
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
it 'instantiates the validator with correct params' do
parse_report
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
expect(validator_class).to have_received(:new).with(
report.type,
data.deep_stringify_keys,
report.version,
project: pipeline.project,
scanner: data.dig(:scan, :scanner).deep_stringify_keys
)
end
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
context 'when the report data is not valid according to the schema' do
it 'adds warnings to the report' do
expect { parse_report }.to change { report.warnings }.from([]).to(
[
{ message: 'foo', type: 'Schema' },
{ message: 'bar', type: 'Schema' }
]
)
2022-05-07 20:08:51 +05:30
end
2022-06-21 17:19:12 +05:30
it 'keeps the execution flow as normal' do
2022-05-07 20:08:51 +05:30
parse_report
2022-06-21 17:19:12 +05:30
expect(parser).to have_received(:create_scanner)
expect(parser).to have_received(:create_scan)
2022-05-07 20:08:51 +05:30
end
2022-06-21 17:19:12 +05:30
end
2022-05-07 20:08:51 +05:30
2022-06-21 17:19:12 +05:30
context 'when the report data is valid according to the schema' do
let(:valid?) { true }
let(:errors) { [] }
let(:warnings) { [] }
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
it 'does not add errors to the report' do
expect { parse_report }.not_to change { report.errors }
2022-05-07 20:08:51 +05:30
end
2022-06-21 17:19:12 +05:30
it 'does not add warnings to the report' do
expect { parse_report }.not_to change { report.warnings }
end
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
it 'keeps the execution flow as normal' do
parse_report
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
expect(parser).to have_received(:create_scanner)
expect(parser).to have_received(:create_scan)
2021-10-27 15:23:28 +05:30
end
2022-04-04 11:22:00 +05:30
end
end
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
context 'when the validate flag is set to `true`' do
let(:validate) { true }
let(:valid?) { false }
let(:errors) { ['foo'] }
let(:warnings) { ['bar'] }
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
before do
allow_next_instance_of(validator_class) do |instance|
allow(instance).to receive(:valid?).and_return(valid?)
allow(instance).to receive(:errors).and_return(errors)
allow(instance).to receive(:warnings).and_return(warnings)
2021-10-27 15:23:28 +05:30
end
2022-06-21 17:19:12 +05:30
allow(parser).to receive_messages(create_scanner: true, create_scan: true)
2021-10-27 15:23:28 +05:30
end
2022-06-21 17:19:12 +05:30
it 'instantiates the validator with correct params' do
parse_report
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
expect(validator_class).to have_received(:new).with(
report.type,
data.deep_stringify_keys,
report.version,
project: pipeline.project,
scanner: data.dig(:scan, :scanner).deep_stringify_keys
)
end
2021-10-27 15:23:28 +05:30
2022-06-21 17:19:12 +05:30
context 'when the report data is not valid according to the schema' do
it 'adds errors to the report' do
expect { parse_report }.to change { report.errors }.from([]).to(
[
{ message: 'foo', type: 'Schema' },
{ message: 'bar', type: 'Schema' }
]
)
2021-10-27 15:23:28 +05:30
end
2022-06-21 17:19:12 +05:30
it 'does not try to create report entities' do
2021-10-27 15:23:28 +05:30
parse_report
2022-06-21 17:19:12 +05:30
expect(parser).not_to have_received(:create_scanner)
expect(parser).not_to have_received(:create_scan)
2022-04-04 11:22:00 +05:30
end
2022-06-21 17:19:12 +05:30
end
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
context 'when the report data is valid according to the schema' do
let(:valid?) { true }
let(:errors) { [] }
let(:warnings) { [] }
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
it 'does not add errors to the report' do
expect { parse_report }.not_to change { report.errors }.from([])
2022-04-04 11:22:00 +05:30
end
2022-06-21 17:19:12 +05:30
it 'does not add warnings to the report' do
expect { parse_report }.not_to change { report.warnings }.from([])
end
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
it 'keeps the execution flow as normal' do
parse_report
2022-04-04 11:22:00 +05:30
2022-06-21 17:19:12 +05:30
expect(parser).to have_received(:create_scanner)
expect(parser).to have_received(:create_scan)
2021-10-27 15:23:28 +05:30
end
end
end
end
2022-05-07 20:08:51 +05:30
context 'report parsing' do
before do
artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) }
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing finding.name' do
let(:artifact) { build(:ci_job_artifact, :common_security_report_with_blank_names) }
context 'when message is provided' do
it 'sets message from the report as a finding name' do
finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
expected_name = Gitlab::Json.parse(finding.raw_metadata)['message']
2021-10-27 15:23:28 +05:30
expect(finding.name).to eq(expected_name)
end
end
2022-05-07 20:08:51 +05:30
context 'when message is not provided' do
context 'and name is provided' do
it 'sets name from the report as a name' do
finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
expected_name = Gitlab::Json.parse(finding.raw_metadata)['name']
expect(finding.name).to eq(expected_name)
2021-10-27 15:23:28 +05:30
end
end
2022-05-07 20:08:51 +05:30
context 'and name is not provided' do
context 'when CVE identifier exists' do
it 'combines identifier with location to create name' do
finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' }
expect(finding.name).to eq("CVE-2017-11429 in yarn.lock")
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
context 'when CWE identifier exists' do
it 'combines identifier with location to create name' do
finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' }
expect(finding.name).to eq("CWE-2017-11429 in yarn.lock")
end
end
context 'when neither CVE nor CWE identifier exist' do
it 'combines identifier with location to create name' do
finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' }
expect(finding.name).to eq("other-2017-11429 in yarn.lock")
end
2021-10-27 15:23:28 +05:30
end
end
end
end
2022-05-07 20:08:51 +05:30
describe 'parsing finding.details' do
context 'when details are provided' do
it 'sets details from the report' do
finding = report.findings.find { |x| x.compare_key == 'CVE-1020' }
expected_details = Gitlab::Json.parse(finding.raw_metadata)['details']
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(finding.details).to eq(expected_details)
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
context 'when details are not provided' do
it 'sets empty hash' do
finding = report.findings.find { |x| x.compare_key == 'CVE-1030' }
expect(finding.details).to eq({})
end
2021-10-27 15:23:28 +05:30
end
end
2022-05-07 20:08:51 +05:30
describe 'top-level scanner' do
it 'is the primary scanner' do
expect(report.primary_scanner.external_id).to eq('gemnasium')
expect(report.primary_scanner.name).to eq('Gemnasium')
expect(report.primary_scanner.vendor).to eq('GitLab')
expect(report.primary_scanner.version).to eq('2.18.0')
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'returns nil report has no scanner' do
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
described_class.parse!({}.to_json, empty_report)
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(empty_report.primary_scanner).to be_nil
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing scanners' do
subject(:scanner) { report.findings.first.scanner }
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
context 'when vendor is not missing in scanner' do
it 'returns scanner with parsed vendor value' do
expect(scanner.vendor).to eq('GitLab')
end
2021-10-27 15:23:28 +05:30
end
end
2022-05-07 20:08:51 +05:30
describe 'parsing scan' do
it 'returns scan object for each finding' do
scans = report.findings.map(&:scan)
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(scans.map(&:status).all?('success')).to be(true)
expect(scans.map(&:start_time).all?('placeholder-value')).to be(true)
expect(scans.map(&:end_time).all?('placeholder-value')).to be(true)
2022-07-16 23:28:13 +05:30
expect(scans.size).to eq(7)
2022-05-07 20:08:51 +05:30
expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan)
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'returns nil when scan is not a hash' do
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
described_class.parse!({}.to_json, empty_report)
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(empty_report.scan).to be(nil)
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing schema version' do
it 'parses the version' do
expect(report.version).to eq('14.0.2')
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'returns nil when there is no version' do
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
described_class.parse!({}.to_json, empty_report)
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(empty_report.version).to be_nil
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing analyzer' do
it 'associates analyzer with report' do
expect(report.analyzer.id).to eq('common-analyzer')
expect(report.analyzer.name).to eq('Common Analyzer')
expect(report.analyzer.version).to eq('2.0.1')
expect(report.analyzer.vendor).to eq('Common')
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'returns nil when analyzer data is not available' do
empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago)
described_class.parse!({}.to_json, empty_report)
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(empty_report.analyzer).to be_nil
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing flags' do
it 'returns flags object for each finding' do
flags = report.findings.first.flags
2021-11-11 11:23:49 +05:30
2022-05-07 20:08:51 +05:30
expect(flags).to contain_exactly(
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'),
2021-11-11 11:23:49 +05:30
have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink')
2022-05-07 20:08:51 +05:30
)
end
2021-11-11 11:23:49 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing links' do
it 'returns links object for each finding', :aggregate_failures do
links = report.findings.flat_map(&:links)
2021-10-27 15:23:28 +05:30
2022-07-16 23:28:13 +05:30
expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030',
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2137", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2138",
"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2139", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-2140"])
expect(links.map(&:name)).to match_array([nil, nil, nil, nil, nil, 'CVE-1030'])
expect(links.size).to eq(6)
2022-05-07 20:08:51 +05:30
expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link)
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'parsing evidence' do
2022-07-16 23:28:13 +05:30
RSpec::Matchers.define_negated_matcher :have_values, :be_empty
2021-10-27 15:23:28 +05:30
2022-07-16 23:28:13 +05:30
it 'returns evidence object for each finding', :aggregate_failures do
all_evidences = report.findings.map(&:evidence)
evidences = all_evidences.compact
data = evidences.map(&:data)
summaries = evidences.map { |e| e.data["summary"] }
expect(all_evidences.size).to eq(7)
expect(evidences.size).to eq(2)
expect(evidences).to all( be_a(::Gitlab::Ci::Reports::Security::Evidence) )
expect(data).to all( have_values )
expect(summaries).to all( match(/The Origin header was changed/) )
2022-05-07 20:08:51 +05:30
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
describe 'setting the uuid' do
let(:finding_uuids) { report.findings.map(&:uuid) }
let(:uuid_1) do
Security::VulnerabilityUUID.generate(
report_type: "sast",
primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint,
location_fingerprint: location.fingerprint,
project_id: pipeline.project_id
)
end
let(:uuid_2) do
Security::VulnerabilityUUID.generate(
report_type: "sast",
primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint,
location_fingerprint: location.fingerprint,
project_id: pipeline.project_id
)
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
let(:expected_uuids) { [uuid_1, uuid_2, nil] }
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'sets the UUIDv5 for findings', :aggregate_failures do
allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report|
allow(report).to receive(:type).and_return('sast')
expect(finding_uuids).to match_array(expected_uuids)
end
2021-10-27 15:23:28 +05:30
end
end
2022-05-07 20:08:51 +05:30
describe 'parsing tracking' do
let(:tracking_data) do
{
2021-10-27 15:23:28 +05:30
'type' => 'source',
'items' => [
2022-05-07 20:08:51 +05:30
'signatures' => [
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
{ 'algorithm' => 'location', 'value' => 'location_value' },
{ 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' }
2021-10-27 15:23:28 +05:30
]
2022-05-07 20:08:51 +05:30
]
}
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
context 'with valid tracking information' do
it 'creates signatures for each algorithm' do
finding = report.findings.first
expect(finding.signatures.size).to eq(3)
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset'])
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
context 'with invalid tracking information' do
let(:tracking_data) do
{
2021-10-27 15:23:28 +05:30
'type' => 'source',
'items' => [
2022-05-07 20:08:51 +05:30
'signatures' => [
{ 'algorithm' => 'hash', 'value' => 'hash_value' },
{ 'algorithm' => 'location', 'value' => 'location_value' },
{ 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' }
2021-10-27 15:23:28 +05:30
]
2022-05-07 20:08:51 +05:30
]
}
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'ignores invalid algorithm types' do
finding = report.findings.first
expect(finding.signatures.size).to eq(2)
expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location'])
end
2021-10-27 15:23:28 +05:30
end
2022-05-07 20:08:51 +05:30
context 'with valid tracking information' do
it 'creates signatures for each signature algorithm' do
finding = report.findings.first
expect(finding.signatures.size).to eq(3)
expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset])
signatures = finding.signatures.index_by(&:algorithm_type)
expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] }
expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value'])
expect(signatures['location'].signature_value).to eq(expected_values['location']['value'])
expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value'])
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
it 'sets the uuid according to the higest priority signature' do
finding = report.findings.first
highest_signature = finding.signatures.max_by(&:priority)
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
identifiers = if vulnerability_finding_signatures_enabled
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}"
else
"#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}"
end
2021-10-27 15:23:28 +05:30
2022-05-07 20:08:51 +05:30
expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers))
end
2021-10-27 15:23:28 +05:30
end
end
end
end
end
end