Commit 93d74803 authored by Daniel Tian's avatar Daniel Tian Committed by Savas Vedova

Add page size selector to vulnerability report

Changelog: added
MR: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83604
EE: true
parent 6d7e0f06
---
name: vulnerability_report_page_size_selector
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82438
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356888
milestone: '14.10'
type: development
group: group::threat insights
default_enabled: false
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export const PAGE_SIZES = [20, 50, 100];
export default {
components: { GlDropdown, GlDropdownItem },
props: {
value: {
type: Number,
required: true,
},
},
methods: {
emitInput(pageSize) {
this.$emit('input', pageSize);
},
getPageSizeText(pageSize) {
return sprintf(s__('SecurityReports|Show %{pageSize} items'), { pageSize });
},
},
PAGE_SIZES,
};
</script>
<template>
<gl-dropdown :text="getPageSizeText(value)" right menu-class="gl-w-auto! gl-min-w-0">
<gl-dropdown-item
v-for="pageSize in $options.PAGE_SIZES"
:key="pageSize"
@click="emitInput(pageSize)"
>
<span class="gl-white-space-nowrap">{{ getPageSizeText(pageSize) }}</span>
</gl-dropdown-item>
</gl-dropdown>
</template>
...@@ -11,7 +11,6 @@ import { ...@@ -11,7 +11,6 @@ import {
import { Portal } from 'portal-vue'; import { Portal } from 'portal-vue';
import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/shared/empty_states/dashboard_has_no_vulnerabilities.vue'; import DashboardHasNoVulnerabilities from 'ee/security_dashboard/components/shared/empty_states/dashboard_has_no_vulnerabilities.vue';
import FiltersProducedNoResults from 'ee/security_dashboard/components/shared/empty_states/filters_produced_no_results.vue'; import FiltersProducedNoResults from 'ee/security_dashboard/components/shared/empty_states/filters_produced_no_results.vue';
import { VULNERABILITIES_PER_PAGE } from 'ee/security_dashboard/store/constants';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type'; import convertReportType from 'ee/vue_shared/security_reports/store/utils/convert_report_type';
import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier'; import getPrimaryIdentifier from 'ee/vue_shared/security_reports/store/utils/get_primary_identifier';
...@@ -81,6 +80,10 @@ export default { ...@@ -81,6 +80,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
pageSize: {
type: Number,
required: true,
},
sort: { sort: {
type: Object, type: Object,
required: false, required: false,
...@@ -254,7 +257,6 @@ export default { ...@@ -254,7 +257,6 @@ export default {
return VULNERABILITY_STATES[stateName] || stateName; return VULNERABILITY_STATES[stateName] || stateName;
}, },
}, },
VULNERABILITIES_PER_PAGE,
}; };
</script> </script>
...@@ -416,7 +418,7 @@ export default { ...@@ -416,7 +418,7 @@ export default {
<template #table-busy> <template #table-busy>
<gl-skeleton-loading <gl-skeleton-loading
v-for="n in $options.VULNERABILITIES_PER_PAGE" v-for="n in pageSize"
:key="n" :key="n"
class="gl-m-3 js-skeleton-loader" class="gl-m-3 js-skeleton-loader"
:lines="2" :lines="2"
......
...@@ -8,10 +8,13 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; ...@@ -8,10 +8,13 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { convertToSnakeCase } from '~/lib/utils/text_utility'; import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import VulnerabilityList from './vulnerability_list.vue'; import VulnerabilityList from './vulnerability_list.vue';
import { FIELDS } from './constants'; import { FIELDS } from './constants';
import PageSizeSelector from './page_size_selector.vue';
const PAGE_SIZE = 20; export const DEFAULT_PAGE_SIZE = 20;
const PAGE_SIZE_STORAGE_KEY = 'vulnerability_list_page_size';
const GRAPHQL_DATA_PATH = { const GRAPHQL_DATA_PATH = {
[DASHBOARD_TYPES.PROJECT]: 'project.vulnerabilities', [DASHBOARD_TYPES.PROJECT]: 'project.vulnerabilities',
...@@ -21,7 +24,14 @@ const GRAPHQL_DATA_PATH = { ...@@ -21,7 +24,14 @@ const GRAPHQL_DATA_PATH = {
}; };
export default { export default {
components: { GlLoadingIcon, GlIntersectionObserver, VulnerabilityList, GlKeysetPagination }, components: {
GlLoadingIcon,
GlIntersectionObserver,
VulnerabilityList,
GlKeysetPagination,
PageSizeSelector,
LocalStorageSync,
},
mixins: [glFeatureFlagsMixin()], mixins: [glFeatureFlagsMixin()],
inject: { inject: {
dashboardType: { dashboardType: {
...@@ -70,6 +80,7 @@ export default { ...@@ -70,6 +80,7 @@ export default {
pageInfo: {}, pageInfo: {},
// The "before" querystring value on page load. // The "before" querystring value on page load.
initialBefore: this.$route.query.before, initialBefore: this.$route.query.before,
pageSize: DEFAULT_PAGE_SIZE,
}; };
}, },
apollo: { apollo: {
...@@ -88,8 +99,8 @@ export default { ...@@ -88,8 +99,8 @@ export default {
// If we're using "after" we need to use "first", and if we're using "before" we need to // If we're using "after" we need to use "first", and if we're using "before" we need to
// use "last". See this comment for more info: // use "last". See this comment for more info:
// https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79834#note_831878506 // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79834#note_831878506
first: this.before ? null : PAGE_SIZE, first: this.before ? null : this.pageSize,
last: this.before ? PAGE_SIZE : null, last: this.before ? this.pageSize : null,
before: this.before, before: this.before,
after: this.after, after: this.after,
...this.filters, ...this.filters,
...@@ -164,6 +175,9 @@ export default { ...@@ -164,6 +175,9 @@ export default {
shouldUsePagination() { shouldUsePagination() {
return Boolean(this.glFeatures.vulnerabilityReportPagination); return Boolean(this.glFeatures.vulnerabilityReportPagination);
}, },
shouldShowPageSizeSelector() {
return Boolean(this.glFeatures.vulnerabilityReportPageSizeSelector);
},
}, },
watch: { watch: {
filters(newFilters, oldFilters) { filters(newFilters, oldFilters) {
...@@ -217,6 +231,7 @@ export default { ...@@ -217,6 +231,7 @@ export default {
return get(data, GRAPHQL_DATA_PATH[this.dashboardType]); return get(data, GRAPHQL_DATA_PATH[this.dashboardType]);
}, },
}, },
PAGE_SIZE_STORAGE_KEY,
}; };
</script> </script>
...@@ -227,12 +242,13 @@ export default { ...@@ -227,12 +242,13 @@ export default {
:vulnerabilities="vulnerabilities" :vulnerabilities="vulnerabilities"
:fields="fields" :fields="fields"
:sort.sync="sort" :sort.sync="sort"
:page-size="pageSize"
:should-show-project-namespace="showProjectNamespace" :should-show-project-namespace="showProjectNamespace"
:portal-name="portalName" :portal-name="portalName"
@vulnerability-clicked="$emit('vulnerability-clicked', $event)" @vulnerability-clicked="$emit('vulnerability-clicked', $event)"
/> />
<div v-if="shouldUsePagination" class="gl-text-center gl-mt-6"> <div v-if="shouldUsePagination" class="gl-text-center gl-mt-6 gl-relative">
<gl-keyset-pagination <gl-keyset-pagination
:has-previous-page="pageInfo.hasPreviousPage" :has-previous-page="pageInfo.hasPreviousPage"
:has-next-page="pageInfo.hasNextPage" :has-next-page="pageInfo.hasNextPage"
...@@ -242,6 +258,15 @@ export default { ...@@ -242,6 +258,15 @@ export default {
@next="getNextPage" @next="getNextPage"
@prev="getPrevPage" @prev="getPrevPage"
/> />
<local-storage-sync
v-if="shouldShowPageSizeSelector"
v-model="pageSize"
as-json
:storage-key="$options.PAGE_SIZE_STORAGE_KEY"
>
<page-size-selector v-model="pageSize" class="gl-absolute gl-right-0" />
</local-storage-sync>
</div> </div>
<gl-intersection-observer v-else-if="pageInfo.hasNextPage" @appear="fetchNextPage"> <gl-intersection-observer v-else-if="pageInfo.hasNextPage" @appear="fetchNextPage">
......
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const VULNERABILITIES_PER_PAGE = 20;
export const DETECTION_METHODS = [ export const DETECTION_METHODS = [
s__('Vulnerability|GitLab Security Report'), s__('Vulnerability|GitLab Security Report'),
s__('Vulnerability|External Security Report'), s__('Vulnerability|External Security Report'),
......
...@@ -13,6 +13,7 @@ module EE ...@@ -13,6 +13,7 @@ module EE
push_frontend_feature_flag(:graphql_code_quality_full_report, project, type: :development, default_enabled: :yaml) push_frontend_feature_flag(:graphql_code_quality_full_report, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml) push_frontend_feature_flag(:secure_vulnerability_training, project, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_page_size_selector, default_enabled: :yaml)
end end
feature_category :license_compliance, [:licenses] feature_category :license_compliance, [:licenses]
......
...@@ -8,6 +8,7 @@ module Groups ...@@ -8,6 +8,7 @@ module Groups
before_action do before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_page_size_selector, default_enabled: :yaml)
end end
feature_category :vulnerability_management feature_category :vulnerability_management
......
...@@ -10,6 +10,7 @@ module Projects ...@@ -10,6 +10,7 @@ module Projects
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:secure_vulnerability_training, @project, default_enabled: :yaml) push_frontend_feature_flag(:secure_vulnerability_training, @project, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_page_size_selector, default_enabled: :yaml)
push_frontend_feature_flag(:new_vulnerability_form, @project, default_enabled: :yaml) push_frontend_feature_flag(:new_vulnerability_form, @project, default_enabled: :yaml)
end end
......
...@@ -7,6 +7,7 @@ module Security ...@@ -7,6 +7,7 @@ module Security
before_action do before_action do
push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_management_survey, type: :ops, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml) push_frontend_feature_flag(:vulnerability_report_pagination, current_user, default_enabled: :yaml)
push_frontend_feature_flag(:vulnerability_report_page_size_selector, default_enabled: :yaml)
end end
end end
end end
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PageSizeSelector, {
PAGE_SIZES,
} from 'ee/security_dashboard/components/shared/vulnerability_report/page_size_selector.vue';
describe('Page size selector component', () => {
let wrapper;
const createWrapper = ({ pageSize = 20 } = {}) => {
wrapper = shallowMount(PageSizeSelector, {
propsData: { value: pageSize },
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
afterEach(() => {
wrapper.destroy();
});
it.each(PAGE_SIZES)('shows expected text in the dropdown button for page size %s', (pageSize) => {
createWrapper({ pageSize });
expect(findDropdown().props('text')).toBe(`Show ${pageSize} items`);
});
it('shows the expected dropdown items', () => {
createWrapper();
PAGE_SIZES.forEach((pageSize, index) => {
expect(findDropdownItems().at(index).text()).toBe(`Show ${pageSize} items`);
});
});
it('will emit the new page size when a dropdown item is clicked', () => {
createWrapper();
findDropdownItems().wrappers.forEach((itemWrapper, index) => {
itemWrapper.vm.$emit('click');
expect(wrapper.emitted('input')[index][0]).toBe(PAGE_SIZES[index]);
});
});
});
...@@ -2,15 +2,19 @@ import Vue, { nextTick } from 'vue'; ...@@ -2,15 +2,19 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GlIntersectionObserver, GlKeysetPagination } from '@gitlab/ui'; import { GlIntersectionObserver, GlKeysetPagination } from '@gitlab/ui';
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import VulnerabilityListGraphql from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue'; import { shallowMount } from '@vue/test-utils';
import VulnerabilityListGraphql, {
DEFAULT_PAGE_SIZE,
} from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list_graphql.vue';
import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue'; import VulnerabilityList from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_list.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import PageSizeSelector from 'ee/security_dashboard/components/shared/vulnerability_report/page_size_selector.vue';
import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql'; import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { FIELDS } from 'ee/security_dashboard/components/shared/vulnerability_report/constants'; import { FIELDS } from 'ee/security_dashboard/components/shared/vulnerability_report/constants';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants'; import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { vulnerabilities } from '../../mock_data'; import { vulnerabilities } from '../../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -57,10 +61,11 @@ describe('Vulnerability list GraphQL component', () => { ...@@ -57,10 +61,11 @@ describe('Vulnerability list GraphQL component', () => {
showProjectNamespace = false, showProjectNamespace = false,
hasJiraVulnerabilitiesIntegrationEnabled = false, hasJiraVulnerabilitiesIntegrationEnabled = false,
vulnerabilityReportPagination = false, vulnerabilityReportPagination = false,
vulnerabilityReportPageSizeSelector = false,
filters = {}, filters = {},
fields = [], fields = [],
} = {}) => { } = {}) => {
wrapper = shallowMountExtended(VulnerabilityListGraphql, { wrapper = shallowMount(VulnerabilityListGraphql, {
router, router,
apolloProvider: createMockApollo([[vulnerabilitiesQuery, vulnerabilitiesHandler]]), apolloProvider: createMockApollo([[vulnerabilitiesQuery, vulnerabilitiesHandler]]),
provide: { provide: {
...@@ -68,7 +73,7 @@ describe('Vulnerability list GraphQL component', () => { ...@@ -68,7 +73,7 @@ describe('Vulnerability list GraphQL component', () => {
dashboardType: DASHBOARD_TYPES.GROUP, dashboardType: DASHBOARD_TYPES.GROUP,
canViewFalsePositive, canViewFalsePositive,
hasJiraVulnerabilitiesIntegrationEnabled, hasJiraVulnerabilitiesIntegrationEnabled,
glFeatures: { vulnerabilityReportPagination }, glFeatures: { vulnerabilityReportPagination, vulnerabilityReportPageSizeSelector },
}, },
propsData: { propsData: {
query: vulnerabilitiesQuery, query: vulnerabilitiesQuery,
...@@ -83,6 +88,8 @@ describe('Vulnerability list GraphQL component', () => { ...@@ -83,6 +88,8 @@ describe('Vulnerability list GraphQL component', () => {
const findVulnerabilityList = () => wrapper.findComponent(VulnerabilityList); const findVulnerabilityList = () => wrapper.findComponent(VulnerabilityList);
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver); const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
const findPagination = () => wrapper.findComponent(GlKeysetPagination); const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findPageSizeSelector = () => wrapper.findComponent(PageSizeSelector);
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -318,6 +325,62 @@ describe('Vulnerability list GraphQL component', () => { ...@@ -318,6 +325,62 @@ describe('Vulnerability list GraphQL component', () => {
); );
}); });
describe('page size selector', () => {
const expectPageSizeUsed = (pageSize) => {
expect(vulnerabilitiesRequestHandler).toHaveBeenCalledWith(
expect.objectContaining({ first: pageSize }),
);
// Vulnerability list needs the page size to show the correct number of skeleton loaders.
expect(findVulnerabilityList().props('pageSize')).toBe(pageSize);
};
it('is not shown if the pagination feature flag is off', () => {
createWrapper();
expect(findLocalStorageSync().exists()).toBe(false);
expect(findPageSizeSelector().exists()).toBe(false);
});
it('is not shown if the pagination feature flag is on but the page size selector feature flag is off', () => {
createWrapper({ vulnerabilityReportPagination: true });
expect(findLocalStorageSync().exists()).toBe(false);
expect(findPageSizeSelector().exists()).toBe(false);
});
describe('both feature flags enabled', () => {
beforeEach(() => {
createWrapper({
vulnerabilityReportPagination: true,
vulnerabilityReportPageSizeSelector: true,
});
});
it('uses the default page size if page size selector was not changed', () => {
expectPageSizeUsed(DEFAULT_PAGE_SIZE);
});
it('uses the page size selected by the page size selector', async () => {
const pageSize = 50;
findPageSizeSelector().vm.$emit('input', pageSize);
await nextTick();
expectPageSizeUsed(pageSize);
});
it('sets up the local storage sync correctly', async () => {
const pageSize = 123;
findPageSizeSelector().vm.$emit('input', pageSize);
await nextTick();
expect(findLocalStorageSync().props()).toMatchObject({
asJson: true,
value: pageSize,
});
});
});
});
describe('intersection observer', () => { describe('intersection observer', () => {
it('is not shown if the pagination feature flag is on', () => { it('is not shown if the pagination feature flag is on', () => {
createWrapper({ vulnerabilityReportPagination: true }); createWrapper({ vulnerabilityReportPagination: true });
......
...@@ -35,6 +35,7 @@ describe('Vulnerability list component', () => { ...@@ -35,6 +35,7 @@ describe('Vulnerability list component', () => {
vulnerabilities: [], vulnerabilities: [],
fields: FIELD_PRESETS.DEVELOPMENT, fields: FIELD_PRESETS.DEVELOPMENT,
portalName, portalName,
pageSize: 20,
...props, ...props,
}, },
stubs: { stubs: {
...@@ -83,6 +84,7 @@ describe('Vulnerability list component', () => { ...@@ -83,6 +84,7 @@ describe('Vulnerability list component', () => {
const findVendorNames = () => wrapper.findByTestId('vulnerability-vendor'); const findVendorNames = () => wrapper.findByTestId('vulnerability-vendor');
const findCheckAllCheckbox = () => wrapper.findByTestId('vulnerability-checkbox-all'); const findCheckAllCheckbox = () => wrapper.findByTestId('vulnerability-checkbox-all');
const findAllRowCheckboxes = () => wrapper.findAllByTestId('vulnerability-checkbox'); const findAllRowCheckboxes = () => wrapper.findAllByTestId('vulnerability-checkbox');
const findSkeletonLoading = () => wrapper.findAllComponents(GlSkeletonLoading);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -562,7 +564,7 @@ describe('Vulnerability list component', () => { ...@@ -562,7 +564,7 @@ describe('Vulnerability list component', () => {
createWrapper({ props: { isLoading, vulnerabilities } }); createWrapper({ props: { isLoading, vulnerabilities } });
expect(findCell('status').exists()).toEqual(!isLoading); expect(findCell('status').exists()).toEqual(!isLoading);
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toEqual(isLoading); expect(findSkeletonLoading().exists()).toBe(isLoading);
}); });
}); });
...@@ -690,6 +692,15 @@ describe('Vulnerability list component', () => { ...@@ -690,6 +692,15 @@ describe('Vulnerability list component', () => {
}); });
}); });
describe('pageSize prop', () => {
it('shows the same number of skeleton loaders as the pageSize prop', () => {
const pageSize = 17;
createWrapper({ props: { pageSize, isLoading: true } });
expect(findSkeletonLoading()).toHaveLength(pageSize);
});
});
describe('operational vulnerabilities', () => { describe('operational vulnerabilities', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ createWrapper({
......
...@@ -33603,6 +33603,9 @@ msgstr "" ...@@ -33603,6 +33603,9 @@ msgstr ""
msgid "SecurityReports|Severity" msgid "SecurityReports|Severity"
msgstr "" msgstr ""
msgid "SecurityReports|Show %{pageSize} items"
msgstr ""
msgid "SecurityReports|Sometimes a scanner can't determine a finding's severity. Those findings may still be a potential source of risk though. Please review these manually." msgid "SecurityReports|Sometimes a scanner can't determine a finding's severity. Those findings may still be a potential source of risk though. Please review these manually."
msgstr "" msgstr ""
......
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