Commit 8dec1e15 authored by Savas Vedova's avatar Savas Vedova

Merge branch 'vulnerability-details-use-be-service' into 'master'

Update to vulnerability details to use backend service

See merge request gitlab-org/gitlab!82436
parents 69065bae b6008e3f
query getSecurityTrainingVulnerability($id: ID!) {
vulnerability(id: $id) {
id
identifiers {
externalType
}
securityTrainingUrls {
name
url
status
}
}
}
...@@ -3,17 +3,10 @@ import { GlFriendlyWrap, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui ...@@ -3,17 +3,10 @@ import { GlFriendlyWrap, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants'; import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue'; import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue'; import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
import { import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
SUPPORTING_MESSAGE_TYPES, import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
VULNERABILITY_TRAINING_HEADING,
} from 'ee/vulnerabilities/constants';
import {
convertObjectPropsToCamelCase,
convertArrayOfObjectsToCamelCase,
} from '~/lib/utils/common_utils';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import CodeBlock from '~/vue_shared/components/code_block.vue'; import CodeBlock from '~/vue_shared/components/code_block.vue';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import getFileLocation from '../store/utils/get_file_location'; import getFileLocation from '../store/utils/get_file_location';
import { bodyWithFallBack } from './helpers'; import { bodyWithFallBack } from './helpers';
import SeverityBadge from './severity_badge.vue'; import SeverityBadge from './severity_badge.vue';
...@@ -30,17 +23,11 @@ export default { ...@@ -30,17 +23,11 @@ export default {
GlLink, GlLink,
GlBadge, GlBadge,
FalsePositiveAlert, FalsePositiveAlert,
VulnerabilityTraining,
}, },
directives: { directives: {
SafeHtml: GlSafeHtmlDirective, SafeHtml: GlSafeHtmlDirective,
}, },
props: { vulnerability: { type: Object, required: true } }, props: { vulnerability: { type: Object, required: true } },
data() {
return {
showTraining: false,
};
},
computed: { computed: {
url() { url() {
return this.vulnerability.request?.url || getFileLocation(this.vulnLocation); return this.vulnerability.request?.url || getFileLocation(this.vulnLocation);
...@@ -154,9 +141,6 @@ export default { ...@@ -154,9 +141,6 @@ export default {
hasRecordedResponse() { hasRecordedResponse() {
return Boolean(this.constructedRecordedResponse); return Boolean(this.constructedRecordedResponse);
}, },
camelCaseFormattedIdentifiers() {
return convertArrayOfObjectsToCamelCase(this.identifiers);
},
}, },
methods: { methods: {
getHeadersAsCodeBlockLines(headers) { getHeadersAsCodeBlockLines(headers) {
...@@ -191,12 +175,6 @@ export default { ...@@ -191,12 +175,6 @@ export default {
? [`${method} ${url}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('') ? [`${method} ${url}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('')
: ''; : '';
}, },
handleShowTraining(showVulnerabilityTraining) {
this.showTraining = showVulnerabilityTraining;
},
},
i18n: {
VULNERABILITY_TRAINING_HEADING,
}, },
}; };
</script> </script>
...@@ -331,13 +309,5 @@ export default { ...@@ -331,13 +309,5 @@ export default {
class="gl-mt-4" class="gl-mt-4"
:details="vulnerability.details" :details="vulnerability.details"
/> />
<div v-if="identifiers" v-show="showTraining">
<vulnerability-detail :label="$options.i18n.VULNERABILITY_TRAINING_HEADING.title">
<vulnerability-training
:identifiers="camelCaseFormattedIdentifiers"
@show-vulnerability-training="handleShowTraining"
/>
</vulnerability-detail>
</div>
</div> </div>
</template> </template>
...@@ -380,7 +380,7 @@ export default { ...@@ -380,7 +380,7 @@ export default {
</ul> </ul>
</template> </template>
<vulnerability-training :identifiers="vulnerability.identifiers"> <vulnerability-training :id="vulnerability.id">
<template #header> <template #header>
<h3>{{ $options.VULNERABILITY_TRAINING_HEADING.title }}</h3> <h3>{{ $options.VULNERABILITY_TRAINING_HEADING.title }}</h3>
</template> </template>
......
...@@ -3,14 +3,13 @@ import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui'; ...@@ -3,14 +3,13 @@ import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql'; import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import securityTrainingVulnerabilityQuery from '~/security_configuration/graphql/security_training_vulnerability.query.graphql';
import { TYPE_VULNERABILITY } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import axios from '~/lib/utils/axios_utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import { import { TRACK_CLICK_TRAINING_LINK_ACTION } from '~/security_configuration/constants';
TRACK_CLICK_TRAINING_LINK_ACTION, import { SUPPORTED_IDENTIFIER_TYPES, SECURITY_TRAINING_URL_STATUS_COMPLETED } from '../constants';
TRACK_TRAINING_LOADED_ACTION,
} from '~/security_configuration/constants';
import { SUPPORTED_IDENTIFIER_TYPES } from '../constants';
export const i18n = { export const i18n = {
trainingDescription: s__( trainingDescription: s__(
...@@ -21,12 +20,6 @@ export const i18n = { ...@@ -21,12 +20,6 @@ export const i18n = {
loading: __('Loading'), loading: __('Loading'),
}; };
export const mockProvider = {
path: 'https://integration-api.securecodewarrior.com/api/v1/trial',
id: 'gitlab',
name: s__('Vulnerability|Secure Code Warrior'),
};
export default { export default {
i18n, i18n,
components: { components: {
...@@ -37,8 +30,8 @@ export default { ...@@ -37,8 +30,8 @@ export default {
mixins: [glFeatureFlagsMixin(), Tracking.mixin()], mixins: [glFeatureFlagsMixin(), Tracking.mixin()],
inject: ['projectFullPath'], inject: ['projectFullPath'],
props: { props: {
identifiers: { id: {
type: Array, type: Number,
required: true, required: true,
}, },
}, },
...@@ -57,33 +50,58 @@ export default { ...@@ -57,33 +50,58 @@ export default {
}; };
}, },
}, },
vulnerability: {
query: securityTrainingVulnerabilityQuery,
update({ vulnerability }) {
const allUrlsAreReady = vulnerability?.securityTrainingUrls?.every(
({ status }) => status === SECURITY_TRAINING_URL_STATUS_COMPLETED,
);
if (allUrlsAreReady) {
// note: once we add polling, we can call `.stopPolling` here
this.isUrlsLoading = false;
}
return vulnerability;
},
variables() {
return { id: convertToGraphQLId(TYPE_VULNERABILITY, this.id) };
},
error(e) {
Sentry.captureException(e);
},
},
}, },
data() { data() {
return { return {
securityTrainingProviders: [], securityTrainingProviders: [],
vulnerability: {},
training: null, training: null,
isLoading: true, isUrlsLoading: true,
hasError: false,
}; };
}, },
computed: { computed: {
showVulnerabilityTraining() { showVulnerabilityTraining() {
return Boolean( return Boolean(
this.glFeatures.secureVulnerabilityTraining && this.glFeatures.secureVulnerabilityTraining && this.hasSecurityTrainingProviders,
this.enabledSecurityTrainingProviders?.length &&
this.identifiers?.length,
); );
}, },
enabledSecurityTrainingProviders() { showTrainingNotFound() {
return this.securityTrainingProviders?.filter((provider) => provider.isEnabled); return !this.hasSupportedIdentifier || !this.hasSecurityTrainingUrls;
},
hasSecurityTrainingProviders() {
return this.securityTrainingProviders?.some(({ isEnabled }) => isEnabled);
}, },
supportedIdentifier() { hasSupportedIdentifier() {
return this.identifiers?.find( return this.vulnerability?.identifiers?.some(
({ externalType }) => externalType?.toLowerCase() === SUPPORTED_IDENTIFIER_TYPES.cwe, ({ externalType }) => externalType?.toLowerCase() === SUPPORTED_IDENTIFIER_TYPES.cwe,
); );
}, },
showTrainingNotFound() { hasSecurityTrainingUrls() {
return !this.supportedIdentifier || this.hasError; return this.vulnerability?.securityTrainingUrls?.length > 0;
},
securityTrainingUrls() {
return this.vulnerability?.securityTrainingUrls;
}, },
}, },
watch: { watch: {
...@@ -93,49 +111,15 @@ export default { ...@@ -93,49 +111,15 @@ export default {
this.$emit('show-vulnerability-training', showVulnerabilityTraining); this.$emit('show-vulnerability-training', showVulnerabilityTraining);
}, },
}, },
supportedIdentifier: {
immediate: true,
handler(supportedIdentifier) {
if (supportedIdentifier) {
const { externalType, externalId } = supportedIdentifier;
this.fetchTraining(externalType, externalId);
} else {
this.isLoading = false;
}
},
},
}, },
methods: { methods: {
async fetchTraining(mappingList, mappingKey) { clickTrainingLink(name, url) {
const { path, id, name } = mockProvider; this.triggerMetric(TRACK_CLICK_TRAINING_LINK_ACTION, name, url);
const params = {
id,
mappingList,
mappingKey,
};
try {
const {
data: { url },
} = await axios.get(path, { params });
this.triggerMetric(TRACK_TRAINING_LOADED_ACTION);
this.training = { name, url };
} catch {
this.hasError = true;
} finally {
this.isLoading = false;
}
}, },
clickTrainingLink() { triggerMetric(action, name, url) {
this.triggerMetric(TRACK_CLICK_TRAINING_LINK_ACTION);
},
triggerMetric(action) {
const { name } = this.supportedIdentifier;
const { id } = mockProvider;
this.track(action, { this.track(action, {
label: `vendor_${id}`, property: url,
property: name, label: `vendor_${name}`,
}); });
}, },
}, },
...@@ -151,15 +135,19 @@ export default { ...@@ -151,15 +135,19 @@ export default {
<p v-if="showTrainingNotFound" data-testid="unavailable-message"> <p v-if="showTrainingNotFound" data-testid="unavailable-message">
{{ $options.i18n.trainingUnavailable }} {{ $options.i18n.trainingUnavailable }}
</p> </p>
<div v-else-if="isLoading"> <div v-else-if="isUrlsLoading">
<gl-skeleton-loader :width="200" :lines="3" /> <gl-skeleton-loader :width="200" :lines="3" />
</div> </div>
<div v-else> <div v-else>
<div class="gl-font-weight-bold gl-font-base">{{ training.name }}</div> <div v-for="({ name, url }, index) in securityTrainingUrls" :key="index" class="gl-mt-6">
<gl-link :href="training.url" target="_blank" @click="clickTrainingLink"> <div>
{{ $options.i18n.viewTraining }} <span class="gl-font-weight-bold gl-font-base">{{ name }}</span>
<gl-icon class="gl-ml-2" name="external-link" :size="12" /> </div>
</gl-link> <gl-link :href="url" target="_blank" @click="clickTrainingLink(name, url)">
{{ $options.i18n.viewTraining }}
<gl-icon class="gl-ml-2" name="external-link" :size="12" />
</gl-link>
</div>
</div> </div>
</div> </div>
</template> </template>
...@@ -93,3 +93,6 @@ export const SUPPORTED_IDENTIFIER_TYPES = { ...@@ -93,3 +93,6 @@ export const SUPPORTED_IDENTIFIER_TYPES = {
export const VULNERABILITY_TRAINING_HEADING = { export const VULNERABILITY_TRAINING_HEADING = {
title: s__('Vulnerability|Training'), title: s__('Vulnerability|Training'),
}; };
export const SECURITY_TRAINING_URL_STATUS_COMPLETED = 'COMPLETED';
export const SECURITY_TRAINING_URL_STATUS_PENDING = 'PENDING';
...@@ -200,17 +200,5 @@ key2: value2 ...@@ -200,17 +200,5 @@ key2: value2
<!----> <!---->
<!----> <!---->
<div
style="display: none;"
>
<vulnerability-detail-stub
label="Training"
>
<vulnerability-training-stub
identifiers="[object Object],[object Object]"
/>
</vulnerability-detail-stub>
</div>
</div> </div>
`; `;
...@@ -9,7 +9,6 @@ import GenericReportSection from 'ee/vulnerabilities/components/generic_report/r ...@@ -9,7 +9,6 @@ import GenericReportSection from 'ee/vulnerabilities/components/generic_report/r
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import { mockFindings } from '../mock_data'; import { mockFindings } from '../mock_data';
function makeVulnerability(changes = {}) { function makeVulnerability(changes = {}) {
...@@ -138,17 +137,6 @@ describe('VulnerabilityDetails component', () => { ...@@ -138,17 +137,6 @@ describe('VulnerabilityDetails component', () => {
); );
}); });
it('renders vulnerability training', () => {
const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }];
const vulnerability = makeVulnerability({ identifiers });
componentFactory(vulnerability);
expect(wrapper.findComponent(VulnerabilityTraining).props()).toMatchObject({
identifiers,
});
});
describe('does not render XSS links', () => { describe('does not render XSS links', () => {
// eslint-disable-next-line no-script-url // eslint-disable-next-line no-script-url
const badUrl = 'javascript:alert("")'; const badUrl = 'javascript:alert("")';
......
import { testProviderName, testTrainingUrls } from 'jest/security_configuration/mock_data';
import {
SUPPORTED_IDENTIFIER_TYPES,
SECURITY_TRAINING_URL_STATUS_COMPLETED,
} from 'ee/vulnerabilities/constants';
export const testIdentifiers = [
{ externalType: SUPPORTED_IDENTIFIER_TYPES.cwe },
{ externalType: 'cve' },
];
export const generateNote = ({ id = 1295 } = {}) => ({ export const generateNote = ({ id = 1295 } = {}) => ({
id: `gid://gitlab/DiscussionNote/${id}`, id: `gid://gitlab/DiscussionNote/${id}`,
body: 'Created a note.', body: 'Created a note.',
...@@ -31,3 +42,41 @@ export const addTypenamesToDiscussion = (discussion) => { ...@@ -31,3 +42,41 @@ export const addTypenamesToDiscussion = (discussion) => {
}, },
}; };
}; };
export const defaultProps = {
id: 200,
};
const createSecurityTrainingVulnerability = ({ urlOverrides = {}, urls, identifiers } = {}) => ({
...defaultProps,
identifiers: identifiers || testIdentifiers,
securityTrainingUrls: urls || [
{
name: testProviderName[0],
url: testTrainingUrls[0],
status: SECURITY_TRAINING_URL_STATUS_COMPLETED,
...urlOverrides.first,
},
{
name: testProviderName[1],
url: testTrainingUrls[1],
status: SECURITY_TRAINING_URL_STATUS_COMPLETED,
...urlOverrides.second,
},
],
});
export const getSecurityTrainingVulnerabilityData = (vulnerabilityOverrides = {}) => {
const vulnerability = createSecurityTrainingVulnerability(vulnerabilityOverrides);
const response = {
data: {
vulnerability,
},
};
return {
response,
data: vulnerability,
};
};
...@@ -13,12 +13,12 @@ describe('Vulnerability Details', () => { ...@@ -13,12 +13,12 @@ describe('Vulnerability Details', () => {
let wrapper; let wrapper;
const vulnerability = { const vulnerability = {
id: 123,
severity: 'bad severity', severity: 'bad severity',
confidence: 'high confidence', confidence: 'high confidence',
reportType: 'Some report type', reportType: 'Some report type',
description: 'vulnerability description', description: 'vulnerability description',
descriptionHtml: 'vulnerability description <code>sample</code>', descriptionHtml: 'vulnerability description <code>sample</code>',
identifiers: [],
}; };
const createWrapper = (vulnerabilityOverrides, { mountFn = mount, options = {} } = {}) => { const createWrapper = (vulnerabilityOverrides, { mountFn = mount, options = {} } = {}) => {
...@@ -204,31 +204,24 @@ describe('Vulnerability Details', () => { ...@@ -204,31 +204,24 @@ describe('Vulnerability Details', () => {
}); });
describe('VulnerabilityTraining', () => { describe('VulnerabilityTraining', () => {
const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }]; const { id } = vulnerability;
it('renders component', () => { it('renders component', () => {
createShallowWrapper({ createShallowWrapper();
identifiers,
});
expect(findVulnerabilityTraining().props()).toMatchObject({ expect(findVulnerabilityTraining().props()).toMatchObject({
identifiers, id,
}); });
}); });
it('renders title text', () => { it('renders title text', () => {
createShallowWrapper( createShallowWrapper(null, {
{ stubs: {
identifiers, VulnerabilityTraining: {
}, template: '<div><slot name="header"></slot></div>',
{
stubs: {
VulnerabilityTraining: {
template: '<div><slot name="header"></slot></div>',
},
}, },
}, },
); });
expect(wrapper.text()).toContain(VULNERABILITY_TRAINING_HEADING.title); expect(wrapper.text()).toContain(VULNERABILITY_TRAINING_HEADING.title);
}); });
......
...@@ -41056,9 +41056,6 @@ msgstr "" ...@@ -41056,9 +41056,6 @@ msgstr ""
msgid "Vulnerability|Scanner Provider" msgid "Vulnerability|Scanner Provider"
msgstr "" msgstr ""
msgid "Vulnerability|Secure Code Warrior"
msgstr ""
msgid "Vulnerability|Security Audit" msgid "Vulnerability|Security Audit"
msgstr "" msgstr ""
......
export const testProjectPath = 'foo/bar'; export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103]; export const testProviderIds = [101, 102, 103];
export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3']; export const testProviderName = ['Vendor Name 1', 'Vendor Name 2', 'Vendor Name 3'];
export const testTrainingUrls = [
'https://www.vendornameone.com/url',
'https://www.vendornametwo.com/url',
];
const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [ const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [
{ {
......
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