# frozen_string_literal: true require 'spec_helper' RSpec.describe VulnerabilitiesHelper do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository, :public) } let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) } let_it_be(:finding) { create(:vulnerabilities_finding, :with_pipeline, project: project, severity: :high) } let(:vulnerability) { create(:vulnerability, title: "My vulnerability", project: project, findings: [finding]) } before do allow(helper).to receive(:current_user).and_return(user) end RSpec.shared_examples 'vulnerability properties' do let(:vulnerability_serializer_hash) do vulnerability.slice( :id, :title, :state, :severity, :confidence, :report_type, :resolved_on_default_branch, :project_default_branch, :resolved_by_id, :dismissed_by_id, :confirmed_by_id) end let(:finding_serializer_hash) do finding.slice(:description, :identifiers, :links, :location, :name, :issue_feedback, :project, :remediations, :solution, :uuid, :details) end let(:desired_serializer_fields) { %i[metadata identifiers name issue_feedback merge_request_feedback project project_fingerprint scanner uuid details dismissal_feedback false_positive] } before do vulnerability_serializer_stub = instance_double("VulnerabilitySerializer") expect(VulnerabilitySerializer).to receive(:new).and_return(vulnerability_serializer_stub) expect(vulnerability_serializer_stub).to receive(:represent).with(vulnerability).and_return(vulnerability_serializer_hash) finding_serializer_stub = instance_double("Vulnerabilities::FindingSerializer") expect(Vulnerabilities::FindingSerializer).to receive(:new).and_return(finding_serializer_stub) expect(finding_serializer_stub).to receive(:represent).with(finding, only: desired_serializer_fields).and_return(finding_serializer_hash) end around do |example| freeze_time { example.run } end it 'has expected vulnerability properties' do expect(subject).to include( timestamp: Time.now.to_i, new_issue_url: "/#{project.full_path}/-/issues/new?vulnerability_id=#{vulnerability.id}", create_jira_issue_url: nil, related_jira_issues_path: "/#{project.full_path}/-/integrations/jira/issues?vulnerability_ids%5B%5D=#{vulnerability.id}", jira_integration_settings_path: "/#{project.full_path}/-/services/jira/edit", has_mr: anything, create_mr_url: "/#{project.full_path}/-/vulnerability_feedback", discussions_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/discussions", notes_url: "/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}/notes", related_issues_help_path: kind_of(String), pipeline: anything, can_modify_related_issues: false ) end context 'when the issues are disabled for the project' do before do allow(project).to receive(:issues_enabled?).and_return(false) end it 'has `new_issue_url` set as nil' do expect(subject).to include(new_issue_url: nil) end end end describe '#vulnerability_details' do before do allow(helper).to receive(:can?).and_return(true) end subject { helper.vulnerability_details(vulnerability, pipeline) } describe '[:can_modify_related_issues]' do context 'with security dashboard feature enabled' do before do stub_licensed_features(security_dashboard: true) end context 'when user can manage related issues' do before do project.add_developer(user) end it { is_expected.to include(can_modify_related_issues: true) } end context 'when user cannot manage related issues' do it { is_expected.to include(can_modify_related_issues: false) } end end context 'with security dashboard feature disabled' do before do stub_licensed_features(security_dashboard: false) project.add_developer(user) end it { is_expected.to include(can_modify_related_issues: false) } end end context 'when pipeline exists' do subject { helper.vulnerability_details(vulnerability, pipeline) } include_examples 'vulnerability properties' it 'returns expected pipeline data' do expect(subject[:pipeline]).to include( id: pipeline.id, created_at: pipeline.created_at.iso8601, url: be_present ) end end context 'when pipeline is nil' do subject { helper.vulnerability_details(vulnerability, nil) } include_examples 'vulnerability properties' it 'returns no pipeline data' do expect(subject[:pipeline]).to be_nil end end describe '[:has_mr]' do subject { helper.vulnerability_details(vulnerability, pipeline)[:has_mr] } context 'with existing merge request feedback' do before do create(:vulnerability_feedback, :merge_request, project: project, pipeline: pipeline, project_fingerprint: finding.project_fingerprint) end it { is_expected.to be_truthy } end context 'without feedback' do it { is_expected.to be_falsey } end end context 'dismissal descriptions' do let(:expected_descriptions) do { acceptable_risk: "The vulnerability is known, and has not been remediated or mitigated, but is considered to be an acceptable business risk.", false_positive: "An error in reporting in which a test result incorrectly indicates the presence of a vulnerability in a system when the vulnerability is not present.", mitigating_control: "A management, operational, or technical control (that is, safeguard or countermeasure) employed by an organization that provides equivalent or comparable protection for an information system.", used_in_tests: "The finding is not a vulnerability because it is part of a test or is test data.", not_applicable: "The vulnerability is known, and has not been remediated or mitigated, but is considered to be in a part of the application that will not be updated." } end let(:translated_descriptions) do # Use dynamic translations via N_(...) expected_descriptions.values.map { |description| _(description) } end it 'includes translated dismissal descriptions' do Gitlab::I18n.with_locale(:en) do # Force loading of the class and configured translations Vulnerabilities::DismissalReasonEnum.translated_descriptions end Gitlab::I18n.with_locale(:zh_CN) do expect(subject[:dismissal_descriptions].keys).to eq(expected_descriptions.keys) expect(subject[:dismissal_descriptions].values).to eq(translated_descriptions) end end end end describe '#create_jira_issue_url_for' do subject { helper.create_jira_issue_url_for(vulnerability) } let(:jira_integration) { double('Integrations::Jira', new_issue_url_with_predefined_fields: 'https://jira.example.com/new') } before do allow(helper).to receive(:can?).and_return(true) allow(vulnerability.project).to receive(:jira_integration).and_return(jira_integration) end context 'with jira vulnerabilities integration enabled' do before do allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(true) allow(project).to receive(:configured_to_create_issues_from_vulnerabilities?).and_return(true) end context 'when the given object is a vulnerability' do let(:expected_jira_issue_description) do <<-JIRA.strip_heredoc Issue created from vulnerability [#{vulnerability.id}|http://localhost/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}] h3. Description: Description of My vulnerability * Severity: high * Confidence: medium * Location: [maven/src/main/java/com/gitlab/security_products/tests/App.java:29|http://localhost/#{project.full_path}/-/blob/b83d6e391c22777fca1ed3012fce84f633d7fed0/maven/src/main/java/com/gitlab/security_products/tests/App.java#L29] ### Solution: See vulnerability [#{vulnerability.id}|http://localhost/#{project.full_path}/-/security/vulnerabilities/#{vulnerability.id}] for any Solution details. h3. Links: * [Cipher does not check for integrity first?|https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first] h3. Scanner: * Name: Find Security Bugs JIRA end it 'delegates rendering URL to Integrations::Jira' do expect(jira_integration).to receive(:new_issue_url_with_predefined_fields).with("Investigate vulnerability: #{vulnerability.title}", expected_jira_issue_description) subject end context 'when scan property is empty' do before do vulnerability.finding.scan = nil end it 'renders description using dedicated template without raising error' do expect(jira_integration).to receive(:new_issue_url_with_predefined_fields).with("Investigate vulnerability: #{vulnerability.title}", expected_jira_issue_description) subject end end end context 'when the given object is an unpersisted finding' do let(:vulnerability) { build(:vulnerabilities_finding, :with_remediation, project: project) } let(:expected_jira_issue_description) do <<~TEXT h3. Description: The cipher does not provide data integrity update 1 * Severity: high * Confidence: medium h3. Links: * [Cipher does not check for integrity first?|https://crypto.stackexchange.com/questions/31428/pbewithmd5anddes-cipher-does-not-check-for-integrity-first] h3. Scanner: * Name: Find Security Bugs TEXT end it 'delegates rendering URL to Integrations::Jira' do expect(jira_integration).to receive(:new_issue_url_with_predefined_fields).with("Investigate vulnerability: #{vulnerability.name}", expected_jira_issue_description) subject end end end context 'with jira vulnerabilities integration disabled' do before do allow(project).to receive(:jira_vulnerabilities_integration_enabled?).and_return(false) allow(project).to receive(:configured_to_create_issues_from_vulnerabilities?).and_return(false) end it { is_expected.to be_nil } end end describe '#vulnerability_finding_data' do subject { helper.vulnerability_finding_data(vulnerability) } it 'returns finding information' do expect(subject.to_h).to match( description: finding.description, description_html: anything, identifiers: kind_of(Array), issue_feedback: anything, links: finding.links, location: finding.location, name: finding.name, merge_request_feedback: anything, project: kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder), project_fingerprint: finding.project_fingerprint, remediations: finding.remediations, solution: kind_of(String), evidence: kind_of(String), scanner: kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder), request: kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder), response: kind_of(Grape::Entity::Exposure::NestingExposure::OutputBuilder), evidence_source: anything, assets: kind_of(Array), supporting_messages: kind_of(Array), uuid: kind_of(String), details: kind_of(Hash), dismissal_feedback: anything ) expect(subject[:location]['blob_path']).to match(kind_of(String)) end context 'when there is no file' do before do vulnerability.finding.location['file'] = nil vulnerability.finding.location.delete('blob_path') end it 'does not have a blob_path if there is no file' do expect(subject[:location]).not_to have_key('blob_path') end end context 'with existing dismissal feedback' do let_it_be(:feedback) { create(:vulnerability_feedback, :comment, :dismissal, project: project, pipeline: pipeline, project_fingerprint: finding.project_fingerprint) } it 'returns dismissal feedback information', :aggregate_failures do dismissal_feedback = subject[:dismissal_feedback] expect(dismissal_feedback[:dismissal_reason]).to eq(feedback.dismissal_reason) expect(dismissal_feedback[:comment_details][:comment]).to eq(feedback.comment) end end end describe '#vulnerability_scan_data?' do subject { helper.vulnerability_scan_data?(vulnerability) } context 'scanner present' do before do allow(vulnerability).to receive(:scanner).and_return(true) end it { is_expected.to be_truthy } end context 'scan present' do before do allow(vulnerability).to receive(:scanner).and_return(false) allow(vulnerability).to receive(:scan).and_return(true) end it { is_expected.to be_truthy } end context 'neither scan nor scanner being present' do before do allow(vulnerability).to receive(:scanner).and_return(false) allow(vulnerability).to receive(:scan).and_return(false) end it { is_expected.to be_falsey } end end end