Commit 56cf6c17 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '209994-vulnerability-issue-history' into 'master'

Add issue creation history to vulnerability page

See merge request gitlab-org/gitlab!27832
parents 937e0948 ff2425a1
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import HeaderApp from 'ee/vulnerabilities/components/app.vue';
import FooterApp from 'ee/vulnerabilities/components/footer.vue';
function createSolutionCardApp() {
const el = document.getElementById('js-vulnerability-solution');
function createFooterApp() {
const el = document.getElementById('js-vulnerability-footer');
if (!el) {
return false;
}
const { solution, vulnerabilityFeedbackHelpPath, vulnerabilityState } = el.dataset;
const hasMr = parseBoolean(el.dataset.hasMr);
const remediation = JSON.parse(el.dataset.remediation);
const { vulnerabilityFeedbackHelpPath, hasMr } = el.dataset;
const vulnerability = JSON.parse(el.dataset.vulnerabilityJson);
const finding = JSON.parse(el.dataset.findingJson);
const { issue_feedback: feedback, remediation, solution } = finding;
const hasDownload = Boolean(
vulnerabilityState !== 'resolved' && remediation?.diff?.length && !hasMr,
vulnerability.state !== 'resolved' && remediation?.diff?.length && !hasMr,
);
const props = {
solutionInfo: {
solution,
remediation,
hasDownload,
......@@ -25,12 +26,18 @@ function createSolutionCardApp() {
hasRemediation: Boolean(remediation),
vulnerabilityFeedbackHelpPath,
isStandaloneVulnerability: true,
},
feedback,
project: {
url: finding.project.full_path,
value: finding.project.full_name,
},
};
return new Vue({
el,
render: h =>
h(SolutionCard, {
h(FooterApp, {
props,
}),
});
......@@ -60,5 +67,5 @@ function createHeaderApp() {
window.addEventListener('DOMContentLoaded', () => {
createHeaderApp();
createSolutionCardApp();
createFooterApp();
});
<script>
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
export default {
name: 'VulnerabilityFooter',
components: { IssueNote, SolutionCard },
props: {
feedback: {
type: Object,
required: false,
default: null,
},
project: {
type: Object,
required: true,
},
solutionInfo: {
type: Object,
required: true,
},
},
computed: {
hasSolution() {
return this.solutionInfo.solution || this.solutionInfo.hasRemediation;
},
},
};
</script>
<template>
<ul class="notes">
<li v-if="hasSolution" class="note">
<solution-card v-bind="solutionInfo" />
</li>
<li>
<hr />
</li>
<li v-if="feedback" class="note card my-4 border-bottom">
<div class="card-body">
<issue-note :feedback="feedback" :project="project" />
</div>
</li>
</ul>
</template>
......@@ -8,7 +8,10 @@ module VulnerabilitiesHelper
vulnerability_json: vulnerability.to_json,
project_fingerprint: vulnerability.finding.project_fingerprint,
create_issue_url: create_vulnerability_feedback_issue_path(vulnerability.finding.project),
pipeline_json: vulnerability_pipeline_data(pipeline).to_json
pipeline_json: vulnerability_pipeline_data(pipeline).to_json,
has_mr: !!vulnerability.finding.merge_request_feedback.try(:merge_request_iid),
vulnerability_feedback_help_path: help_page_path('user/application_security/index', anchor: 'interacting-with-the-vulnerabilities'),
finding_json: vulnerability_finding_data(vulnerability.finding).to_json
}
end
......@@ -21,4 +24,21 @@ module VulnerabilitiesHelper
url: pipeline_path(pipeline)
}
end
def vulnerability_finding_data(finding)
occurrence = Vulnerabilities::OccurrenceSerializer.new(current_user: current_user).represent(finding)
remediation = occurrence[:remediations]&.first
occurrence.slice(
:description,
:identifiers,
:links,
:location,
:name,
:issue_feedback,
:project
).merge(
solution: remediation ? remediation['summary'] : occurrence[:solution]
)
end
end
......@@ -7,7 +7,7 @@
#js-vulnerability-management-app{ data: vulnerability_data(@vulnerability, @pipeline) }
.issue-details.issuable-details
.detail-page-description.content-block
.detail-page-description
%h2.title= @vulnerability.title
.description
.md
......@@ -37,9 +37,5 @@
- @vulnerability.finding.identifiers.each do |identifier|
%li
%a{ :href=>identifier.url, target: "_blank", rel: 'noopener noreferrer' }= identifier.name
- if @vulnerability.finding.solution || @vulnerability.finding.remediations
#js-vulnerability-solution{ data: { vulnerability_state: @vulnerability.state,
solution: @vulnerability.finding.solution,
remediation: @vulnerability.finding.remediations&.first.to_json,
has_mr: !!@vulnerability.finding.merge_request_feedback.try(:merge_request_iid),
vulnerability_feedback_help_path: help_page_path("user/application_security/index", anchor: "interacting-with-the-vulnerabilities") } }
#js-vulnerability-footer{ data: vulnerability_data(@vulnerability, @pipeline) }
......@@ -80,7 +80,7 @@ describe Projects::Security::VulnerabilitiesController do
it 'renders the solution card' do
show_vulnerability
expect(response.body).to have_css("#js-vulnerability-solution")
expect(response.body).to have_css("#js-vulnerability-footer")
end
end
......
......@@ -43,12 +43,20 @@ FactoryBot.define do
trait :with_findings do
after(:build) do |vulnerability|
vulnerability.findings = build_list(
occurrences_with_solution = build_list(
:vulnerabilities_occurrence,
2,
vulnerability: vulnerability,
report_type: vulnerability.report_type,
project: vulnerability.project)
occurrences_with_remediation = build_list(
:vulnerabilities_occurrence,
2,
:with_remediation,
vulnerability: vulnerability,
report_type: vulnerability.report_type,
project: vulnerability.project)
vulnerability.findings = occurrences_with_solution + occurrences_with_remediation
end
end
......
......@@ -69,6 +69,19 @@ FactoryBot.define do
end
end
trait :with_remediation do
after(:build) do |finding|
raw_metadata = JSON.parse(finding.raw_metadata)
raw_metadata.delete(:solution)
raw_metadata[:remediations] = [
{
summary: "Use GCM mode which includes HMAC in the resulting encrypted data, providing integrity of the result."
}
]
finding.raw_metadata = raw_metadata.to_json
end
end
::Vulnerabilities::Occurrence::REPORT_TYPES.keys.each do |security_report_type|
trait security_report_type do
report_type { security_report_type }
......
import { shallowMount } from '@vue/test-utils';
import VulnerabilityFooter from 'ee/vulnerabilities/components/footer.vue';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card.vue';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note.vue';
import { TEST_HOST } from 'helpers/test_constants';
describe('Vulnerability Footer', () => {
let wrapper;
const minimumProps = {
solutionInfo: {
hasDownload: false,
hasMr: false,
hasRemediation: false,
isStandaloneVulnerability: true,
remediation: null,
solution: undefined,
vulnerabilityFeedbackHelpPath:
'/help/user/application_security/index#interacting-with-the-vulnerabilities',
},
project: {
url: '/root/security-reports',
value: 'Administrator / Security Reports',
},
};
const solutionInfoProp = {
hasDownload: true,
hasMr: false,
hasRemediation: true,
isStandaloneVulnerability: true,
remediation: {},
solution: 'Upgrade to fixed version.\n',
vulnerabilityFeedbackHelpPath:
'/help/user/application_security/index#interacting-with-the-vulnerabilities',
};
const feedbackProps = {
author: {},
branch: null,
category: 'container_scanning',
created_at: '2020-03-18T00:10:49.527Z',
feedback_type: 'issue',
id: 36,
issue_iid: 22,
issue_url: `${TEST_HOST}/root/security-reports/-/issues/22`,
project_fingerprint: 'f7319ea35fc016e754e9549dd89b338aea4c72cc',
project_id: 19,
};
const createWrapper = (props = minimumProps) => {
wrapper = shallowMount(VulnerabilityFooter, {
propsData: props,
});
};
afterEach(() => {
wrapper.destroy();
});
describe('solution card', () => {
it('does show solution card when there is one', () => {
createWrapper({ ...minimumProps, solutionInfo: solutionInfoProp });
expect(wrapper.contains(SolutionCard)).toBe(true);
expect(wrapper.find(SolutionCard).props()).toMatchObject(solutionInfoProp);
});
it('does not show solution card when there is not one', () => {
createWrapper();
expect(wrapper.contains(SolutionCard)).toBe(false);
});
});
describe('issue history', () => {
it('does show issue history when there is one', () => {
createWrapper({ ...minimumProps, feedback: feedbackProps });
expect(wrapper.contains(IssueNote)).toBe(true);
expect(wrapper.find(IssueNote).props()).toMatchObject({
feedback: feedbackProps,
project: minimumProps.project,
});
});
it('does not show issue history when there is not one', () => {
createWrapper();
expect(wrapper.contains(IssueNote)).toBe(false);
});
});
});
......@@ -8,7 +8,10 @@ describe VulnerabilitiesHelper do
expect(subject).to include(
vulnerability_json: vulnerability.to_json,
project_fingerprint: vulnerability.finding.project_fingerprint,
create_issue_url: be_present
create_issue_url: anything,
has_mr: anything,
vulnerability_feedback_help_path: anything,
finding_json: anything
)
end
end
......@@ -50,4 +53,39 @@ describe VulnerabilitiesHelper do
end
end
end
describe '#vulnerability_finding_data' do
let(:vulnerability) { create(:vulnerability, :with_findings) }
let(:finding) { vulnerability.finding }
subject { helper.vulnerability_finding_data(finding) }
it "returns finding information" do
expect(subject).to include(
description: finding.description,
identifiers: finding.identifiers,
links: finding.links,
location: finding.location,
name: finding.name,
issue_feedback: anything,
project: anything
)
end
context "when finding has a remediations key" do
let(:finding) { vulnerability.findings.select { |finding| finding.raw_metadata.include?("remediations") }.first }
it "uses the first remediation summary" do
expect(subject[:solution]).to start_with "Use GCM mode"
end
end
context "when finding has a solution key" do
let(:finding) { vulnerability.findings.select { |finding| finding.raw_metadata.include?("solution") }.first }
it "uses the solution key" do
expect(subject[:solution]).to start_with "GCM mode"
end
end
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