Commit 8ff24e29 authored by David O'Regan's avatar David O'Regan

Merge branch '273423-implement-counts-in-core-security-mr-widget' into 'master'

Implement vulnerability counts in basic security MR widget [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!47656
parents f86a621c 71adfb2f
......@@ -3,7 +3,7 @@ import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue';
import IssuesList from './issues_list.vue';
import { status } from '../constants';
import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants';
export default {
name: 'ReportSection',
......@@ -152,12 +152,12 @@ export default {
},
slotName() {
if (this.isSuccess) {
return 'success';
return SLOT_SUCCESS;
} else if (this.isLoading) {
return 'loading';
return SLOT_LOADING;
}
return 'error';
return SLOT_ERROR;
},
},
methods: {
......
......@@ -25,3 +25,11 @@ export const status = {
export const ACCESSIBILITY_ISSUE_ERROR = 'error';
export const ACCESSIBILITY_ISSUE_WARNING = 'warning';
/**
* Slot names for the ReportSection component, corresponding to the success,
* loading and error statuses.
*/
export const SLOT_SUCCESS = 'success';
export const SLOT_LOADING = 'loading';
export const SLOT_ERROR = 'error';
......@@ -242,6 +242,10 @@ export default class MergeRequestStore {
this.baseBlobPath = blobPath.base_path || '';
this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate;
// Security reports
this.sastComparisonPath = data.sast_comparison_path;
this.secretScanningComparisonPath = data.secret_scanning_comparison_path;
}
get isNothingToMergeState() {
......
export const SEVERITY_CLASS_NAME_MAP = {
critical: 'text-danger-800',
high: 'text-danger-600',
medium: 'text-warning-400',
low: 'text-warning-200',
info: 'text-primary-400',
unknown: 'text-secondary-400',
};
export const FEEDBACK_TYPE_DISMISSAL = 'dismissal';
export const FEEDBACK_TYPE_ISSUE = 'issue';
export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request';
/**
* Security scan report types, as provided by the backend.
*/
export const REPORT_TYPE_SAST = 'sast';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
<script>
import { mapActions, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReportSection from '~/reports/components/report_section.vue';
import { status } from '~/reports/constants';
import { LOADING, ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
import { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import Flash from '~/flash';
import createFlash from '~/flash';
import Api from '~/api';
import SecuritySummary from './components/security_summary.vue';
import store from './store';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import { REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION } from './constants';
export default {
store,
components: {
GlIcon,
GlLink,
GlSprintf,
ReportSection,
SecuritySummary,
},
mixins: [glFeatureFlagsMixin()],
props: {
pipelineId: {
type: Number,
......@@ -27,25 +36,53 @@ export default {
type: String,
required: true,
},
sastComparisonPath: {
type: String,
required: false,
default: '',
},
secretScanningComparisonPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
hasSecurityReports: false,
availableSecurityReports: [],
canShowCounts: false,
// Error state is shown even when successfully loaded, since success
// When core_security_mr_widget_counts is not enabled, the
// error state is shown even when successfully loaded, since success
// state suggests that the security scans detected no security problems,
// which is not necessarily the case. A future iteration will actually
// check whether problems were found and display the appropriate status.
status: status.ERROR,
status: ERROR,
};
},
computed: {
...mapGetters(['groupedSummaryText', 'summaryStatus']),
hasSecurityReports() {
return this.availableSecurityReports.length > 0;
},
hasSastReports() {
return this.availableSecurityReports.includes(REPORT_TYPE_SAST);
},
hasSecretDetectionReports() {
return this.availableSecurityReports.includes(REPORT_TYPE_SECRET_DETECTION);
},
isLoaded() {
return this.summaryStatus !== LOADING;
},
},
created() {
this.checkHasSecurityReports(this.$options.reportTypes)
.then(hasSecurityReports => {
this.hasSecurityReports = hasSecurityReports;
this.checkAvailableSecurityReports(this.$options.reportTypes)
.then(availableSecurityReports => {
this.availableSecurityReports = Array.from(availableSecurityReports);
this.fetchCounts();
})
.catch(error => {
Flash({
createFlash({
message: this.$options.i18n.apiError,
captureError: true,
error,
......@@ -53,7 +90,18 @@ export default {
});
},
methods: {
async checkHasSecurityReports(reportTypes) {
...mapActions(MODULE_SAST, {
setSastDiffEndpoint: 'setDiffEndpoint',
fetchSastDiff: 'fetchDiff',
}),
...mapActions(MODULE_SECRET_DETECTION, {
setSecretDetectionDiffEndpoint: 'setDiffEndpoint',
fetchSecretDetectionDiff: 'fetchDiff',
}),
async checkAvailableSecurityReports(reportTypes) {
const reportTypesSet = new Set(reportTypes);
const availableReportTypes = new Set();
let page = 1;
while (page) {
// eslint-disable-next-line no-await-in-loop
......@@ -62,18 +110,40 @@ export default {
page,
});
const hasSecurityReports = jobs.some(({ artifacts = [] }) =>
artifacts.some(({ file_type }) => reportTypes.includes(file_type)),
);
jobs.forEach(({ artifacts = [] }) => {
artifacts.forEach(({ file_type }) => {
if (reportTypesSet.has(file_type)) {
availableReportTypes.add(file_type);
}
});
});
if (hasSecurityReports) {
return true;
// If we've found artifacts for all the report types, stop looking!
if (availableReportTypes.size === reportTypesSet.size) {
return availableReportTypes;
}
page = parseIntPagination(normalizeHeaders(headers)).nextPage;
}
return false;
return availableReportTypes;
},
fetchCounts() {
if (!this.glFeatures.coreSecurityMrWidgetCounts) {
return;
}
if (this.sastComparisonPath && this.hasSastReports) {
this.setSastDiffEndpoint(this.sastComparisonPath);
this.fetchSastDiff();
this.canShowCounts = true;
}
if (this.secretScanningComparisonPath && this.hasSecretDetectionReports) {
this.setSecretDetectionDiffEndpoint(this.secretScanningComparisonPath);
this.fetchSecretDetectionDiff();
this.canShowCounts = true;
}
},
activatePipelinesTab() {
if (window.mrTabs) {
......@@ -81,7 +151,7 @@ export default {
}
},
},
reportTypes: ['sast', 'secret_detection'],
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
i18n: {
apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
......@@ -89,13 +159,57 @@ export default {
scansHaveRun: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
downloadFromPipelineTab: s__(
'SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports',
),
securityReportsHelp: s__('SecurityReports|Security reports help page link'),
},
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
};
</script>
<template>
<report-section
v-if="hasSecurityReports"
v-if="canShowCounts"
:status="summaryStatus"
:has-issues="false"
class="mr-widget-border-top mr-report"
data-testid="security-mr-widget"
>
<template v-for="slot in $options.summarySlots" #[slot]>
<span :key="slot">
<security-summary :message="groupedSummaryText" />
<gl-link
target="_blank"
data-testid="help"
:href="securityReportsDocsPath"
:aria-label="$options.i18n.securityReportsHelp"
>
<gl-icon name="question" />
</gl-link>
</span>
</template>
<template v-if="isLoaded" #sub-heading>
<span class="gl-font-sm">
<gl-sprintf :message="$options.i18n.downloadFromPipelineTab">
<template #link="{ content }">
<gl-link
class="gl-font-sm"
data-testid="show-pipelines"
@click="activatePipelinesTab"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</span>
</template>
</report-section>
<!-- TODO: Remove this section when removing core_security_mr_widget_counts
feature flag. See https://gitlab.com/gitlab-org/gitlab/-/issues/284097 -->
<report-section
v-else-if="hasSecurityReports"
:status="status"
:has-issues="false"
class="mr-widget-border-top mr-report"
......
......@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:test_failure_history, @project)
......
---
name: core_security_mr_widget_counts
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47656
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/284097
milestone: '13.7'
type: development
group: group::static analysis
default_enabled: false
......@@ -318,6 +318,8 @@ export default {
:pipeline-id="mr.pipeline.id"
:project-id="mr.targetProjectId"
:security-reports-docs-path="mr.securityReportsDocsPath"
:sast-comparison-path="mr.sastComparisonPath"
:secret-scanning-comparison-path="mr.secretScanningComparisonPath"
/>
<grouped-security-reports-app
v-else-if="shouldRenderExtendedSecurityReport"
......@@ -349,6 +351,12 @@ export default {
:mr-state="mr.state"
:target-branch-tree-path="mr.targetBranchTreePath"
:new-pipeline-path="mr.newPipelinePath"
:container-scanning-comparison-path="mr.containerScanningComparisonPath"
:coverage-fuzzing-comparison-path="mr.coverageFuzzingComparisonPath"
:dast-comparison-path="mr.dastComparisonPath"
:dependency-scanning-comparison-path="mr.dependencyScanningComparisonPath"
:sast-comparison-path="mr.sastComparisonPath"
:secret-scanning-comparison-path="mr.secretScanningComparisonPath"
class="js-security-widget"
/>
<mr-widget-licenses
......
......@@ -50,6 +50,17 @@ export default class MergeRequestStore extends CEMergeRequestStore {
super.setData(data, isRebased);
}
setPaths(data) {
// Paths are set on the first load of the page and not auto-refreshed
super.setPaths(data);
// Security scan diff paths
this.containerScanningComparisonPath = data.container_scanning_comparison_path;
this.coverageFuzzingComparisonPath = data.coverage_fuzzing_comparison_path;
this.dastComparisonPath = data.dast_comparison_path;
this.dependencyScanningComparisonPath = data.dependency_scanning_comparison_path;
}
initGeo(data) {
this.isGeoSecondaryNode = this.isGeoSecondaryNode || data.is_geo_secondary_node;
this.geoSecondaryHelpPath = this.geoSecondaryHelpPath || data.geo_secondary_help_path;
......
import { s__ } from '~/locale';
export const SEVERITY_CLASS_NAME_MAP = {
critical: 'text-danger-800',
high: 'text-danger-600',
medium: 'text-warning-400',
low: 'text-warning-200',
info: 'text-primary-400',
unknown: 'text-secondary-400',
};
export * from '~/vue_shared/security_reports/components/constants';
export const SEVERITY_TOOLTIP_TITLE_MAP = {
unknown: s__(
......
......@@ -9,6 +9,7 @@ import ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue';
import Tracking from '~/tracking';
import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue';
import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
import IssueModal from './components/modal.vue';
import DastModal from './components/dast_modal.vue';
import securityReportsMixin from './mixins/security_report_mixin';
......@@ -16,7 +17,6 @@ import createStore from './store';
import { mrStates } from '~/mr_popover/constants';
import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue';
import {
MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING,
......@@ -190,6 +190,36 @@ export default {
type: String,
required: true,
},
containerScanningComparisonPath: {
type: String,
required: false,
default: '',
},
coverageFuzzingComparisonPath: {
type: String,
required: false,
default: '',
},
dastComparisonPath: {
type: String,
required: false,
default: '',
},
dependencyScanningComparisonPath: {
type: String,
required: false,
default: '',
},
sastComparisonPath: {
type: String,
required: false,
default: '',
},
secretScanningComparisonPath: {
type: String,
required: false,
default: '',
},
},
componentNames,
computed: {
......@@ -305,44 +335,33 @@ export default {
this.setPipelineId(this.pipelineId);
this.setPipelineJobsId(this.pipelineId);
const sastDiffEndpoint = gl?.mrWidgetData?.sast_comparison_path;
if (sastDiffEndpoint && this.hasSastReports) {
this.setSastDiffEndpoint(sastDiffEndpoint);
if (this.sastComparisonPath && this.hasSastReports) {
this.setSastDiffEndpoint(this.sastComparisonPath);
this.fetchSastDiff();
}
const containerScanningDiffEndpoint = gl?.mrWidgetData?.container_scanning_comparison_path;
if (containerScanningDiffEndpoint && this.hasContainerScanningReports) {
this.setContainerScanningDiffEndpoint(containerScanningDiffEndpoint);
if (this.containerScanningComparisonPath && this.hasContainerScanningReports) {
this.setContainerScanningDiffEndpoint(this.containerScanningComparisonPath);
this.fetchContainerScanningDiff();
}
const dastDiffEndpoint = gl?.mrWidgetData?.dast_comparison_path;
if (dastDiffEndpoint && this.hasDastReports) {
this.setDastDiffEndpoint(dastDiffEndpoint);
if (this.dastComparisonPath && this.hasDastReports) {
this.setDastDiffEndpoint(this.dastComparisonPath);
this.fetchDastDiff();
}
const dependencyScanningDiffEndpoint = gl?.mrWidgetData?.dependency_scanning_comparison_path;
if (dependencyScanningDiffEndpoint && this.hasDependencyScanningReports) {
this.setDependencyScanningDiffEndpoint(dependencyScanningDiffEndpoint);
if (this.dependencyScanningComparisonPath && this.hasDependencyScanningReports) {
this.setDependencyScanningDiffEndpoint(this.dependencyScanningComparisonPath);
this.fetchDependencyScanningDiff();
}
const secretDetectionDiffEndpoint = gl?.mrWidgetData?.secret_scanning_comparison_path;
if (secretDetectionDiffEndpoint && this.hasSecretDetectionReports) {
this.setSecretDetectionDiffEndpoint(secretDetectionDiffEndpoint);
if (this.secretScanningComparisonPath && this.hasSecretDetectionReports) {
this.setSecretDetectionDiffEndpoint(this.secretScanningComparisonPath);
this.fetchSecretDetectionDiff();
}
const coverageFuzzingDiffEndpoint = gl?.mrWidgetData?.coverage_fuzzing_comparison_path;
if (coverageFuzzingDiffEndpoint && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint);
if (this.coverageFuzzingComparisonPath && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(this.coverageFuzzingComparisonPath);
this.fetchCoverageFuzzingDiff();
this.fetchPipelineJobs();
}
......
......@@ -12,6 +12,10 @@ export default {
license_management: false,
secret_detection: false,
},
container_scanning_comparison_path: '/container_scanning_comparison_path',
dependency_scanning_comparison_path: '/dependency_scanning_comparison_path',
dast_comparison_path: '/dast_comparison_path',
coverage_fuzzing_comparison_path: '/coverage_fuzzing_comparison_path',
};
// Browser Performance Testing
......
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import mockData from 'ee_jest/vue_mr_widget/mock_data';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import { convertToCamelCase } from '~/lib/utils/text_utility';
describe('MergeRequestStore', () => {
let store;
......@@ -66,4 +67,23 @@ describe('MergeRequestStore', () => {
});
});
});
describe('setPaths', () => {
it.each([
'container_scanning_comparison_path',
'dependency_scanning_comparison_path',
'sast_comparison_path',
'dast_comparison_path',
'secret_scanning_comparison_path',
'coverage_fuzzing_comparison_path',
])('should set %s path', property => {
// Ensure something is set in the mock data
expect(property in mockData).toBe(true);
const expectedValue = mockData[property];
store.setPaths({ ...mockData });
expect(store[convertToCamelCase(property)]).toBe(expectedValue);
});
});
});
......@@ -54,6 +54,12 @@ describe('Grouped security reports app', () => {
pipelineId: 123,
projectId: 321,
projectFullPath: 'path',
containerScanningComparisonPath: CONTAINER_SCANNING_DIFF_ENDPOINT,
coverageFuzzingComparisonPath: COVERAGE_FUZZING_DIFF_ENDPOINT,
dastComparisonPath: DAST_DIFF_ENDPOINT,
dependencyScanningComparisonPath: DEPENDENCY_SCANNING_DIFF_ENDPOINT,
sastComparisonPath: SAST_DIFF_ENDPOINT,
secretScanningComparisonPath: SECRET_DETECTION_DIFF_ENDPOINT,
};
const defaultDastSummary = {
......@@ -112,16 +118,6 @@ describe('Grouped security reports app', () => {
},
};
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.container_scanning_comparison_path = CONTAINER_SCANNING_DIFF_ENDPOINT;
gl.mrWidgetData.dependency_scanning_comparison_path = DEPENDENCY_SCANNING_DIFF_ENDPOINT;
gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT;
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_DETECTION_DIFF_ENDPOINT;
gl.mrWidgetData.coverage_fuzzing_comparison_path = COVERAGE_FUZZING_DIFF_ENDPOINT;
});
describe('with error', () => {
beforeEach(() => {
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(500);
......@@ -394,13 +390,8 @@ describe('Grouped security reports app', () => {
});
describe('coverage fuzzing reports', () => {
describe.each([true, false])(
'given coverage fuzzing comparison endpoint is /fuzzing and featureEnabled is %s',
shouldShowFuzzing => {
describe.each([true, false])('given featureEnabled is %s', shouldShowFuzzing => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.coverage_fuzzing_comparison_path = '/fuzzing';
createWrapper(
{
...props,
......@@ -420,15 +411,11 @@ describe('Grouped security reports app', () => {
shouldShowFuzzing,
);
});
},
);
});
});
describe('container scanning reports', () => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.container_scanning_comparison_path = CONTAINER_SCANNING_DIFF_ENDPOINT;
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
createWrapper({
......@@ -456,9 +443,6 @@ describe('Grouped security reports app', () => {
describe('dependency scanning reports', () => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.dependency_scanning_comparison_path = DEPENDENCY_SCANNING_DIFF_ENDPOINT;
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock);
createWrapper({
......@@ -486,9 +470,6 @@ describe('Grouped security reports app', () => {
describe('dast reports', () => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT;
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {
...dastDiffSuccessMock,
base_report_out_of_date: true,
......@@ -562,9 +543,6 @@ describe('Grouped security reports app', () => {
describe('secret scanning reports', () => {
const initSecretScan = (isEnabled = true) => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.secret_scanning_comparison_path = SECRET_DETECTION_DIFF_ENDPOINT;
mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
createWrapper({
......@@ -615,9 +593,6 @@ describe('Grouped security reports app', () => {
describe('sast reports', () => {
beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, { ...sastDiffSuccessMock });
createWrapper({
......@@ -643,9 +618,6 @@ describe('Grouped security reports app', () => {
describe('Out of date report', () => {
const createComponent = (extraProp, done) => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
mock
.onGet(SAST_DIFF_ENDPOINT)
.reply(200, { ...sastDiffSuccessMock, base_report_out_of_date: true });
......
import { mockFindings } from 'jest/vue_shared/security_reports/mock_data';
export * from 'jest/vue_shared/security_reports/mock_data';
const libTiffCveFingerprint2 = '29af456d1107381bc2511646e2ae488ddfe9a8ed';
export const sastParsedIssues = [
......@@ -302,317 +306,6 @@ export const coverageFuzzingFeedbacks = [
},
];
export const mockFindings = [
{
id: null,
report_type: 'dependency_scanning',
name: 'Cross-site Scripting in serialize-javascript',
severity: 'critical',
scanner: {
external_id: 'gemnasium',
name: 'Gemnasium',
version: '1.1.1',
url: 'https://gitlab.com/gitlab-org/security-products/gemnasium',
},
identifiers: [
{
external_type: 'gemnasium',
external_id: '58caa017-9a9a-46d6-bab2-ec930f46833c',
name: 'Gemnasium-58caa017-9a9a-46d6-bab2-ec930f46833c',
url:
'https://deps.sec.gitlab.com/packages/npm/serialize-javascript/versions/1.7.0/advisories',
},
{
external_type: 'cve',
external_id: 'CVE-2019-16769',
name: 'CVE-2019-16769',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16769',
},
],
project_fingerprint: '09df9f4d11c8deb93d81bdcc39f7667b44143298',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: null,
issue_feedback: null,
merge_request_feedback: null,
description:
'The serialize-javascript npm package is vulnerable to Cross-site Scripting (XSS). It does not properly mitigate against unsafe characters in serialized regular expressions. If serialized data of regular expression objects are used in an environment other than Node.js, it is affected by this vulnerability.',
links: [{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-16769' }],
location: {
file: 'yarn.lock',
dependency: { package: { name: 'serialize-javascript' }, version: '1.7.0' },
},
remediations: [null],
solution: 'Upgrade to version 2.1.1 or above.',
state: 'opened',
blob_path: '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/yarn.lock',
evidence: 'Credit Card Detected: Diners Card',
},
{
id: null,
report_type: 'dependency_scanning',
name: '3rd party CORS request may execute in jquery',
severity: 'high',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2015-9251',
name: 'CVE-2015-9251',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-9251',
},
],
project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: {
id: 2528,
created_at: '2019-08-26T12:30:32.349Z',
project_id: 7071551,
author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
comment_details: {
comment: 'This particular jQuery version appears in a test path of tinycolor2.\n',
comment_timestamp: '2019-08-26T12:30:37.610Z',
comment_author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
},
pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
destroy_vulnerability_feedback_dismissal_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback/2528',
category: 'dependency_scanning',
feedback_type: 'dismissal',
branch: 'leipert-dogfood-secure',
project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
},
issue_feedback: null,
merge_request_feedback: null,
description: null,
links: [
{ url: 'https://github.com/jquery/jquery/issues/2432' },
{ url: 'http://blog.jquery.com/2016/01/08/jquery-2-2-and-1-12-released/' },
{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2015-9251' },
{ url: 'http://research.insecurelabs.org/jquery/test/' },
],
location: {
file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
dependency: { package: { name: 'jquery' }, version: '1.9.1' },
},
remediations: [null],
solution: null,
state: 'dismissed',
blob_path:
'/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
},
{
id: null,
report_type: 'dependency_scanning',
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2019-11358',
name: 'CVE-2019-11358',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
},
],
project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: {
id: 4197,
created_at: '2019-11-14T11:03:18.472Z',
project_id: 7071551,
author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
comment_details: {
comment:
'This is a false positive, as it just part of some documentation assets of sass-true.',
comment_timestamp: '2019-11-14T11:03:18.464Z',
comment_author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
},
destroy_vulnerability_feedback_dismissal_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback/4197',
category: 'dependency_scanning',
feedback_type: 'dismissal',
branch: null,
project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
},
issue_feedback: null,
merge_request_feedback: null,
description: null,
links: [
{ url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
{ url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
],
location: {
file: 'node_modules/sass-true/docs/assets/webpack/common.min.js',
dependency: { package: { name: 'jquery' }, version: '3.3.1' },
},
remediations: [null],
solution: null,
state: 'dismissed',
blob_path:
'/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/sass-true/docs/assets/webpack/common.min.js',
},
{
id: null,
report_type: 'dependency_scanning',
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2019-11358',
name: 'CVE-2019-11358',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
},
],
project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: {
id: 2527,
created_at: '2019-08-26T12:29:43.624Z',
project_id: 7071551,
author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
comment_details: {
comment: 'This particular jQuery version appears in a test path of tinycolor2.',
comment_timestamp: '2019-08-26T12:30:14.840Z',
comment_author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
},
pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
destroy_vulnerability_feedback_dismissal_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback/2527',
category: 'dependency_scanning',
feedback_type: 'dismissal',
branch: 'leipert-dogfood-secure',
project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
},
issue_feedback: null,
merge_request_feedback: null,
description: null,
links: [
{ url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
{ url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
],
location: {
file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
dependency: { package: { name: 'jquery' }, version: '1.9.1' },
},
remediations: [null],
solution: null,
state: 'dismissed',
blob_path:
'/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
},
];
export const sastDiffSuccessMock = {
added: [mockFindings[0]],
fixed: [mockFindings[1], mockFindings[2]],
existing: [mockFindings[3]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
export const dastDiffSuccessMock = {
added: [mockFindings[0]],
fixed: [mockFindings[1], mockFindings[2]],
......@@ -637,14 +330,6 @@ export const dependencyScanningDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
export const secretScanningDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
export const coverageFuzzingDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]],
......
......@@ -24171,6 +24171,9 @@ msgstr ""
msgid "SecurityReports|Fuzzing artifacts"
msgstr ""
msgid "SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
msgstr ""
msgid "SecurityReports|Hide dismissed"
msgstr ""
......
......@@ -264,6 +264,8 @@ export default {
merge_trains_count: 3,
merge_train_index: 1,
security_reports_docs_path: 'security-reports-docs-path',
sast_comparison_path: '/sast_comparison_path',
secret_scanning_comparison_path: '/secret_scanning_comparison_path',
};
export const mockStore = {
......
import { convertToCamelCase } from '~/lib/utils/text_utility';
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from '../mock_data';
......@@ -152,5 +153,18 @@ describe('MergeRequestStore', () => {
expect(store.securityReportsDocsPath).toBe('security-reports-docs-path');
});
it.each(['sast_comparison_path', 'secret_scanning_comparison_path'])(
'should set %s path',
property => {
// Ensure something is set in the mock data
expect(property in mockData).toBe(true);
const expectedValue = mockData[property];
store.setPaths({ ...mockData });
expect(store[convertToCamelCase(property)]).toBe(expectedValue);
},
);
});
});
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SecuritySummary from 'ee/vue_shared/security_reports/components/security_summary.vue';
import { groupedTextBuilder } from 'ee/vue_shared/security_reports/store/utils';
import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
describe('Severity Summary', () => {
let wrapper;
......
export const mockFindings = [
{
id: null,
report_type: 'dependency_scanning',
name: 'Cross-site Scripting in serialize-javascript',
severity: 'critical',
scanner: {
external_id: 'gemnasium',
name: 'Gemnasium',
version: '1.1.1',
url: 'https://gitlab.com/gitlab-org/security-products/gemnasium',
},
identifiers: [
{
external_type: 'gemnasium',
external_id: '58caa017-9a9a-46d6-bab2-ec930f46833c',
name: 'Gemnasium-58caa017-9a9a-46d6-bab2-ec930f46833c',
url:
'https://deps.sec.gitlab.com/packages/npm/serialize-javascript/versions/1.7.0/advisories',
},
{
external_type: 'cve',
external_id: 'CVE-2019-16769',
name: 'CVE-2019-16769',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16769',
},
],
project_fingerprint: '09df9f4d11c8deb93d81bdcc39f7667b44143298',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: null,
issue_feedback: null,
merge_request_feedback: null,
description:
'The serialize-javascript npm package is vulnerable to Cross-site Scripting (XSS). It does not properly mitigate against unsafe characters in serialized regular expressions. If serialized data of regular expression objects are used in an environment other than Node.js, it is affected by this vulnerability.',
links: [{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-16769' }],
location: {
file: 'yarn.lock',
dependency: { package: { name: 'serialize-javascript' }, version: '1.7.0' },
},
remediations: [null],
solution: 'Upgrade to version 2.1.1 or above.',
state: 'opened',
blob_path: '/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/yarn.lock',
evidence: 'Credit Card Detected: Diners Card',
},
{
id: null,
report_type: 'dependency_scanning',
name: '3rd party CORS request may execute in jquery',
severity: 'high',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2015-9251',
name: 'CVE-2015-9251',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2015-9251',
},
],
project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: {
id: 2528,
created_at: '2019-08-26T12:30:32.349Z',
project_id: 7071551,
author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
comment_details: {
comment: 'This particular jQuery version appears in a test path of tinycolor2.\n',
comment_timestamp: '2019-08-26T12:30:37.610Z',
comment_author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
},
pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
destroy_vulnerability_feedback_dismissal_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback/2528',
category: 'dependency_scanning',
feedback_type: 'dismissal',
branch: 'leipert-dogfood-secure',
project_fingerprint: '1ecd3b214cf39c0b9ad23a0a9679778d7cf55876',
},
issue_feedback: null,
merge_request_feedback: null,
description: null,
links: [
{ url: 'https://github.com/jquery/jquery/issues/2432' },
{ url: 'http://blog.jquery.com/2016/01/08/jquery-2-2-and-1-12-released/' },
{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2015-9251' },
{ url: 'http://research.insecurelabs.org/jquery/test/' },
],
location: {
file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
dependency: { package: { name: 'jquery' }, version: '1.9.1' },
},
remediations: [null],
solution: null,
state: 'dismissed',
blob_path:
'/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
},
{
id: null,
report_type: 'dependency_scanning',
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2019-11358',
name: 'CVE-2019-11358',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
},
],
project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: {
id: 4197,
created_at: '2019-11-14T11:03:18.472Z',
project_id: 7071551,
author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
comment_details: {
comment:
'This is a false positive, as it just part of some documentation assets of sass-true.',
comment_timestamp: '2019-11-14T11:03:18.464Z',
comment_author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
},
destroy_vulnerability_feedback_dismissal_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback/4197',
category: 'dependency_scanning',
feedback_type: 'dismissal',
branch: null,
project_fingerprint: 'aeb4b2442d92d0ccf7023f0c220bda8b4ba910e3',
},
issue_feedback: null,
merge_request_feedback: null,
description: null,
links: [
{ url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
{ url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
],
location: {
file: 'node_modules/sass-true/docs/assets/webpack/common.min.js',
dependency: { package: { name: 'jquery' }, version: '3.3.1' },
},
remediations: [null],
solution: null,
state: 'dismissed',
blob_path:
'/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/sass-true/docs/assets/webpack/common.min.js',
},
{
id: null,
report_type: 'dependency_scanning',
name:
'jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution in jquery',
severity: 'low',
scanner: { external_id: 'retire.js', name: 'Retire.js' },
identifiers: [
{
external_type: 'cve',
external_id: 'CVE-2019-11358',
name: 'CVE-2019-11358',
url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358',
},
],
project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
create_vulnerability_feedback_issue_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_merge_request_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback',
create_vulnerability_feedback_dismissal_path: '/gitlab-org/gitlab-ui/vulnerability_feedback',
project: {
id: 7071551,
name: 'gitlab-ui',
full_path: '/gitlab-org/gitlab-ui',
full_name: 'GitLab.org / gitlab-ui',
},
dismissal_feedback: {
id: 2527,
created_at: '2019-08-26T12:29:43.624Z',
project_id: 7071551,
author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
comment_details: {
comment: 'This particular jQuery version appears in a test path of tinycolor2.',
comment_timestamp: '2019-08-26T12:30:14.840Z',
comment_author: {
id: 181229,
name: "Lukas 'Eipi' Eipert",
username: 'leipert',
state: 'active',
avatar_url:
'https://secure.gravatar.com/avatar/19a1f1260fa70323f35bc508927921a2?s=80\u0026d=identicon',
web_url: 'https://gitlab.com/leipert',
status_tooltip_html: null,
path: '/leipert',
},
},
pipeline: { id: 78375355, path: '/gitlab-org/gitlab-ui/pipelines/78375355' },
destroy_vulnerability_feedback_dismissal_path:
'/gitlab-org/gitlab-ui/vulnerability_feedback/2527',
category: 'dependency_scanning',
feedback_type: 'dismissal',
branch: 'leipert-dogfood-secure',
project_fingerprint: 'eb86aa13eb9d897a083ead6e134aa78aa9cadd52',
},
issue_feedback: null,
merge_request_feedback: null,
description: null,
links: [
{ url: 'https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/' },
{ url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-11358' },
{ url: 'https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b' },
],
location: {
file: 'node_modules/tinycolor2/demo/jquery-1.9.1.js',
dependency: { package: { name: 'jquery' }, version: '1.9.1' },
},
remediations: [null],
solution: null,
state: 'dismissed',
blob_path:
'/gitlab-org/gitlab-ui/blob/ad137f0a8ac59af961afe47d04e5cc062c6864a9/node_modules/tinycolor2/demo/jquery-1.9.1.js',
},
];
export const sastDiffSuccessMock = {
added: [mockFindings[0]],
fixed: [mockFindings[1], mockFindings[2]],
existing: [mockFindings[3]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
export const secretScanningDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]],
base_report_created_at: '2020-01-01T10:00:00.000Z',
base_report_out_of_date: false,
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
import { mount } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
import Vuex from 'vuex';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
sastDiffSuccessMock,
secretScanningDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
import Api from '~/api';
import Flash from '~/flash';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
} from '~/vue_shared/security_reports/constants';
import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(Vuex);
const SAST_COMPARISON_PATH = '/sast.json';
const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
describe('Security reports app', () => {
let wrapper;
let mrTabsMock;
const props = {
pipelineId: 123,
......@@ -15,42 +34,72 @@ describe('Security reports app', () => {
securityReportsDocsPath: '/docs',
};
const createComponent = () => {
wrapper = mount(SecurityReportsApp, {
const createComponent = options => {
wrapper = mount(
SecurityReportsApp,
merge(
{
localVue,
propsData: { ...props },
});
},
options,
),
);
};
const anyParams = expect.any(Object);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]');
const setupMrTabsMock = () => {
mrTabsMock = { tabShown: jest.fn() };
window.mrTabs = mrTabsMock;
};
const setupMockJobArtifact = reportType => {
jest
.spyOn(Api, 'pipelineJobs')
.mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] });
};
const expectPipelinesTabAnchor = () => {
const mrTabsMock = { tabShown: jest.fn() };
window.mrTabs = mrTabsMock;
findPipelinesTabAnchor().trigger('click');
expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
};
afterEach(() => {
wrapper.destroy();
delete window.mrTabs;
});
describe.each([false, true])(
'given the coreSecurityMrWidgetCounts feature flag is %p',
coreSecurityMrWidgetCounts => {
const createComponentWithFlag = options =>
createComponent(
merge(
{
provide: {
glFeatures: {
coreSecurityMrWidgetCounts,
},
},
},
options,
),
);
describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
beforeEach(() => {
window.mrTabs = { tabShown: jest.fn() };
setupMockJobArtifact(reportType);
createComponent();
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
expect(Api.pipelineJobs).toHaveBeenCalledWith(
props.projectId,
props.pipelineId,
anyParams,
);
});
it('renders the expected message', () => {
......@@ -58,13 +107,8 @@ describe('Security reports app', () => {
});
describe('clicking the anchor to the pipelines tab', () => {
beforeEach(() => {
setupMrTabsMock();
findPipelinesTabAnchor().trigger('click');
});
it('calls the mrTabs.tabShown global', () => {
expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
expectPipelinesTabAnchor();
});
});
......@@ -78,13 +122,17 @@ describe('Security reports app', () => {
describe('given a report type "foo"', () => {
beforeEach(() => {
setupMockJobArtifact('foo');
createComponent();
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
expect(Api.pipelineJobs).toHaveBeenCalledWith(
props.projectId,
props.pipelineId,
anyParams,
);
});
it('renders nothing', () => {
......@@ -115,7 +163,7 @@ describe('Security reports app', () => {
throw new Error('Test failed due to request of non-existent jobs page');
});
createComponent();
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
......@@ -134,21 +182,25 @@ describe('Security reports app', () => {
beforeEach(() => {
error = new Error('an error');
jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
createComponent();
createComponentWithFlag();
return wrapper.vm.$nextTick();
});
it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
expect(Api.pipelineJobs).toHaveBeenCalledWith(props.projectId, props.pipelineId, anyParams);
expect(Api.pipelineJobs).toHaveBeenCalledWith(
props.projectId,
props.pipelineId,
anyParams,
);
});
it('renders nothing', () => {
expect(wrapper.html()).toBe('');
});
it('calls Flash correctly', () => {
expect(Flash.mock.calls).toEqual([
it('calls createFlash correctly', () => {
expect(createFlash.mock.calls).toEqual([
[
{
message: SecurityReportsApp.i18n.apiError,
......@@ -159,4 +211,113 @@ describe('Security reports app', () => {
]);
});
});
},
);
describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
let mock;
const createComponentWithFlagEnabled = options =>
createComponent(
merge(options, {
provide: {
glFeatures: {
coreSecurityMrWidgetCounts: true,
},
},
}),
);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
const SAST_SUCCESS_MESSAGE =
'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
const SECRET_SCANNING_SUCCESS_MESSAGE =
'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
describe.each`
reportType | pathProp | path | successResponse | successMessage
${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
`(
'given a $pathProp and $reportType artifact',
({ reportType, pathProp, path, successResponse, successMessage }) => {
beforeEach(() => {
setupMockJobArtifact(reportType);
});
describe('when loading', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
mock.onGet(path).replyOnce(200, successResponse);
createComponentWithFlagEnabled({
propsData: {
[pathProp]: path,
},
});
return waitForPromises();
});
it('should have loading message', () => {
expect(wrapper.text()).toBe('Security scanning is loading');
});
it('should not render the pipeline tab anchor', () => {
expect(findPipelinesTabAnchor().exists()).toBe(false);
});
});
describe('when successfully loaded', () => {
beforeEach(() => {
mock.onGet(path).replyOnce(200, successResponse);
createComponentWithFlagEnabled({
propsData: {
[pathProp]: path,
},
});
return waitForPromises();
});
it('should show counts', () => {
expect(trimText(wrapper.text())).toContain(successMessage);
});
it('should render the pipeline tab anchor', () => {
expectPipelinesTabAnchor();
});
});
describe('when an error occurs', () => {
beforeEach(() => {
mock.onGet(path).replyOnce(500);
createComponentWithFlagEnabled({
propsData: {
[pathProp]: path,
},
});
return waitForPromises();
});
it('should show error message', () => {
expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
});
it('should render the pipeline tab anchor', () => {
expectPipelinesTabAnchor();
});
});
},
);
});
});
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