Commit 1f951eae authored by Denys Mishunov's avatar Denys Mishunov

Merge branch '347133-scanner-names-translations' into 'master'

Ensure all scanners are translated in alerts

See merge request gitlab-org/gitlab!76940
parents 6434c8c5 a24d1feb
...@@ -22,6 +22,7 @@ import configureSecretDetectionMutation from '../graphql/configure_secret_detect ...@@ -22,6 +22,7 @@ import configureSecretDetectionMutation from '../graphql/configure_secret_detect
/** /**
* Translations & helpPagePaths for Security Configuration Page * Translations & helpPagePaths for Security Configuration Page
* Make sure to add new scanner translations to the SCANNER_NAMES_MAP below.
*/ */
export const SAST_NAME = __('Static Application Security Testing (SAST)'); export const SAST_NAME = __('Static Application Security Testing (SAST)');
...@@ -138,6 +139,18 @@ export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath( ...@@ -138,6 +139,18 @@ export const LICENSE_COMPLIANCE_HELP_PATH = helpPagePath(
'user/compliance/license_compliance/index', 'user/compliance/license_compliance/index',
); );
export const SCANNER_NAMES_MAP = {
SAST: SAST_SHORT_NAME,
SAST_IAC: SAST_IAC_NAME,
DAST: DAST_SHORT_NAME,
API_FUZZING: API_FUZZING_NAME,
CONTAINER_SCANNING: CONTAINER_SCANNING_NAME,
CLUSTER_IMAGE_SCANNING: CLUSTER_IMAGE_SCANNING_NAME,
COVERAGE_FUZZING: COVERAGE_FUZZING_NAME,
SECRET_DETECTION: SECRET_DETECTION_NAME,
DEPENDENCY_SCANNING: DEPENDENCY_SCANNING_NAME,
};
export const securityFeatures = [ export const securityFeatures = [
{ {
name: SAST_NAME, name: SAST_NAME,
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => { export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => {
const featuresByType = features.reduce((acc, feature) => { const featuresByType = features.reduce((acc, feature) => {
...@@ -24,3 +25,13 @@ export const augmentFeatures = (securityFeatures, complianceFeatures, features = ...@@ -24,3 +25,13 @@ export const augmentFeatures = (securityFeatures, complianceFeatures, features =
augmentedComplianceFeatures: complianceFeatures.map((feature) => augmentFeature(feature)), augmentedComplianceFeatures: complianceFeatures.map((feature) => augmentFeature(feature)),
}; };
}; };
/**
* Converts a list of security scanner IDs (such as SAST_IAC) into a list of their translated
* names defined in the SCANNER_NAMES_MAP constant (eg. IaC Scanning).
*
* @param {String[]} scannerNames
* @returns {String[]}
*/
export const translateScannerNames = (scannerNames = []) =>
scannerNames.map((scannerName) => SCANNER_NAMES_MAP[scannerName] || scannerName);
...@@ -8,8 +8,8 @@ import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_ ...@@ -8,8 +8,8 @@ import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/project_
import { preparePageInfo } from 'ee/security_dashboard/helpers'; import { preparePageInfo } from 'ee/security_dashboard/helpers';
import { VULNERABILITIES_PER_PAGE } from 'ee/security_dashboard/store/constants'; import { VULNERABILITIES_PER_PAGE } from 'ee/security_dashboard/store/constants';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { translateScannerNames } from '~/security_configuration/utils';
import VulnerabilityList from '../shared/vulnerability_list.vue'; import VulnerabilityList from '../shared/vulnerability_list.vue';
import SecurityScannerAlert from './security_scanner_alert.vue'; import SecurityScannerAlert from './security_scanner_alert.vue';
...@@ -92,13 +92,11 @@ export default { ...@@ -92,13 +92,11 @@ export default {
}, },
update({ project = {} }) { update({ project = {} }) {
const { available = [], enabled = [], pipelineRun = [] } = project?.securityScanners || {}; const { available = [], enabled = [], pipelineRun = [] } = project?.securityScanners || {};
const translateScannerName = (scannerName) =>
this.$options.i18n[scannerName] || scannerName;
return { return {
available: available.map(translateScannerName), available: translateScannerNames(available),
enabled: enabled.map(translateScannerName), enabled: translateScannerNames(enabled),
pipelineRun: pipelineRun.map(translateScannerName), pipelineRun: translateScannerNames(pipelineRun),
}; };
}, },
}, },
...@@ -164,14 +162,6 @@ export default { ...@@ -164,14 +162,6 @@ export default {
}, },
}, },
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY: 'vulnerability_list_scanner_alert_dismissed', SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY: 'vulnerability_list_scanner_alert_dismissed',
i18n: {
API_FUZZING: __('API Fuzzing'),
CONTAINER_SCANNING: __('Container Scanning'),
CLUSTER_IMAGE_SCANNING: s__('ciReport|Cluster Image Scanning'),
COVERAGE_FUZZING: __('Coverage Fuzzing'),
SECRET_DETECTION: __('Secret Detection'),
DEPENDENCY_SCANNING: __('Dependency Scanning'),
},
}; };
</script> </script>
......
...@@ -4,14 +4,7 @@ import { difference } from 'lodash'; ...@@ -4,14 +4,7 @@ import { difference } from 'lodash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { import { translateScannerNames } from '~/security_configuration/utils';
API_FUZZING_NAME,
CLUSTER_IMAGE_SCANNING_NAME,
CONTAINER_SCANNING_NAME,
COVERAGE_FUZZING_NAME,
DEPENDENCY_SCANNING_NAME,
SECRET_DETECTION_NAME,
} from '~/security_configuration/components/constants';
import ReportNotConfiguredProject from '../shared/empty_states/report_not_configured_project.vue'; import ReportNotConfiguredProject from '../shared/empty_states/report_not_configured_project.vue';
import VulnerabilityReportTabs from '../shared/vulnerability_report/vulnerability_report_tabs.vue'; import VulnerabilityReportTabs from '../shared/vulnerability_report/vulnerability_report_tabs.vue';
import projectVulnerabilitiesQuery from '../../graphql/queries/project_vulnerabilities.query.graphql'; import projectVulnerabilitiesQuery from '../../graphql/queries/project_vulnerabilities.query.graphql';
...@@ -47,13 +40,11 @@ export default { ...@@ -47,13 +40,11 @@ export default {
}, },
update({ project = {} }) { update({ project = {} }) {
const { available = [], enabled = [], pipelineRun = [] } = project?.securityScanners || {}; const { available = [], enabled = [], pipelineRun = [] } = project?.securityScanners || {};
const translateScannerName = (scannerName) =>
this.$options.i18n[scannerName] || scannerName;
return { return {
available: available.map(translateScannerName), available: translateScannerNames(available),
enabled: enabled.map(translateScannerName), enabled: translateScannerNames(enabled),
pipelineRun: pipelineRun.map(translateScannerName), pipelineRun: translateScannerNames(pipelineRun),
}; };
}, },
}, },
...@@ -90,14 +81,6 @@ export default { ...@@ -90,14 +81,6 @@ export default {
projectVulnerabilitiesQuery, projectVulnerabilitiesQuery,
autoFixUserCalloutCookieName: 'auto_fix_user_callout_dismissed', autoFixUserCalloutCookieName: 'auto_fix_user_callout_dismissed',
SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY: 'vulnerability_list_scanner_alert_dismissed', SCANNER_ALERT_DISMISSED_LOCAL_STORAGE_KEY: 'vulnerability_list_scanner_alert_dismissed',
i18n: {
API_FUZZING: API_FUZZING_NAME,
CONTAINER_SCANNING: CONTAINER_SCANNING_NAME,
CLUSTER_IMAGE_SCANNING: CLUSTER_IMAGE_SCANNING_NAME,
COVERAGE_FUZZING: COVERAGE_FUZZING_NAME,
SECRET_DETECTION: SECRET_DETECTION_NAME,
DEPENDENCY_SCANNING: DEPENDENCY_SCANNING_NAME,
},
}; };
</script> </script>
......
...@@ -11,6 +11,7 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper'; ...@@ -11,6 +11,7 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
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 LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
import { generateVulnerabilities } from '../mock_data'; import { generateVulnerabilities } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
...@@ -286,6 +287,25 @@ describe('Vulnerabilities app component', () => { ...@@ -286,6 +287,25 @@ describe('Vulnerabilities app component', () => {
}); });
}); });
describe.each(Object.keys(SCANNER_NAMES_MAP))(
'When %s is available but not enabled',
(scanner) => {
const translatedScannerName = SCANNER_NAMES_MAP[scanner];
beforeEach(() => {
createWrapperForScannerAlerts({
securityScanners: { available: [scanner], enabled: [], pipelineRun: [] },
});
});
it(`passes the translated scanner's name to the alert (${translatedScannerName})`, () => {
expect(findSecurityScannerAlert().props('notEnabledScanners')[0]).toBe(
translatedScannerName,
);
});
},
);
describe('dismissal', () => { describe('dismissal', () => {
beforeEach(() => { beforeEach(() => {
return createWrapperForScannerAlerts({ return createWrapperForScannerAlerts({
......
...@@ -13,6 +13,7 @@ import AutoFixUserCallout from 'ee/security_dashboard/components/shared/auto_fix ...@@ -13,6 +13,7 @@ import AutoFixUserCallout from 'ee/security_dashboard/components/shared/auto_fix
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(VueApollo); localVue.use(VueApollo);
...@@ -141,6 +142,25 @@ describe('Project vulnerability report app component', () => { ...@@ -141,6 +142,25 @@ describe('Project vulnerability report app component', () => {
); );
}); });
}); });
describe.each(Object.keys(SCANNER_NAMES_MAP))(
'When %s is available but not enabled',
(scanner) => {
const translatedScannerName = SCANNER_NAMES_MAP[scanner];
beforeEach(() => {
createWrapper({
securityScanners: { available: [scanner], enabled: [], pipelineRun: [] },
});
});
it(`passes the translated scanner's name to the alert (${translatedScannerName})`, () => {
expect(findSecurityScannerAlert().props('notEnabledScanners')[0]).toBe(
translatedScannerName,
);
});
},
);
}); });
describe('auto fix user callout component', () => { describe('auto fix user callout component', () => {
......
import { augmentFeatures } from '~/security_configuration/utils'; import { augmentFeatures, translateScannerNames } from '~/security_configuration/utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
const mockSecurityFeatures = [
{ describe('augmentFeatures', () => {
name: 'SAST', const mockSecurityFeatures = [
type: 'SAST', {
}, name: 'SAST',
]; type: 'SAST',
const mockComplianceFeatures = [
{
name: 'LICENSE_COMPLIANCE',
type: 'LICENSE_COMPLIANCE',
},
];
const mockFeaturesWithSecondary = [
{
name: 'DAST',
type: 'DAST',
secondary: {
type: 'DAST PROFILES',
name: 'DAST PROFILES',
}, },
}, ];
];
const mockComplianceFeatures = [
const mockInvalidCustomFeature = [ {
{ name: 'LICENSE_COMPLIANCE',
foo: 'bar', type: 'LICENSE_COMPLIANCE',
}, },
]; ];
const mockValidCustomFeature = [ const mockFeaturesWithSecondary = [
{ {
name: 'SAST', name: 'DAST',
type: 'SAST', type: 'DAST',
customField: 'customvalue', secondary: {
}, type: 'DAST PROFILES',
]; name: 'DAST PROFILES',
},
const mockValidCustomFeatureSnakeCase = [ },
{ ];
name: 'SAST',
type: 'SAST', const mockInvalidCustomFeature = [
custom_field: 'customvalue', {
}, foo: 'bar',
]; },
];
const expectedOutputDefault = {
augmentedSecurityFeatures: mockSecurityFeatures, const mockValidCustomFeature = [
augmentedComplianceFeatures: mockComplianceFeatures, {
}; name: 'SAST',
type: 'SAST',
const expectedOutputSecondary = { customField: 'customvalue',
augmentedSecurityFeatures: mockSecurityFeatures, },
augmentedComplianceFeatures: mockFeaturesWithSecondary, ];
};
const mockValidCustomFeatureSnakeCase = [
const expectedOutputCustomFeature = { {
augmentedSecurityFeatures: mockValidCustomFeature, name: 'SAST',
augmentedComplianceFeatures: mockComplianceFeatures, type: 'SAST',
}; custom_field: 'customvalue',
},
describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => { ];
it('given an empty array', () => {
expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual( const expectedOutputDefault = {
expectedOutputDefault, augmentedSecurityFeatures: mockSecurityFeatures,
); augmentedComplianceFeatures: mockComplianceFeatures,
};
const expectedOutputSecondary = {
augmentedSecurityFeatures: mockSecurityFeatures,
augmentedComplianceFeatures: mockFeaturesWithSecondary,
};
const expectedOutputCustomFeature = {
augmentedSecurityFeatures: mockValidCustomFeature,
augmentedComplianceFeatures: mockComplianceFeatures,
};
describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => {
it('given an empty array', () => {
expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual(
expectedOutputDefault,
);
});
it('given an invalid populated array', () => {
expect(
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature),
).toEqual(expectedOutputDefault);
});
it('features have secondary key', () => {
expect(augmentFeatures(mockSecurityFeatures, mockFeaturesWithSecondary, [])).toEqual(
expectedOutputSecondary,
);
});
it('given a valid populated array', () => {
expect(
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
).toEqual(expectedOutputCustomFeature);
});
}); });
it('given an invalid populated array', () => { describe('returns an object with camelcased keys', () => {
expect( it('given a customfeature in snakecase', () => {
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature), expect(
).toEqual(expectedOutputDefault); augmentFeatures(
mockSecurityFeatures,
mockComplianceFeatures,
mockValidCustomFeatureSnakeCase,
),
).toEqual(expectedOutputCustomFeature);
});
}); });
});
it('features have secondary key', () => { describe('translateScannerNames', () => {
expect(augmentFeatures(mockSecurityFeatures, mockFeaturesWithSecondary, [])).toEqual( it.each(['', undefined, null, 1, 'UNKNOWN_SCANNER_KEY'])('returns %p as is', (key) => {
expectedOutputSecondary, expect(translateScannerNames([key])).toEqual([key]);
);
}); });
it('given a valid populated array', () => { it('returns an empty array if no input is provided', () => {
expect( expect(translateScannerNames([])).toEqual([]);
augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
).toEqual(expectedOutputCustomFeature);
}); });
});
describe('returns an object with camelcased keys', () => { it('returns translated scanner names', () => {
it('given a customfeature in snakecase', () => { expect(translateScannerNames(Object.keys(SCANNER_NAMES_MAP))).toEqual(
expect( Object.values(SCANNER_NAMES_MAP),
augmentFeatures( );
mockSecurityFeatures,
mockComplianceFeatures,
mockValidCustomFeatureSnakeCase,
),
).toEqual(expectedOutputCustomFeature);
}); });
}); });
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