Commit b6008e3f authored by Samantha Ming's avatar Samantha Ming
parent 6fd529f6
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
import { REPORT_TYPES } from 'ee/security_dashboard/store/constants';
import FalsePositiveAlert from 'ee/vulnerabilities/components/false_positive_alert.vue';
import GenericReportSection from 'ee/vulnerabilities/components/generic_report/report_section.vue';
import {
SUPPORTING_MESSAGE_TYPES,
VULNERABILITY_TRAINING_HEADING,
} from 'ee/vulnerabilities/constants';
import {
convertObjectPropsToCamelCase,
convertArrayOfObjectsToCamelCase,
} from '~/lib/utils/common_utils';
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { s__, sprintf } from '~/locale';
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 { bodyWithFallBack } from './helpers';
import SeverityBadge from './severity_badge.vue';
......@@ -30,17 +23,11 @@ export default {
GlLink,
GlBadge,
FalsePositiveAlert,
VulnerabilityTraining,
},
directives: {
SafeHtml: GlSafeHtmlDirective,
},
props: { vulnerability: { type: Object, required: true } },
data() {
return {
showTraining: false,
};
},
computed: {
url() {
return this.vulnerability.request?.url || getFileLocation(this.vulnLocation);
......@@ -154,9 +141,6 @@ export default {
hasRecordedResponse() {
return Boolean(this.constructedRecordedResponse);
},
camelCaseFormattedIdentifiers() {
return convertArrayOfObjectsToCamelCase(this.identifiers);
},
},
methods: {
getHeadersAsCodeBlockLines(headers) {
......@@ -191,12 +175,6 @@ export default {
? [`${method} ${url}\n`, headerLines, '\n\n', bodyWithFallBack(body)].join('')
: '';
},
handleShowTraining(showVulnerabilityTraining) {
this.showTraining = showVulnerabilityTraining;
},
},
i18n: {
VULNERABILITY_TRAINING_HEADING,
},
};
</script>
......@@ -331,13 +309,5 @@ export default {
class="gl-mt-4"
: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>
</template>
......@@ -380,7 +380,7 @@ export default {
</ul>
</template>
<vulnerability-training :identifiers="vulnerability.identifiers">
<vulnerability-training :id="vulnerability.id">
<template #header>
<h3>{{ $options.VULNERABILITY_TRAINING_HEADING.title }}</h3>
</template>
......
......@@ -3,14 +3,13 @@ import { GlLink, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { s__, __ } from '~/locale';
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 axios from '~/lib/utils/axios_utils';
import Tracking from '~/tracking';
import {
TRACK_CLICK_TRAINING_LINK_ACTION,
TRACK_TRAINING_LOADED_ACTION,
} from '~/security_configuration/constants';
import { SUPPORTED_IDENTIFIER_TYPES } from '../constants';
import { TRACK_CLICK_TRAINING_LINK_ACTION } from '~/security_configuration/constants';
import { SUPPORTED_IDENTIFIER_TYPES, SECURITY_TRAINING_URL_STATUS_COMPLETED } from '../constants';
export const i18n = {
trainingDescription: s__(
......@@ -21,12 +20,6 @@ export const i18n = {
loading: __('Loading'),
};
export const mockProvider = {
path: 'https://integration-api.securecodewarrior.com/api/v1/trial',
id: 'gitlab',
name: s__('Vulnerability|Secure Code Warrior'),
};
export default {
i18n,
components: {
......@@ -37,8 +30,8 @@ export default {
mixins: [glFeatureFlagsMixin(), Tracking.mixin()],
inject: ['projectFullPath'],
props: {
identifiers: {
type: Array,
id: {
type: Number,
required: true,
},
},
......@@ -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() {
return {
securityTrainingProviders: [],
vulnerability: {},
training: null,
isLoading: true,
hasError: false,
isUrlsLoading: true,
};
},
computed: {
showVulnerabilityTraining() {
return Boolean(
this.glFeatures.secureVulnerabilityTraining &&
this.enabledSecurityTrainingProviders?.length &&
this.identifiers?.length,
this.glFeatures.secureVulnerabilityTraining && this.hasSecurityTrainingProviders,
);
},
enabledSecurityTrainingProviders() {
return this.securityTrainingProviders?.filter((provider) => provider.isEnabled);
showTrainingNotFound() {
return !this.hasSupportedIdentifier || !this.hasSecurityTrainingUrls;
},
hasSecurityTrainingProviders() {
return this.securityTrainingProviders?.some(({ isEnabled }) => isEnabled);
},
supportedIdentifier() {
return this.identifiers?.find(
hasSupportedIdentifier() {
return this.vulnerability?.identifiers?.some(
({ externalType }) => externalType?.toLowerCase() === SUPPORTED_IDENTIFIER_TYPES.cwe,
);
},
showTrainingNotFound() {
return !this.supportedIdentifier || this.hasError;
hasSecurityTrainingUrls() {
return this.vulnerability?.securityTrainingUrls?.length > 0;
},
securityTrainingUrls() {
return this.vulnerability?.securityTrainingUrls;
},
},
watch: {
......@@ -93,49 +111,15 @@ export default {
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: {
async fetchTraining(mappingList, mappingKey) {
const { path, id, name } = mockProvider;
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(name, url) {
this.triggerMetric(TRACK_CLICK_TRAINING_LINK_ACTION, name, url);
},
clickTrainingLink() {
this.triggerMetric(TRACK_CLICK_TRAINING_LINK_ACTION);
},
triggerMetric(action) {
const { name } = this.supportedIdentifier;
const { id } = mockProvider;
triggerMetric(action, name, url) {
this.track(action, {
label: `vendor_${id}`,
property: name,
property: url,
label: `vendor_${name}`,
});
},
},
......@@ -151,15 +135,19 @@ export default {
<p v-if="showTrainingNotFound" data-testid="unavailable-message">
{{ $options.i18n.trainingUnavailable }}
</p>
<div v-else-if="isLoading">
<div v-else-if="isUrlsLoading">
<gl-skeleton-loader :width="200" :lines="3" />
</div>
<div v-else>
<div class="gl-font-weight-bold gl-font-base">{{ training.name }}</div>
<gl-link :href="training.url" target="_blank" @click="clickTrainingLink">
{{ $options.i18n.viewTraining }}
<gl-icon class="gl-ml-2" name="external-link" :size="12" />
</gl-link>
<div v-for="({ name, url }, index) in securityTrainingUrls" :key="index" class="gl-mt-6">
<div>
<span class="gl-font-weight-bold gl-font-base">{{ name }}</span>
</div>
<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>
</template>
......@@ -93,3 +93,6 @@ export const SUPPORTED_IDENTIFIER_TYPES = {
export const VULNERABILITY_TRAINING_HEADING = {
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
<!---->
<!---->
<div
style="display: none;"
>
<vulnerability-detail-stub
label="Training"
>
<vulnerability-training-stub
identifiers="[object Object],[object Object]"
/>
</vulnerability-detail-stub>
</div>
</div>
`;
......@@ -9,7 +9,6 @@ import GenericReportSection from 'ee/vulnerabilities/components/generic_report/r
import { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { TEST_HOST } from 'helpers/test_constants';
import VulnerabilityTraining from 'ee/vulnerabilities/components/vulnerability_training.vue';
import { mockFindings } from '../mock_data';
function makeVulnerability(changes = {}) {
......@@ -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', () => {
// eslint-disable-next-line no-script-url
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 } = {}) => ({
id: `gid://gitlab/DiscussionNote/${id}`,
body: 'Created a note.',
......@@ -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', () => {
let wrapper;
const vulnerability = {
id: 123,
severity: 'bad severity',
confidence: 'high confidence',
reportType: 'Some report type',
description: 'vulnerability description',
descriptionHtml: 'vulnerability description <code>sample</code>',
identifiers: [],
};
const createWrapper = (vulnerabilityOverrides, { mountFn = mount, options = {} } = {}) => {
......@@ -204,31 +204,24 @@ describe('Vulnerability Details', () => {
});
describe('VulnerabilityTraining', () => {
const identifiers = [{ externalType: 'cwe' }, { externalType: 'cve' }];
const { id } = vulnerability;
it('renders component', () => {
createShallowWrapper({
identifiers,
});
createShallowWrapper();
expect(findVulnerabilityTraining().props()).toMatchObject({
identifiers,
id,
});
});
it('renders title text', () => {
createShallowWrapper(
{
identifiers,
},
{
stubs: {
VulnerabilityTraining: {
template: '<div><slot name="header"></slot></div>',
},
createShallowWrapper(null, {
stubs: {
VulnerabilityTraining: {
template: '<div><slot name="header"></slot></div>',
},
},
);
});
expect(wrapper.text()).toContain(VULNERABILITY_TRAINING_HEADING.title);
});
......
......@@ -41011,9 +41011,6 @@ msgstr ""
msgid "Vulnerability|Scanner Provider"
msgstr ""
msgid "Vulnerability|Secure Code Warrior"
msgstr ""
msgid "Vulnerability|Security Audit"
msgstr ""
......
export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102, 103];
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 = {} }) => [
{
......
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