diff --git a/ee/app/assets/javascripts/security_inventory/components/project_tool_coverage_indicator.vue b/ee/app/assets/javascripts/security_inventory/components/project_tool_coverage_indicator.vue new file mode 100644 index 0000000000000000000000000000000000000000..ca722ca433c317faf5ee732852515cd4b0733f16 --- /dev/null +++ b/ee/app/assets/javascripts/security_inventory/components/project_tool_coverage_indicator.vue @@ -0,0 +1,85 @@ +<script> +import { GlBadge } from '@gitlab/ui'; +import { SCANNERS } from '../constants'; + +export default { + name: 'ProjectToolCoverageIndicator', + components: { GlBadge }, + props: { + securityScanners: { + type: Object, + required: false, + default: () => ({ + enabled: [], + pipelineRun: [], + }), + }, + // TODO: switch to object with status enum + // scanners: { + // type: Object, + // required: false, + // default: () => ({ + // SAST: { + // status: 'SCANNER_DISABLED' + // }, + // DAST: { + // status: 'SCANNER_DISABLED' + // }, + // SAST_IAC: { + // status: 'SCANNER_DISABLED' + // }, + // SECRET_DETECTION: { + // status: 'SCANNER_DISABLED' + // }, + // DEPENDENCY_SCANNING: { + // status: 'SCANNER_DISABLED' + // }, + // CONTAINER_SCANNING: { + // status: 'SCANNER_DISABLED' + // }, + // }) + // } + }, + methods: { + isEnabled(scanner) { + return ( + this.securityScanners.enabled?.includes(scanner) && + this.securityScanners.pipelineRun?.includes(scanner) + ); + }, + isFailed(scanner) { + return ( + this.securityScanners.enabled?.includes(scanner) && + !this.securityScanners.pipelineRun?.includes(scanner) + ); + }, + scannerStyling(scanner) { + if (this.isEnabled(scanner)) { + // TODO: replace with this.scanners[scanner].status === 'SCANNER_ENABLED' + return { variant: 'success', class: 'gl-border-transparent' }; + } + if (this.isFailed(scanner)) { + // TODO: replace with this.scanners[scanner].status === 'SCANNER_FAILED' + return { variant: 'danger', class: 'gl-border-red-600' }; + } + // otherwise assume status is SCANNER_DISABLED + return { class: '!gl-bg-white gl-border-gray-200 gl-border-dashed' }; + }, + }, + SCANNERS, +}; +</script> + +<template> + <div class="gl-flex gl-flex-row gl-gap-2"> + <gl-badge + v-for="{ scanner, label } in $options.SCANNERS" + :key="scanner" + v-bind="scannerStyling(scanner)" + class="gl-border gl-w-8 gl-text-xs gl-font-bold" + :data-testid="`${scanner}-badge`" + > + {{ label }} + </gl-badge> + </div> +</template> diff --git a/ee/app/assets/javascripts/security_inventory/constants.js b/ee/app/assets/javascripts/security_inventory/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..96392bdc5b9e2fc1ee0b838695ad760c0c4eca1c --- /dev/null +++ b/ee/app/assets/javascripts/security_inventory/constants.js @@ -0,0 +1,35 @@ +import { s__ } from '~/locale'; + +const SAST_LABEL = s__('SecurityInventory|SAST'); +const DAST_LABEL = s__('SecurityInventory|DAST'); +const SAST_IAC_LABEL = s__('SecurityInventory|IaC'); +const SECRET_DETECTION_LABEL = s__('SecurityInventory|SD'); +const DEPENDENCY_SCANNING_LABEL = s__('SecurityInventory|DS'); +const CONTAINER_SCANNING_LABEL = s__('SecurityInventory|CS'); + +export const SCANNERS = [ + { + scanner: 'SAST', + label: SAST_LABEL, + }, + { + scanner: 'DAST', + label: DAST_LABEL, + }, + { + scanner: 'SAST_IAC', + label: SAST_IAC_LABEL, + }, + { + scanner: 'SECRET_DETECTION', + label: SECRET_DETECTION_LABEL, + }, + { + scanner: 'DEPENDENCY_SCANNING', + label: DEPENDENCY_SCANNING_LABEL, + }, + { + scanner: 'CONTAINER_SCANNING', + label: CONTAINER_SCANNING_LABEL, + }, +]; diff --git a/ee/spec/frontend/security_inventory/components/project_tool_coverage_indicator_spec.js b/ee/spec/frontend/security_inventory/components/project_tool_coverage_indicator_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..692e6a8ec4ff536fd54d83168c16f1be451858b6 --- /dev/null +++ b/ee/spec/frontend/security_inventory/components/project_tool_coverage_indicator_spec.js @@ -0,0 +1,35 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import ProjectToolCoverageIndicator from 'ee/security_inventory/components/project_tool_coverage_indicator.vue'; +import { SCANNERS } from 'ee/security_inventory/constants'; + +describe('ProjectToolCoverageIndicator', () => { + let wrapper; + + const createComponent = (propsData) => { + wrapper = shallowMountExtended(ProjectToolCoverageIndicator, { propsData }); + }; + + describe.each(SCANNERS)('$scanner badge', ({ scanner, label }) => { + it('shows success variant when enabled', () => { + createComponent({ securityScanners: { enabled: [scanner], pipelineRun: [scanner] } }); + + expect(wrapper.findByTestId(`${scanner}-badge`).props('variant')).toBe('success'); + expect(wrapper.findByTestId(`${scanner}-badge`).text()).toBe(label); + }); + + it('shows danger variant with border when failed', () => { + createComponent({ securityScanners: { enabled: [scanner] } }); + + expect(wrapper.findByTestId(`${scanner}-badge`).props('variant')).toBe('danger'); + expect(wrapper.findByTestId(`${scanner}-badge`).classes()).toContain('gl-border-red-600'); + expect(wrapper.findByTestId(`${scanner}-badge`).text()).toBe(label); + }); + + it('shows dashed outline for $scanner when disabled', () => { + createComponent({ enabledScanners: [] }); + + expect(wrapper.findByTestId(`${scanner}-badge`).classes()).toContain('gl-border-dashed'); + expect(wrapper.findByTestId(`${scanner}-badge`).text()).toBe(label); + }); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 4152ab759eeaa6f3edbe1cf478046ae6789849e5..37714d4574119c66259a8b89dd801f1d1b7aa270 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -52568,6 +52568,24 @@ msgstr "" msgid "SecurityExclusions|ex: spec/**/*.rb" msgstr "" +msgid "SecurityInventory|CS" +msgstr "" + +msgid "SecurityInventory|DAST" +msgstr "" + +msgid "SecurityInventory|DS" +msgstr "" + +msgid "SecurityInventory|IaC" +msgstr "" + +msgid "SecurityInventory|SAST" +msgstr "" + +msgid "SecurityInventory|SD" +msgstr "" + msgid "SecurityInventory|Security inventory" msgstr ""