Skip to content
Snippets Groups Projects
Commit 149d907c authored by Mark Florian's avatar Mark Florian
Browse files

Merge branch '217151-fuzzing-download' into 'master'

UI for "Coverage-guided fuzzing results download"

See merge request gitlab-org/gitlab!36676
parents 20c359d2 507ce12d
No related branches found
No related tags found
No related merge requests found
Showing
with 295 additions and 10 deletions
Loading
Loading
@@ -27,14 +27,17 @@ export default {
:filter="filter"
@setFilter="setFilter"
/>
<div class="ml-lg-auto p-2">
<strong>{{ s__('SecurityReports|Hide dismissed') }}</strong>
<gl-toggle-vuex
class="d-block mt-1 js-toggle"
store-module="filters"
state-property="hideDismissed"
set-action="setToggleValue"
/>
<div class="gl-display-flex ml-lg-auto p-2">
<slot name="buttons"></slot>
<div class="pl-md-6">
<strong>{{ s__('SecurityReports|Hide dismissed') }}</strong>
<gl-toggle-vuex
class="d-block mt-1 js-toggle"
store-module="filters"
state-property="hideDismissed"
set-action="setToggleValue"
/>
</div>
</div>
</div>
</div>
Loading
Loading
<script>
import { s__ } from '~/locale';
import { GlButton, GlNewDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
i18n: {
FUZZING_ARTIFACTS: s__('SecurityReports|Fuzzing artifacts'),
},
components: {
GlButton,
GlNewDropdown,
GlDropdownItem,
},
props: {
jobs: {
type: Array,
required: true,
},
projectId: {
type: Number,
required: true,
},
},
computed: {
hasDropdown() {
return this.jobs.length > 1;
},
},
methods: {
artifactDownloadUrl(job) {
return `/api/v4/projects/${this.projectId}/jobs/artifacts/${
job.ref
}/download?job=${encodeURIComponent(job.name)}`;
},
},
};
</script>
<template>
<div>
<strong>{{ s__('SecurityReports|Download Report') }}</strong>
<gl-new-dropdown
v-if="hasDropdown"
class="d-block mt-1"
:text="$options.i18n.FUZZING_ARTIFACTS"
category="secondary"
variant="info"
size="small"
>
<gl-dropdown-item v-for="job in jobs" :key="job.id" :href="artifactDownloadUrl(job)">{{
job.name
}}</gl-dropdown-item>
</gl-new-dropdown>
<gl-button
v-else
class="d-block mt-1"
category="secondary"
variant="info"
size="small"
:href="artifactDownloadUrl(jobs[0])"
>
{{ $options.i18n.FUZZING_ARTIFACTS }}
</gl-button>
</div>
</template>
Loading
Loading
@@ -77,6 +77,11 @@ export default {
required: false,
default: '',
},
pipelineJobsPath: {
type: String,
required: false,
default: '',
},
},
computed: {
emptyStateProps() {
Loading
Loading
@@ -93,9 +98,12 @@ export default {
},
created() {
this.setSourceBranch(this.sourceBranch);
this.setPipelineJobsPath(this.pipelineJobsPath);
this.setProjectId(this.projectId);
},
methods: {
...mapActions('vulnerabilities', ['setSourceBranch']),
...mapActions('pipelineJobs', ['setPipelineJobsPath', 'setProjectId']),
},
};
</script>
Loading
Loading
Loading
Loading
@@ -8,6 +8,7 @@ import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityChart from './vulnerability_chart.vue';
import VulnerabilityCountList from './vulnerability_count_list_vuex.vue';
import VulnerabilitySeverity from './vulnerability_severity.vue';
import FuzzingArtifactsDownload from './fuzzing_artifacts_download.vue';
import LoadingError from './loading_error.vue';
 
export default {
Loading
Loading
@@ -19,6 +20,7 @@ export default {
VulnerabilityChart,
VulnerabilityCountList,
VulnerabilitySeverity,
FuzzingArtifactsDownload,
LoadingError,
},
props: {
Loading
Loading
@@ -71,8 +73,10 @@ export default {
'isDismissingVulnerability',
'isCreatingMergeRequest',
]),
...mapState('pipelineJobs', ['projectId']),
...mapGetters('filters', ['activeFilters']),
...mapGetters('vulnerabilities', ['loadingVulnerabilitiesFailedWithRecognizedErrorCode']),
...mapGetters('pipelineJobs', ['hasFuzzingArtifacts', 'fuzzingJobsWithArtifact']),
canCreateIssue() {
const path = this.vulnerability.create_vulnerability_feedback_issue_path;
return Boolean(path);
Loading
Loading
@@ -122,6 +126,7 @@ export default {
this.fetchVulnerabilities({ ...this.activeFilters, page: this.pageInfo.page });
this.fetchVulnerabilitiesCount(this.activeFilters);
this.fetchVulnerabilitiesHistory(this.activeFilters);
this.fetchPipelineJobs();
},
methods: {
...mapActions('vulnerabilities', [
Loading
Loading
@@ -144,6 +149,7 @@ export default {
'undoDismiss',
'downloadPatch',
]),
...mapActions('pipelineJobs', ['fetchPipelineJobs']),
...mapActions('filters', ['lockFilter', 'setHideDismissedToggleInitialState']),
emitVulnerabilitiesCountChanged(count) {
this.$emit('vulnerabilitiesCountChanged', count);
Loading
Loading
@@ -163,7 +169,11 @@ export default {
<security-dashboard-layout>
<template #header>
<vulnerability-count-list v-if="shouldShowCountList" />
<filters />
<filters>
<template v-if="hasFuzzingArtifacts" #buttons>
<fuzzing-artifacts-download :jobs="fuzzingJobsWithArtifact" :project-id="projectId" />
</template>
</filters>
</template>
 
<security-dashboard-table>
Loading
Loading
Loading
Loading
@@ -24,6 +24,7 @@ export default () => {
emptyStateUnauthorizedSvgPath,
emptyStateForbiddenSvgPath,
projectFullPath,
pipelineJobsPath,
} = el.dataset;
 
const loadingErrorIllustrations = {
Loading
Loading
@@ -50,6 +51,7 @@ export default () => {
emptyStateSvgPath,
loadingErrorIllustrations,
projectFullPath,
pipelineJobsPath,
},
});
},
Loading
Loading
Loading
Loading
@@ -8,6 +8,7 @@ import filters from './modules/filters/index';
import vulnerabilities from './modules/vulnerabilities/index';
import vulnerableProjects from './modules/vulnerable_projects/index';
import unscannedProjects from './modules/unscanned_projects/index';
import pipelineJobs from './modules/pipeline_jobs/index';
 
Vue.use(Vuex);
 
Loading
Loading
@@ -21,6 +22,7 @@ export default ({ dashboardType = DASHBOARD_TYPES.PROJECT, plugins = [] } = {})
filters,
vulnerabilities,
unscannedProjects,
pipelineJobs,
},
plugins: [mediator, ...plugins],
});
import * as Sentry from '@sentry/browser';
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
export const setPipelineJobsPath = ({ commit }, path) => commit(types.SET_PIPELINE_JOBS_PATH, path);
export const setProjectId = ({ commit }, id) => commit(types.SET_PROJECT_ID, id);
export const fetchPipelineJobs = ({ commit, state }) => {
if (!state.pipelineJobsPath) {
return commit(types.RECEIVE_PIPELINE_JOBS_ERROR);
}
commit(types.REQUEST_PIPELINE_JOBS);
return axios({
method: 'GET',
url: state.pipelineJobsPath,
})
.then(response => {
const { data } = response;
commit(types.RECEIVE_PIPELINE_JOBS_SUCCESS, data);
})
.catch(error => {
Sentry.captureException(error);
commit(types.RECEIVE_PIPELINE_JOBS_ERROR);
});
};
/* eslint-disable import/prefer-default-export */
export const FUZZING_STAGE = 'fuzz';
import { FUZZING_STAGE } from './constants';
export const hasFuzzingArtifacts = state => {
return state.pipelineJobs.some(job => {
return job.stage === FUZZING_STAGE && job.artifacts.length > 0;
});
};
export const fuzzingJobsWithArtifact = state => {
return state.pipelineJobs.filter(job => {
return job.stage === FUZZING_STAGE && job.artifacts.length > 0;
});
};
import state from './state';
import mutations from './mutations';
import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
getters,
actions,
};
export const SET_PIPELINE_JOBS_PATH = 'SET_PIPELINE_JOBS_PATH';
export const SET_PROJECT_ID = 'SET_PROJECT_ID ';
export const REQUEST_PIPELINE_JOBS = 'REQUEST_PIPELINE_JOBS';
export const RECEIVE_PIPELINE_JOBS_SUCCESS = 'RECEIVE_PIPELINE_JOBS_SUCESS';
export const RECEIVE_PIPELINE_JOBS_ERROR = 'RECEIVE_PIPELINE_JOBS_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_PIPELINE_JOBS_PATH](state, payload) {
state.pipelineJobsPath = payload;
},
[types.SET_PROJECT_ID](state, payload) {
state.projectId = payload;
},
[types.REQUEST_PIPELINE_JOBS](state) {
state.isLoading = true;
},
[types.RECEIVE_PIPELINE_JOBS_SUCCESS](state, payload) {
state.isLoading = false;
state.pipelineJobs = payload;
},
[types.RECEIVE_PIPELINE_JOBS_ERROR](state) {
state.isLoading = false;
},
};
export default () => ({
projectId: undefined,
pipelineJobsPath: '',
isLoading: false,
pipelineJobs: [],
});
Loading
Loading
@@ -25,7 +25,8 @@ module EE
batch_lookup_report_artifact_for_file_type(:secret_detection) ||
batch_lookup_report_artifact_for_file_type(:dependency_scanning) ||
batch_lookup_report_artifact_for_file_type(:dast) ||
batch_lookup_report_artifact_for_file_type(:container_scanning)
batch_lookup_report_artifact_for_file_type(:container_scanning) ||
batch_lookup_report_artifact_for_file_type(:coverage_fuzzing)
end
 
def degradation_threshold(file_type)
Loading
Loading
Loading
Loading
@@ -15,6 +15,7 @@
pipeline_iid: pipeline.iid,
project_id: project.id,
source_branch: pipeline.source_ref,
pipeline_jobs_path: expose_path(api_v4_projects_pipelines_jobs_path(id: project.id, pipeline_id: pipeline.id)),
vulnerabilities_endpoint: vulnerabilities_endpoint_path,
vulnerability_exports_endpoint: vulnerability_exports_endpoint_path,
vulnerability_feedback_help_path: help_page_path('user/application_security/index'),
Loading
Loading
---
title: Add ability to download fuzzing artifacts from pipeline page
merge_request: 36676
author:
type: added
Loading
Loading
@@ -17,6 +17,9 @@ describe('Filter component', () => {
propsData: {
...props,
},
slots: {
buttons: '<div class="button-slot"></div>',
},
});
};
 
Loading
Loading
@@ -42,4 +45,11 @@ describe('Filter component', () => {
expect(wrapper.findAll('.js-toggle')).toHaveLength(1);
});
});
describe('buttons slot', () => {
it('should exist', () => {
createWrapper();
expect(wrapper.contains('.button-slot')).toBe(true);
});
});
});
import Vuex from 'vuex';
import FuzzingArtifactsDownload from 'ee/security_dashboard/components/fuzzing_artifacts_download.vue';
import createStore from 'ee/security_dashboard/store';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton, GlNewDropdown, GlDropdownItem } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Filter component', () => {
const projectId = 1;
const jobs = [{ ref: 'master', name: 'fuzz' }, { ref: 'master', name: 'fuzz 2' }];
let wrapper;
let store;
const createWrapper = (props = {}) => {
wrapper = shallowMount(FuzzingArtifactsDownload, {
localVue,
store,
propsData: {
projectId,
...props,
},
});
};
beforeEach(() => {
store = createStore();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('with one fuzzing job with artifacts', () => {
beforeEach(() => {
createWrapper({ jobs: [jobs[0]] });
});
it('should render a download button', () => {
expect(wrapper.find(GlButton).exists()).toBe(true);
expect(wrapper.find(GlNewDropdown).exists()).toBe(false);
});
it('should render with href set to the correct filepath', () => {
const href = `/api/v4/projects/${projectId}/jobs/artifacts/${
jobs[0].ref
}/download?job=${encodeURIComponent(jobs[0].name)}`;
expect(wrapper.find(GlButton).attributes('href')).toBe(href);
});
});
describe('with several fuzzing jobs with artifacts', () => {
beforeEach(() => {
createWrapper({ jobs });
});
it('should render a dropdown button with several items', () => {
expect(wrapper.find(GlButton).exists()).toBe(false);
expect(wrapper.find(GlNewDropdown).exists()).toBe(true);
expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
});
it('should render with href set to the correct filepath for every element', () => {
const wrapperArray = wrapper.findAll(GlDropdownItem);
wrapperArray.wrappers.forEach((_, index) => {
const href = `/api/v4/projects/${projectId}/jobs/artifacts/${
jobs[index].ref
}/download?job=${encodeURIComponent(jobs[index].name)}`;
expect(wrapperArray.at(index).attributes().href).toBe(href);
});
});
});
});
Loading
Loading
@@ -34,6 +34,13 @@ describe('Pipeline Security Dashboard component', () => {
setSourceBranch() {},
},
},
pipelineJobs: {
namespaced: true,
actions: {
setPipelineJobsPath() {},
setProjectId() {},
},
},
},
});
jest.spyOn(store, 'dispatch').mockImplementation();
Loading
Loading
@@ -74,6 +81,8 @@ describe('Pipeline Security Dashboard component', () => {
it('dispatches the expected actions', () => {
expect(store.dispatch.mock.calls).toEqual([
['vulnerabilities/setSourceBranch', sourceBranch],
['pipelineJobs/setPipelineJobsPath', ''],
['pipelineJobs/setProjectId', 5678],
]);
});
 
Loading
Loading
Loading
Loading
@@ -32,6 +32,7 @@ describe('Security Dashboard component', () => {
let mock;
let lockFilterSpy;
let setPipelineIdSpy;
let fetchPipelineJobsSpy;
let store;
 
const createComponent = props => {
Loading
Loading
@@ -43,6 +44,7 @@ describe('Security Dashboard component', () => {
methods: {
lockFilter: lockFilterSpy,
setPipelineId: setPipelineIdSpy,
fetchPipelineJobs: fetchPipelineJobsSpy,
},
propsData: {
dashboardDocumentation: '',
Loading
Loading
@@ -61,6 +63,7 @@ describe('Security Dashboard component', () => {
mock = new MockAdapter(axios);
lockFilterSpy = jest.fn();
setPipelineIdSpy = jest.fn();
fetchPipelineJobsSpy = jest.fn();
store = createStore();
});
 
Loading
Loading
@@ -104,6 +107,10 @@ describe('Security Dashboard component', () => {
expect(setPipelineIdSpy).toHaveBeenCalledWith(pipelineId);
});
 
it('fetchs the pipeline jobs', () => {
expect(fetchPipelineJobsSpy).toHaveBeenCalledWith();
});
describe('when the total number of vulnerabilities change', () => {
const newCount = 3;
 
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment