diff options
Diffstat (limited to 'spec/lib/gitlab/ci/parsers/security/common_spec.rb')
-rw-r--r-- | spec/lib/gitlab/ci/parsers/security/common_spec.rb | 494 |
1 files changed, 270 insertions, 224 deletions
diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index 7eec78ff186..1e96c717a4f 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -26,8 +26,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do allow(parser).to receive(:tracking_data).and_return(tracking_data) allow(parser).to receive(:create_flags).and_return(vulnerability_flags_data) end - - artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) } end describe 'schema validation' do @@ -40,40 +38,50 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do allow(validator_class).to receive(:new).and_call_original end - context 'when enforce_security_report_validation is enabled' do + context 'when show_report_validation_warnings is enabled' do before do - stub_feature_flags(enforce_security_report_validation: true) + stub_feature_flags(show_report_validation_warnings: true) end - context 'when the validate flag is set as `true`' do - let(:validate) { true } + context 'when the validate flag is set to `false`' do + let(:validate) { false } + let(:valid?) { false } + let(:errors) { ['foo'] } - it 'instantiates the validator with correct params' do - parse_report + 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) + end - expect(validator_class).to have_received(:new).with(report.type, {}) + allow(parser).to receive_messages(create_scanner: true, create_scan: true) end - context 'when the report data is valid according to the schema' do - let(:valid?) { true } + it 'instantiates the validator with correct params' do + parse_report - 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([]) - end + expect(validator_class).to have_received(:new).with(report.type, {}, report.version) + end - allow(parser).to receive_messages(create_scanner: true, create_scan: true) + 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' }]) end - it 'does not add errors to the report' do - expect { parse_report }.not_to change { report.errors }.from([]) + it 'keeps the execution flow as normal' do + parse_report + + expect(parser).to have_received(:create_scanner) + expect(parser).to have_received(:create_scan) end + end - it 'adds the schema validation status to the report' do - parse_report + context 'when the report data is valid according to the schema' do + let(:valid?) { true } + let(:errors) { [] } - expect(report.schema_validation_status).to eq(:valid_schema) + it 'does not add warnings to the report' do + expect { parse_report }.not_to change { report.errors } end it 'keeps the execution flow as normal' do @@ -83,42 +91,62 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do expect(parser).to have_received(:create_scan) end end + end - context 'when the report data is not valid according to the schema' do - let(:valid?) { false } - - 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(['foo']) - end + context 'when the validate flag is set to `true`' do + let(:validate) { true } + let(:valid?) { false } + let(:errors) { ['foo'] } - allow(parser).to receive_messages(create_scanner: true, create_scan: true) + 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) end + allow(parser).to receive_messages(create_scanner: true, create_scan: true) + end + + it 'instantiates the validator with correct params' do + parse_report + + expect(validator_class).to have_received(:new).with(report.type, {}, report.version) + end + + 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' }]) end - it 'adds the schema validation status to the report' do + it 'does not try to create report entities' do parse_report - expect(report.schema_validation_status).to eq(:invalid_schema) + expect(parser).not_to have_received(:create_scanner) + expect(parser).not_to have_received(:create_scan) + end + end + + context 'when the report data is valid according to the schema' do + let(:valid?) { true } + let(:errors) { [] } + + it 'does not add errors to the report' do + expect { parse_report }.not_to change { report.errors }.from([]) end - it 'does not try to create report entities' do + it 'keeps the execution flow as normal' do parse_report - expect(parser).not_to have_received(:create_scanner) - expect(parser).not_to have_received(:create_scan) + expect(parser).to have_received(:create_scanner) + expect(parser).to have_received(:create_scan) end end end end - context 'when enforce_security_report_validation is disabled' do + context 'when show_report_validation_warnings is disabled' do before do - stub_feature_flags(enforce_security_report_validation: false) + stub_feature_flags(show_report_validation_warnings: false) end context 'when the validate flag is set as `false`' do @@ -147,7 +175,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do it 'instantiates the validator with correct params' do parse_report - expect(validator_class).to have_received(:new).with(report.type, {}) + expect(validator_class).to have_received(:new).with(report.type, {}, report.version) end context 'when the report data is not valid according to the schema' do @@ -181,265 +209,283 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do end end - 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'] - - expect(finding.name).to eq(expected_name) - end + context 'report parsing' do + before do + artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) } end - 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'] + 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'] expect(finding.name).to eq(expected_name) end end - 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") + 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) end end - 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") + 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 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") + 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 end end end end - end - 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'] + 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'] - expect(finding.details).to eq(expected_details) + expect(finding.details).to eq(expected_details) + end end - end - 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({}) + 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 end end - end - 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 + 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 - 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) + 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) - expect(empty_report.primary_scanner).to be_nil + expect(empty_report.primary_scanner).to be_nil + end end - end - describe 'parsing scanners' do - subject(:scanner) { report.findings.first.scanner } + describe 'parsing scanners' do + subject(:scanner) { report.findings.first.scanner } - context 'when vendor is not missing in scanner' do - it 'returns scanner with parsed vendor value' do - expect(scanner.vendor).to eq('GitLab') + context 'when vendor is not missing in scanner' do + it 'returns scanner with parsed vendor value' do + expect(scanner.vendor).to eq('GitLab') + end end end - end - describe 'parsing scan' do - it 'returns scan object for each finding' do - scans = report.findings.map(&:scan) + describe 'parsing scan' do + it 'returns scan object for each finding' do + scans = report.findings.map(&:scan) - 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) - expect(scans.size).to eq(3) - expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan) - end + 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) + expect(scans.size).to eq(3) + expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan) + end - 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) + 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) - expect(empty_report.scan).to be(nil) + expect(empty_report.scan).to be(nil) + end end - end - describe 'parsing schema version' do - it 'parses the version' do - expect(report.version).to eq('14.0.2') - end + describe 'parsing schema version' do + it 'parses the version' do + expect(report.version).to eq('14.0.2') + end - 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) + 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) - expect(empty_report.version).to be_nil + expect(empty_report.version).to be_nil + end end - end - 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 + 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 - 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) + 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) - expect(empty_report.analyzer).to be_nil + expect(empty_report.analyzer).to be_nil + end end - end - describe 'parsing flags' do - it 'returns flags object for each finding' do - flags = report.findings.first.flags + describe 'parsing flags' do + it 'returns flags object for each finding' do + flags = report.findings.first.flags - expect(flags).to contain_exactly( - have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'), + expect(flags).to contain_exactly( + have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer X', description: 'static string to sink'), have_attributes(type: 'flagged-as-likely-false-positive', origin: 'post analyzer Y', description: 'integer to sink') - ) + ) + end end - end - describe 'parsing links' do - it 'returns links object for each finding', :aggregate_failures do - links = report.findings.flat_map(&:links) + describe 'parsing links' do + it 'returns links object for each finding', :aggregate_failures do + links = report.findings.flat_map(&:links) - 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']) - expect(links.map(&:name)).to match_array([nil, 'CVE-1030']) - expect(links.size).to eq(2) - expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link) + 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']) + expect(links.map(&:name)).to match_array([nil, 'CVE-1030']) + expect(links.size).to eq(2) + expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link) + end end - end - 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 + describe 'parsing evidence' do + it 'returns evidence object for each finding', :aggregate_failures do + evidences = report.findings.map(&:evidence) - 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 - ) + expect(evidences.first.data).not_to be_empty + expect(evidences.first.data["summary"]).to match(/The Origin header was changed/) + expect(evidences.size).to eq(3) + expect(evidences.compact.size).to eq(2) + expect(evidences.first).to be_a(::Gitlab::Ci::Reports::Security::Evidence) + end end - let(:expected_uuids) { [uuid_1, uuid_2, nil] } + 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 - 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') + let(:expected_uuids) { [uuid_1, uuid_2, nil] } - expect(finding_uuids).to match_array(expected_uuids) + 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 end end - end - describe 'parsing tracking' do - let(:tracking_data) do - { + describe 'parsing tracking' do + let(:tracking_data) do + { 'type' => 'source', 'items' => [ - 'signatures' => [ - { 'algorithm' => 'hash', 'value' => 'hash_value' }, - { 'algorithm' => 'location', 'value' => 'location_value' }, - { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' } - ] + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' } ] - } - end + ] + } + end - 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']) + 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 end - end - context 'with invalid tracking information' do - let(:tracking_data) do - { + context 'with invalid tracking information' do + let(:tracking_data) do + { 'type' => 'source', 'items' => [ - 'signatures' => [ - { 'algorithm' => 'hash', 'value' => 'hash_value' }, - { 'algorithm' => 'location', 'value' => 'location_value' }, - { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' } - ] + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' } ] - } - end + ] + } + end - 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']) + 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 end - end - 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 + 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 - it 'sets the uuid according to the higest priority signature' do - finding = report.findings.first - highest_signature = finding.signatures.max_by(&:priority) + it 'sets the uuid according to the higest priority signature' do + finding = report.findings.first + highest_signature = finding.signatures.max_by(&:priority) - 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 + 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 - expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers)) + expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers)) + end end end end |