Commit 96358b71 authored by Savas Vedova's avatar Savas Vedova

Merge branch '346067-training-vulnerability-modal' into 'master'

Add security training UI to vulnerability modal

See merge request gitlab-org/gitlab!80193
parents 01c5f177 f903717c
...@@ -3,10 +3,17 @@ import { GlFriendlyWrap, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui ...@@ -3,10 +3,17 @@ 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 { SUPPORTING_MESSAGE_TYPES } from 'ee/vulnerabilities/constants'; import {
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; SUPPORTING_MESSAGE_TYPES,
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';
...@@ -23,11 +30,17 @@ export default { ...@@ -23,11 +30,17 @@ 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);
...@@ -141,6 +154,9 @@ export default { ...@@ -141,6 +154,9 @@ export default {
hasRecordedResponse() { hasRecordedResponse() {
return Boolean(this.constructedRecordedResponse); return Boolean(this.constructedRecordedResponse);
}, },
camelCaseFormattedIdentifiers() {
return convertArrayOfObjectsToCamelCase(this.identifiers);
},
}, },
methods: { methods: {
getHeadersAsCodeBlockLines(headers) { getHeadersAsCodeBlockLines(headers) {
...@@ -175,6 +191,12 @@ export default { ...@@ -175,6 +191,12 @@ 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>
...@@ -309,5 +331,13 @@ export default { ...@@ -309,5 +331,13 @@ 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>
...@@ -65,10 +65,10 @@ export default { ...@@ -65,10 +65,10 @@ export default {
}, },
computed: { computed: {
showVulnerabilityTraining() { showVulnerabilityTraining() {
return ( return Boolean(
this.glFeatures.secureVulnerabilityTraining && this.glFeatures.secureVulnerabilityTraining &&
this.enabledSecurityTrainingProviders?.length && this.enabledSecurityTrainingProviders?.length &&
this.identifiers?.length this.identifiers?.length,
); );
}, },
enabledSecurityTrainingProviders() { enabledSecurityTrainingProviders() {
...@@ -84,6 +84,12 @@ export default { ...@@ -84,6 +84,12 @@ export default {
}, },
}, },
watch: { watch: {
showVulnerabilityTraining: {
immediate: true,
handler(showVulnerabilityTraining) {
this.$emit('show-vulnerability-training', showVulnerabilityTraining);
},
},
supportedIdentifier: { supportedIdentifier: {
immediate: true, immediate: true,
handler(supportedIdentifier) { handler(supportedIdentifier) {
......
...@@ -11,6 +11,7 @@ module EE ...@@ -11,6 +11,7 @@ module EE
before_action do before_action do
push_frontend_feature_flag(:pipeline_security_dashboard_graphql, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:pipeline_security_dashboard_graphql, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_code_quality_full_report, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:graphql_code_quality_full_report, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml)
end end
feature_category :license_compliance, [:licenses] feature_category :license_compliance, [:licenses]
......
...@@ -200,5 +200,17 @@ key2: value2 ...@@ -200,5 +200,17 @@ 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,7 @@ import GenericReportSection from 'ee/vulnerabilities/components/generic_report/r ...@@ -9,7 +9,7 @@ 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 = {}) {
...@@ -23,6 +23,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -23,6 +23,9 @@ describe('VulnerabilityDetails component', () => {
wrapper = mountExtended(VulnerabilityDetails, { wrapper = mountExtended(VulnerabilityDetails, {
propsData: { vulnerability }, propsData: { vulnerability },
provide, provide,
stubs: {
VulnerabilityTraining: true,
},
}); });
}; };
...@@ -135,6 +138,17 @@ describe('VulnerabilityDetails component', () => { ...@@ -135,6 +138,17 @@ 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("")';
......
...@@ -86,6 +86,9 @@ describe('Grouped security reports app', () => { ...@@ -86,6 +86,9 @@ describe('Grouped security reports app', () => {
const createWrapper = (propsData, options, provide) => { const createWrapper = (propsData, options, provide) => {
wrapper = mount(GroupedSecurityReportsApp, { wrapper = mount(GroupedSecurityReportsApp, {
propsData, propsData,
stubs: {
VulnerabilityTraining: true,
},
mocks: { mocks: {
$apollo: { $apollo: {
queries: { queries: {
......
import Vue from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
...@@ -110,6 +110,17 @@ describe('VulnerabilityTraining component', () => { ...@@ -110,6 +110,17 @@ describe('VulnerabilityTraining component', () => {
expect(wrapper.html()).toBeFalsy(); expect(wrapper.html()).toBeFalsy();
}); });
it('watches showVulnerabilityTraining and emits change', async () => {
createApolloProvider();
createComponent();
await waitForQueryToBeLoaded();
await nextTick();
// Note: the event emits twice - the second time is when the query is loaded
expect(wrapper.emitted('show-vulnerability-training')).toEqual([[false], [true]]);
});
}); });
describe('with title slot', () => { describe('with title slot', () => {
......
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