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'; ...@@ -3,7 +3,7 @@ import { __ } from '~/locale';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
import Popover from '~/vue_shared/components/help_popover.vue'; import Popover from '~/vue_shared/components/help_popover.vue';
import IssuesList from './issues_list.vue'; import IssuesList from './issues_list.vue';
import { status } from '../constants'; import { status, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '../constants';
export default { export default {
name: 'ReportSection', name: 'ReportSection',
...@@ -152,12 +152,12 @@ export default { ...@@ -152,12 +152,12 @@ export default {
}, },
slotName() { slotName() {
if (this.isSuccess) { if (this.isSuccess) {
return 'success'; return SLOT_SUCCESS;
} else if (this.isLoading) { } else if (this.isLoading) {
return 'loading'; return SLOT_LOADING;
} }
return 'error'; return SLOT_ERROR;
}, },
}, },
methods: { methods: {
......
...@@ -25,3 +25,11 @@ export const status = { ...@@ -25,3 +25,11 @@ export const status = {
export const ACCESSIBILITY_ISSUE_ERROR = 'error'; export const ACCESSIBILITY_ISSUE_ERROR = 'error';
export const ACCESSIBILITY_ISSUE_WARNING = 'warning'; 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 { ...@@ -242,6 +242,10 @@ export default class MergeRequestStore {
this.baseBlobPath = blobPath.base_path || ''; this.baseBlobPath = blobPath.base_path || '';
this.codequalityHelpPath = data.codequality_help_path; this.codequalityHelpPath = data.codequality_help_path;
this.codeclimate = data.codeclimate; this.codeclimate = data.codeclimate;
// Security reports
this.sastComparisonPath = data.sast_comparison_path;
this.secretScanningComparisonPath = data.secret_scanning_comparison_path;
} }
get isNothingToMergeState() { 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_DISMISSAL = 'dismissal';
export const FEEDBACK_TYPE_ISSUE = 'issue'; export const FEEDBACK_TYPE_ISSUE = 'issue';
export const FEEDBACK_TYPE_MERGE_REQUEST = 'merge_request'; 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> <script>
import { mapActions, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui'; 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 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 { s__ } from '~/locale';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils'; import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import Flash from '~/flash'; import createFlash from '~/flash';
import Api from '~/api'; 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 { export default {
store,
components: { components: {
GlIcon, GlIcon,
GlLink, GlLink,
GlSprintf, GlSprintf,
ReportSection, ReportSection,
SecuritySummary,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
pipelineId: { pipelineId: {
type: Number, type: Number,
...@@ -27,25 +36,53 @@ export default { ...@@ -27,25 +36,53 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
sastComparisonPath: {
type: String,
required: false,
default: '',
},
secretScanningComparisonPath: {
type: String,
required: false,
default: '',
},
}, },
data() { data() {
return { 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, // state suggests that the security scans detected no security problems,
// which is not necessarily the case. A future iteration will actually // which is not necessarily the case. A future iteration will actually
// check whether problems were found and display the appropriate status. // 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() { created() {
this.checkHasSecurityReports(this.$options.reportTypes) this.checkAvailableSecurityReports(this.$options.reportTypes)
.then(hasSecurityReports => { .then(availableSecurityReports => {
this.hasSecurityReports = hasSecurityReports; this.availableSecurityReports = Array.from(availableSecurityReports);
this.fetchCounts();
}) })
.catch(error => { .catch(error => {
Flash({ createFlash({
message: this.$options.i18n.apiError, message: this.$options.i18n.apiError,
captureError: true, captureError: true,
error, error,
...@@ -53,7 +90,18 @@ export default { ...@@ -53,7 +90,18 @@ export default {
}); });
}, },
methods: { 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; let page = 1;
while (page) { while (page) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
...@@ -62,18 +110,40 @@ export default { ...@@ -62,18 +110,40 @@ export default {
page, page,
}); });
const hasSecurityReports = jobs.some(({ artifacts = [] }) => jobs.forEach(({ artifacts = [] }) => {
artifacts.some(({ file_type }) => reportTypes.includes(file_type)), artifacts.forEach(({ file_type }) => {
); if (reportTypesSet.has(file_type)) {
availableReportTypes.add(file_type);
}
});
});
if (hasSecurityReports) { // If we've found artifacts for all the report types, stop looking!
return true; if (availableReportTypes.size === reportTypesSet.size) {
return availableReportTypes;
} }
page = parseIntPagination(normalizeHeaders(headers)).nextPage; 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() { activatePipelinesTab() {
if (window.mrTabs) { if (window.mrTabs) {
...@@ -81,7 +151,7 @@ export default { ...@@ -81,7 +151,7 @@ export default {
} }
}, },
}, },
reportTypes: ['sast', 'secret_detection'], reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
i18n: { i18n: {
apiError: s__( apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.', 'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
...@@ -89,13 +159,57 @@ export default { ...@@ -89,13 +159,57 @@ export default {
scansHaveRun: s__( scansHaveRun: s__(
'SecurityReports|Security scans have run. Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports', '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'), securityReportsHelp: s__('SecurityReports|Security reports help page link'),
}, },
summarySlots: [SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR],
}; };
</script> </script>
<template> <template>
<report-section <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" :status="status"
:has-issues="false" :has-issues="false"
class="mr-widget-border-top mr-report" class="mr-widget-border-top mr-report"
......
...@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -39,6 +39,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:highlight_current_diff_row, @project) push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @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, @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(:remove_resolve_note, @project, default_enabled: true)
push_frontend_feature_flag(:test_failure_history, @project) 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 { ...@@ -318,6 +318,8 @@ export default {
:pipeline-id="mr.pipeline.id" :pipeline-id="mr.pipeline.id"
:project-id="mr.targetProjectId" :project-id="mr.targetProjectId"
:security-reports-docs-path="mr.securityReportsDocsPath" :security-reports-docs-path="mr.securityReportsDocsPath"
:sast-comparison-path="mr.sastComparisonPath"
:secret-scanning-comparison-path="mr.secretScanningComparisonPath"
/> />
<grouped-security-reports-app <grouped-security-reports-app
v-else-if="shouldRenderExtendedSecurityReport" v-else-if="shouldRenderExtendedSecurityReport"
...@@ -349,6 +351,12 @@ export default { ...@@ -349,6 +351,12 @@ export default {
:mr-state="mr.state" :mr-state="mr.state"
:target-branch-tree-path="mr.targetBranchTreePath" :target-branch-tree-path="mr.targetBranchTreePath"
:new-pipeline-path="mr.newPipelinePath" :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" class="js-security-widget"
/> />
<mr-widget-licenses <mr-widget-licenses
......
...@@ -50,6 +50,17 @@ export default class MergeRequestStore extends CEMergeRequestStore { ...@@ -50,6 +50,17 @@ export default class MergeRequestStore extends CEMergeRequestStore {
super.setData(data, isRebased); 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) { initGeo(data) {
this.isGeoSecondaryNode = this.isGeoSecondaryNode || data.is_geo_secondary_node; this.isGeoSecondaryNode = this.isGeoSecondaryNode || data.is_geo_secondary_node;
this.geoSecondaryHelpPath = this.geoSecondaryHelpPath || data.geo_secondary_help_path; this.geoSecondaryHelpPath = this.geoSecondaryHelpPath || data.geo_secondary_help_path;
......
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const SEVERITY_CLASS_NAME_MAP = { export * from '~/vue_shared/security_reports/components/constants';
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 SEVERITY_TOOLTIP_TITLE_MAP = { export const SEVERITY_TOOLTIP_TITLE_MAP = {
unknown: s__( unknown: s__(
......
...@@ -9,6 +9,7 @@ import ReportSection from '~/reports/components/report_section.vue'; ...@@ -9,6 +9,7 @@ import ReportSection from '~/reports/components/report_section.vue';
import SummaryRow from '~/reports/components/summary_row.vue'; import SummaryRow from '~/reports/components/summary_row.vue';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import GroupedIssuesList from '~/reports/components/grouped_issues_list.vue'; 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 IssueModal from './components/modal.vue';
import DastModal from './components/dast_modal.vue'; import DastModal from './components/dast_modal.vue';
import securityReportsMixin from './mixins/security_report_mixin'; import securityReportsMixin from './mixins/security_report_mixin';
...@@ -16,7 +17,6 @@ import createStore from './store'; ...@@ -16,7 +17,6 @@ import createStore from './store';
import { mrStates } from '~/mr_popover/constants'; import { mrStates } from '~/mr_popover/constants';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql'; import securityReportSummaryQuery from './graphql/mr_security_report_summary.graphql';
import SecuritySummary from './components/security_summary.vue';
import { import {
MODULE_CONTAINER_SCANNING, MODULE_CONTAINER_SCANNING,
MODULE_COVERAGE_FUZZING, MODULE_COVERAGE_FUZZING,
...@@ -190,6 +190,36 @@ export default { ...@@ -190,6 +190,36 @@ export default {
type: String, type: String,
required: true, 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, componentNames,
computed: { computed: {
...@@ -305,44 +335,33 @@ export default { ...@@ -305,44 +335,33 @@ export default {
this.setPipelineId(this.pipelineId); this.setPipelineId(this.pipelineId);
this.setPipelineJobsId(this.pipelineId); this.setPipelineJobsId(this.pipelineId);
const sastDiffEndpoint = gl?.mrWidgetData?.sast_comparison_path; if (this.sastComparisonPath && this.hasSastReports) {
this.setSastDiffEndpoint(this.sastComparisonPath);
if (sastDiffEndpoint && this.hasSastReports) {
this.setSastDiffEndpoint(sastDiffEndpoint);
this.fetchSastDiff(); this.fetchSastDiff();
} }
const containerScanningDiffEndpoint = gl?.mrWidgetData?.container_scanning_comparison_path; if (this.containerScanningComparisonPath && this.hasContainerScanningReports) {
this.setContainerScanningDiffEndpoint(this.containerScanningComparisonPath);
if (containerScanningDiffEndpoint && this.hasContainerScanningReports) {
this.setContainerScanningDiffEndpoint(containerScanningDiffEndpoint);
this.fetchContainerScanningDiff(); this.fetchContainerScanningDiff();
} }
const dastDiffEndpoint = gl?.mrWidgetData?.dast_comparison_path; if (this.dastComparisonPath && this.hasDastReports) {
this.setDastDiffEndpoint(this.dastComparisonPath);
if (dastDiffEndpoint && this.hasDastReports) {
this.setDastDiffEndpoint(dastDiffEndpoint);
this.fetchDastDiff(); this.fetchDastDiff();
} }
const dependencyScanningDiffEndpoint = gl?.mrWidgetData?.dependency_scanning_comparison_path; if (this.dependencyScanningComparisonPath && this.hasDependencyScanningReports) {
this.setDependencyScanningDiffEndpoint(this.dependencyScanningComparisonPath);
if (dependencyScanningDiffEndpoint && this.hasDependencyScanningReports) {
this.setDependencyScanningDiffEndpoint(dependencyScanningDiffEndpoint);
this.fetchDependencyScanningDiff(); this.fetchDependencyScanningDiff();
} }
const secretDetectionDiffEndpoint = gl?.mrWidgetData?.secret_scanning_comparison_path; if (this.secretScanningComparisonPath && this.hasSecretDetectionReports) {
if (secretDetectionDiffEndpoint && this.hasSecretDetectionReports) { this.setSecretDetectionDiffEndpoint(this.secretScanningComparisonPath);
this.setSecretDetectionDiffEndpoint(secretDetectionDiffEndpoint);
this.fetchSecretDetectionDiff(); this.fetchSecretDetectionDiff();
} }
const coverageFuzzingDiffEndpoint = gl?.mrWidgetData?.coverage_fuzzing_comparison_path; if (this.coverageFuzzingComparisonPath && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(this.coverageFuzzingComparisonPath);
if (coverageFuzzingDiffEndpoint && this.hasCoverageFuzzingReports) {
this.setCoverageFuzzingDiffEndpoint(coverageFuzzingDiffEndpoint);
this.fetchCoverageFuzzingDiff(); this.fetchCoverageFuzzingDiff();
this.fetchPipelineJobs(); this.fetchPipelineJobs();
} }
......
...@@ -12,6 +12,10 @@ export default { ...@@ -12,6 +12,10 @@ export default {
license_management: false, license_management: false,
secret_detection: 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 // Browser Performance Testing
......
import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store'; import MergeRequestStore from 'ee/vue_merge_request_widget/stores/mr_widget_store';
import mockData from 'ee_jest/vue_mr_widget/mock_data'; import mockData from 'ee_jest/vue_mr_widget/mock_data';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import { convertToCamelCase } from '~/lib/utils/text_utility';
describe('MergeRequestStore', () => { describe('MergeRequestStore', () => {
let store; let store;
...@@ -66,4 +67,23 @@ describe('MergeRequestStore', () => { ...@@ -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', () => { ...@@ -54,6 +54,12 @@ describe('Grouped security reports app', () => {
pipelineId: 123, pipelineId: 123,
projectId: 321, projectId: 321,
projectFullPath: 'path', 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 = { const defaultDastSummary = {
...@@ -112,16 +118,6 @@ describe('Grouped security reports app', () => { ...@@ -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', () => { describe('with error', () => {
beforeEach(() => { beforeEach(() => {
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(500); mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(500);
...@@ -394,13 +390,8 @@ describe('Grouped security reports app', () => { ...@@ -394,13 +390,8 @@ describe('Grouped security reports app', () => {
}); });
describe('coverage fuzzing reports', () => { describe('coverage fuzzing reports', () => {
describe.each([true, false])( describe.each([true, false])('given featureEnabled is %s', shouldShowFuzzing => {
'given coverage fuzzing comparison endpoint is /fuzzing and featureEnabled is %s',
shouldShowFuzzing => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.coverage_fuzzing_comparison_path = '/fuzzing';
createWrapper( createWrapper(
{ {
...props, ...props,
...@@ -420,15 +411,11 @@ describe('Grouped security reports app', () => { ...@@ -420,15 +411,11 @@ describe('Grouped security reports app', () => {
shouldShowFuzzing, shouldShowFuzzing,
); );
}); });
}, });
);
}); });
describe('container scanning reports', () => { describe('container scanning reports', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.container_scanning_comparison_path = CONTAINER_SCANNING_DIFF_ENDPOINT;
mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock); mock.onGet(CONTAINER_SCANNING_DIFF_ENDPOINT).reply(200, containerScanningDiffSuccessMock);
createWrapper({ createWrapper({
...@@ -456,9 +443,6 @@ describe('Grouped security reports app', () => { ...@@ -456,9 +443,6 @@ describe('Grouped security reports app', () => {
describe('dependency scanning reports', () => { describe('dependency scanning reports', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.dependency_scanning_comparison_path = DEPENDENCY_SCANNING_DIFF_ENDPOINT;
mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock); mock.onGet(DEPENDENCY_SCANNING_DIFF_ENDPOINT).reply(200, dependencyScanningDiffSuccessMock);
createWrapper({ createWrapper({
...@@ -486,9 +470,6 @@ describe('Grouped security reports app', () => { ...@@ -486,9 +470,6 @@ describe('Grouped security reports app', () => {
describe('dast reports', () => { describe('dast reports', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.dast_comparison_path = DAST_DIFF_ENDPOINT;
mock.onGet(DAST_DIFF_ENDPOINT).reply(200, { mock.onGet(DAST_DIFF_ENDPOINT).reply(200, {
...dastDiffSuccessMock, ...dastDiffSuccessMock,
base_report_out_of_date: true, base_report_out_of_date: true,
...@@ -562,9 +543,6 @@ describe('Grouped security reports app', () => { ...@@ -562,9 +543,6 @@ describe('Grouped security reports app', () => {
describe('secret scanning reports', () => { describe('secret scanning reports', () => {
const initSecretScan = (isEnabled = true) => { 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); mock.onGet(SECRET_DETECTION_DIFF_ENDPOINT).reply(200, secretScanningDiffSuccessMock);
createWrapper({ createWrapper({
...@@ -615,9 +593,6 @@ describe('Grouped security reports app', () => { ...@@ -615,9 +593,6 @@ describe('Grouped security reports app', () => {
describe('sast reports', () => { describe('sast reports', () => {
beforeEach(() => { beforeEach(() => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
mock.onGet(SAST_DIFF_ENDPOINT).reply(200, { ...sastDiffSuccessMock }); mock.onGet(SAST_DIFF_ENDPOINT).reply(200, { ...sastDiffSuccessMock });
createWrapper({ createWrapper({
...@@ -643,9 +618,6 @@ describe('Grouped security reports app', () => { ...@@ -643,9 +618,6 @@ describe('Grouped security reports app', () => {
describe('Out of date report', () => { describe('Out of date report', () => {
const createComponent = (extraProp, done) => { const createComponent = (extraProp, done) => {
gl.mrWidgetData = gl.mrWidgetData || {};
gl.mrWidgetData.sast_comparison_path = SAST_DIFF_ENDPOINT;
mock mock
.onGet(SAST_DIFF_ENDPOINT) .onGet(SAST_DIFF_ENDPOINT)
.reply(200, { ...sastDiffSuccessMock, base_report_out_of_date: true }); .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'; const libTiffCveFingerprint2 = '29af456d1107381bc2511646e2ae488ddfe9a8ed';
export const sastParsedIssues = [ export const sastParsedIssues = [
...@@ -302,317 +306,6 @@ export const coverageFuzzingFeedbacks = [ ...@@ -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 = { export const dastDiffSuccessMock = {
added: [mockFindings[0]], added: [mockFindings[0]],
fixed: [mockFindings[1], mockFindings[2]], fixed: [mockFindings[1], mockFindings[2]],
...@@ -637,14 +330,6 @@ export const dependencyScanningDiffSuccessMock = { ...@@ -637,14 +330,6 @@ export const dependencyScanningDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z', 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 = { export const coverageFuzzingDiffSuccessMock = {
added: [mockFindings[0], mockFindings[1]], added: [mockFindings[0], mockFindings[1]],
fixed: [mockFindings[2]], fixed: [mockFindings[2]],
......
...@@ -24171,6 +24171,9 @@ msgstr "" ...@@ -24171,6 +24171,9 @@ msgstr ""
msgid "SecurityReports|Fuzzing artifacts" msgid "SecurityReports|Fuzzing artifacts"
msgstr "" msgstr ""
msgid "SecurityReports|Go to the %{linkStart}pipelines tab%{linkEnd} to download the security reports"
msgstr ""
msgid "SecurityReports|Hide dismissed" msgid "SecurityReports|Hide dismissed"
msgstr "" msgstr ""
......
...@@ -264,6 +264,8 @@ export default { ...@@ -264,6 +264,8 @@ export default {
merge_trains_count: 3, merge_trains_count: 3,
merge_train_index: 1, merge_train_index: 1,
security_reports_docs_path: 'security-reports-docs-path', 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 = { export const mockStore = {
......
import { convertToCamelCase } from '~/lib/utils/text_utility';
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store'; import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps'; import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from '../mock_data'; import mockData from '../mock_data';
...@@ -152,5 +153,18 @@ describe('MergeRequestStore', () => { ...@@ -152,5 +153,18 @@ describe('MergeRequestStore', () => {
expect(store.securityReportsDocsPath).toBe('security-reports-docs-path'); 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 { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import SecuritySummary from 'ee/vue_shared/security_reports/components/security_summary.vue'; import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
import { groupedTextBuilder } from 'ee/vue_shared/security_reports/store/utils'; import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
describe('Severity Summary', () => { describe('Severity Summary', () => {
let wrapper; 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 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'; import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
jest.mock('~/flash'); 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', () => { describe('Security reports app', () => {
let wrapper; let wrapper;
let mrTabsMock;
const props = { const props = {
pipelineId: 123, pipelineId: 123,
...@@ -15,42 +34,72 @@ describe('Security reports app', () => { ...@@ -15,42 +34,72 @@ describe('Security reports app', () => {
securityReportsDocsPath: '/docs', securityReportsDocsPath: '/docs',
}; };
const createComponent = () => { const createComponent = options => {
wrapper = mount(SecurityReportsApp, { wrapper = mount(
SecurityReportsApp,
merge(
{
localVue,
propsData: { ...props }, propsData: { ...props },
}); },
options,
),
);
}; };
const anyParams = expect.any(Object); const anyParams = expect.any(Object);
const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]'); const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpLink = () => wrapper.find('[data-testid="help"]'); const findHelpLink = () => wrapper.find('[data-testid="help"]');
const setupMrTabsMock = () => {
mrTabsMock = { tabShown: jest.fn() };
window.mrTabs = mrTabsMock;
};
const setupMockJobArtifact = reportType => { const setupMockJobArtifact = reportType => {
jest jest
.spyOn(Api, 'pipelineJobs') .spyOn(Api, 'pipelineJobs')
.mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] }); .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(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
delete window.mrTabs; 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 => { describe.each(SecurityReportsApp.reportTypes)('given a report type %p', reportType => {
beforeEach(() => { beforeEach(() => {
window.mrTabs = { tabShown: jest.fn() }; window.mrTabs = { tabShown: jest.fn() };
setupMockJobArtifact(reportType); setupMockJobArtifact(reportType);
createComponent(); createComponentWithFlag();
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
it('calls the pipelineJobs API correctly', () => { it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); 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', () => { it('renders the expected message', () => {
...@@ -58,13 +107,8 @@ describe('Security reports app', () => { ...@@ -58,13 +107,8 @@ describe('Security reports app', () => {
}); });
describe('clicking the anchor to the pipelines tab', () => { describe('clicking the anchor to the pipelines tab', () => {
beforeEach(() => {
setupMrTabsMock();
findPipelinesTabAnchor().trigger('click');
});
it('calls the mrTabs.tabShown global', () => { it('calls the mrTabs.tabShown global', () => {
expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]); expectPipelinesTabAnchor();
}); });
}); });
...@@ -78,13 +122,17 @@ describe('Security reports app', () => { ...@@ -78,13 +122,17 @@ describe('Security reports app', () => {
describe('given a report type "foo"', () => { describe('given a report type "foo"', () => {
beforeEach(() => { beforeEach(() => {
setupMockJobArtifact('foo'); setupMockJobArtifact('foo');
createComponent(); createComponentWithFlag();
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
it('calls the pipelineJobs API correctly', () => { it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); 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', () => { it('renders nothing', () => {
...@@ -115,7 +163,7 @@ describe('Security reports app', () => { ...@@ -115,7 +163,7 @@ describe('Security reports app', () => {
throw new Error('Test failed due to request of non-existent jobs page'); throw new Error('Test failed due to request of non-existent jobs page');
}); });
createComponent(); createComponentWithFlag();
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
...@@ -134,21 +182,25 @@ describe('Security reports app', () => { ...@@ -134,21 +182,25 @@ describe('Security reports app', () => {
beforeEach(() => { beforeEach(() => {
error = new Error('an error'); error = new Error('an error');
jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error); jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
createComponent(); createComponentWithFlag();
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
it('calls the pipelineJobs API correctly', () => { it('calls the pipelineJobs API correctly', () => {
expect(Api.pipelineJobs).toHaveBeenCalledTimes(1); 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', () => { it('renders nothing', () => {
expect(wrapper.html()).toBe(''); expect(wrapper.html()).toBe('');
}); });
it('calls Flash correctly', () => { it('calls createFlash correctly', () => {
expect(Flash.mock.calls).toEqual([ expect(createFlash.mock.calls).toEqual([
[ [
{ {
message: SecurityReportsApp.i18n.apiError, message: SecurityReportsApp.i18n.apiError,
...@@ -159,4 +211,113 @@ describe('Security reports app', () => { ...@@ -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