diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 016e3c4546b965c9dc46ea076bfd2b105e59bf4c..0733e9ab1b1826d7527cedefaf66e453cc5301b7 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -21,12 +21,23 @@ export const OPERATOR_AFTER = '≥'; export const OPERATOR_AFTER_TEXT = __('on or after'); export const OPERATOR_BEFORE = '<'; export const OPERATOR_BEFORE_TEXT = __('before'); +export const OPERATOR_BIGGER_THAN_OR_EQUAL = '>='; +export const OPERATOR_BIGGER_THAN_OR_EQUAL_TEXT = __('greater than or equal'); +export const OPERATOR_LESS_THAN_OR_EQUAL = '<='; +export const OPERATOR_LESS_THAN_OR_EQUAL_TEXT = __('less than or equal'); export const OPERATORS_IS = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }]; export const OPERATORS_NOT = [{ value: OPERATOR_NOT, description: OPERATOR_NOT_TEXT }]; export const OPERATORS_OR = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }]; export const OPERATORS_AFTER = [{ value: OPERATOR_AFTER, description: OPERATOR_AFTER_TEXT }]; export const OPERATORS_BEFORE = [{ value: OPERATOR_BEFORE, description: OPERATOR_BEFORE_TEXT }]; +export const OPERATORS_BIGGER_THAN_OR_EQUAL = [ + { value: OPERATOR_BIGGER_THAN_OR_EQUAL, description: OPERATOR_BIGGER_THAN_OR_EQUAL_TEXT }, +]; +export const OPERATORS_LESS_THAN_OR_EQUAL = [ + { value: OPERATOR_LESS_THAN_OR_EQUAL, description: OPERATOR_LESS_THAN_OR_EQUAL_TEXT }, +]; + export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT]; export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR]; export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE]; diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js index 81e179630ba7cb45e976521b0b263fdcae01c366..9ba865fe068a9b8255fd84d9ce9e5f0a8b10423c 100644 --- a/app/assets/javascripts/vue_shared/constants.js +++ b/app/assets/javascripts/vue_shared/constants.js @@ -116,6 +116,7 @@ export const SEVERITY_LEVEL_UNKNOWN = 'unknown'; export const SEVERITY_LEVEL_MEDIUM = 'medium'; export const SEVERITY_LEVEL_LOW = 'low'; export const SEVERITY_LEVEL_INFO = 'info'; +export const SEVERITY_LEVEL_ANY = 'any'; export const SEVERITY_LEVELS = { [SEVERITY_LEVEL_CRITICAL]: s__('severity|Critical'), @@ -125,3 +126,7 @@ export const SEVERITY_LEVELS = { [SEVERITY_LEVEL_INFO]: s__('severity|Info'), [SEVERITY_LEVEL_UNKNOWN]: s__('severity|Unknown'), }; + +export const SEVERITY_LEVELS_ANY = { + [SEVERITY_LEVEL_ANY]: s__('severity|Any'), +}; diff --git a/ee/app/assets/javascripts/security_inventory/components/inventory_dashboard_filtered_search_bar.vue b/ee/app/assets/javascripts/security_inventory/components/inventory_dashboard_filtered_search_bar.vue index 8d5891905c29a8c37931d45104a3e2e744428c82..380e55a73529c098aa069ef3c3af3f062683e329 100644 --- a/ee/app/assets/javascripts/security_inventory/components/inventory_dashboard_filtered_search_bar.vue +++ b/ee/app/assets/javascripts/security_inventory/components/inventory_dashboard_filtered_search_bar.vue @@ -1,13 +1,63 @@ diff --git a/ee/app/assets/javascripts/security_inventory/components/tokens/custom_title_option.vue b/ee/app/assets/javascripts/security_inventory/components/tokens/custom_title_option.vue new file mode 100644 index 0000000000000000000000000000000000000000..80ac13054fd998feef2cbbafad406cae2d660279 --- /dev/null +++ b/ee/app/assets/javascripts/security_inventory/components/tokens/custom_title_option.vue @@ -0,0 +1,19 @@ + + + diff --git a/ee/app/assets/javascripts/security_inventory/components/tokens/header_options.vue b/ee/app/assets/javascripts/security_inventory/components/tokens/header_options.vue new file mode 100644 index 0000000000000000000000000000000000000000..1cfa3c6b87da83f41c196d20d48b641a48e9be80 --- /dev/null +++ b/ee/app/assets/javascripts/security_inventory/components/tokens/header_options.vue @@ -0,0 +1,20 @@ + + + diff --git a/ee/app/assets/javascripts/security_inventory/constants.js b/ee/app/assets/javascripts/security_inventory/constants.js index 10d1dc3b774a5471e1ead38af4da3013c97e5097..06237f1cb01bc6953bf78f4e9b6d91969692140e 100644 --- a/ee/app/assets/javascripts/security_inventory/constants.js +++ b/ee/app/assets/javascripts/security_inventory/constants.js @@ -1,15 +1,15 @@ import { s__ } from '~/locale'; import { CRITICAL, HIGH, MEDIUM, LOW } from 'ee/vulnerabilities/constants'; -const DEPENDENCY_SCANNING_KEY = 'DEPENDENCY_SCANNING'; -const SAST_KEY = 'SAST'; +export const DEPENDENCY_SCANNING_KEY = 'DEPENDENCY_SCANNING'; +export const SAST_KEY = 'SAST'; export const SAST_ADVANCED_KEY = 'SAST_ADVANCED'; -const SECRET_DETECTION_KEY = 'SECRET_DETECTION'; +export const SECRET_DETECTION_KEY = 'SECRET_DETECTION'; export const SECRET_PUSH_PROTECTION_KEY = 'SECRET_PUSH_PROTECTION'; -const CONTAINER_SCANNING_KEY = 'CONTAINER_SCANNING'; +export const CONTAINER_SCANNING_KEY = 'CONTAINER_SCANNING'; export const CONTAINER_SCANNING_FOR_REGISTRY_KEY = 'CONTAINER_SCANNING_FOR_REGISTRY'; -const DAST_KEY = 'DAST'; -const SAST_IAC_KEY = 'SAST_IAC'; +export const DAST_KEY = 'DAST'; +export const SAST_IAC_KEY = 'SAST_IAC'; export const SIDEBAR_WIDTH_INITIAL = 300; export const SIDEBAR_WIDTH_MINIMUM = 200; @@ -82,6 +82,20 @@ export const SCANNER_POPOVER_LABELS = { [CONTAINER_SCANNING_FOR_REGISTRY_KEY]: s__('SecurityInventory|Container scanning for registry'), }; +export const SCANNER_FILTER_LABELS = { + [DEPENDENCY_SCANNING_KEY]: s__('SecurityInventory|Dependency scanning (DS)'), + [SAST_KEY]: s__('SecurityInventory|Basic SAST (SAST)'), + [SAST_ADVANCED_KEY]: s__('SecurityInventory|Advanced SAST (SAST)'), + [SECRET_DETECTION_KEY]: s__('SecurityInventory|Pipeline secret detection (SD)'), + [SECRET_PUSH_PROTECTION_KEY]: s__('SecurityInventory|Secret push protection (SD)'), + [CONTAINER_SCANNING_KEY]: s__('SecurityInventory|Container scanning (CS)'), + [CONTAINER_SCANNING_FOR_REGISTRY_KEY]: s__( + 'SecurityInventory|Container scanning for registry (CS)', + ), + [DAST_KEY]: s__('SecurityInventory|Dynamic Application Security Testing (DAST)'), + [SAST_IAC_KEY]: s__('SecurityInventory|Infrastructure as Code (IaC)'), +}; + export const SEVERITY_SEGMENTS = [CRITICAL, HIGH, MEDIUM, LOW]; export const SEVERITY_BACKGROUND_COLORS = { @@ -100,3 +114,7 @@ export const PROJECT_SECURITY_CONFIGURATION_PATH = '/-/security/configuration'; export const PROJECT_VULNERABILITY_REPORT_PATH = '/-/security/vulnerability_report'; export const GROUP_VULNERABILITY_REPORT_PATH = '/-/security/vulnerabilities'; export const PROJECT_PIPELINE_JOB_PATH = '/-/jobs'; + +export const TOOL_ENABLED = 'Enabled'; +export const TOOL_NOT_ENABLED = 'Not enabled'; +export const TOOL_FAILED = 'Failed'; diff --git a/ee/app/controllers/groups/security/inventory_controller.rb b/ee/app/controllers/groups/security/inventory_controller.rb index e0ca8684c6d32050b7e3128310cecb247258c848..4d0a679c48894fb9d2f1efc863e5f57671ad085d 100644 --- a/ee/app/controllers/groups/security/inventory_controller.rb +++ b/ee/app/controllers/groups/security/inventory_controller.rb @@ -9,6 +9,7 @@ class InventoryController < Groups::ApplicationController before_action do push_frontend_feature_flag(:security_inventory_dashboard, @group.root_ancestor) + push_frontend_feature_flag(:security_inventory_filtering, @group.root_ancestor) end feature_category :security_asset_inventories diff --git a/ee/config/feature_flags/beta/security_inventory_filtering.yml b/ee/config/feature_flags/beta/security_inventory_filtering.yml new file mode 100644 index 0000000000000000000000000000000000000000..50821a35896189b83e05996c0cb2b2744f47d076 --- /dev/null +++ b/ee/config/feature_flags/beta/security_inventory_filtering.yml @@ -0,0 +1,10 @@ +--- +name: security_inventory_filtering +description: +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/16484 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/196057 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/552224 +milestone: '18.3' +group: group::security platform management +type: beta +default_enabled: false diff --git a/ee/spec/frontend/security_inventory/components/inventory_dashboard_filtered_search_bar_spec.js b/ee/spec/frontend/security_inventory/components/inventory_dashboard_filtered_search_bar_spec.js index afaf9eb1aa8b1295bb1bf4d8d5d22c948ec24c05..0d16d805ebb8081e3aca507bd8e76620477496fe 100644 --- a/ee/spec/frontend/security_inventory/components/inventory_dashboard_filtered_search_bar_spec.js +++ b/ee/spec/frontend/security_inventory/components/inventory_dashboard_filtered_search_bar_spec.js @@ -1,8 +1,16 @@ import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; +import { GlFilteredSearchToken } from '@gitlab/ui'; import InventoryDashboardFilteredSearchBar from 'ee/security_inventory/components/inventory_dashboard_filtered_search_bar.vue'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import { queryToObject } from '~/lib/utils/url_utility'; +import HeaderOptions from 'ee/security_inventory/components/tokens/header_options.vue'; +import customTitleOption from 'ee/security_inventory/components/tokens/custom_title_option.vue'; +import { + OPERATORS_BIGGER_THAN_OR_EQUAL, + OPERATORS_IS, + OPERATORS_LESS_THAN_OR_EQUAL, +} from '~/vue_shared/components/filtered_search_bar/constants'; jest.mock('~/lib/utils/url_utility', () => ({ queryToObject: jest.fn().mockReturnValue({}), @@ -12,12 +20,15 @@ jest.mock('~/lib/utils/url_utility', () => ({ describe('InventoryDashboardFilteredSearchBar', () => { let wrapper; - const createComponent = ({ props = {} } = {}) => { + const createComponent = ({ props = {}, securityInventoryFiltering = false } = {}) => { wrapper = shallowMount(InventoryDashboardFilteredSearchBar, { propsData: { namespace: 'group1', ...props, }, + provide: { + glFeatures: { securityInventoryFiltering }, + }, }); }; @@ -140,4 +151,49 @@ describe('InventoryDashboardFilteredSearchBar', () => { }); }); }); + + describe('when `securityInventoryFiltering` is disabled', () => { + it('renders empty array tokens', () => { + expect(findFilteredSearch().props('tokens')).toMatchObject([]); + }); + }); + + describe('when `securityInventoryFiltering` is enabled', () => { + it('renders all tokens', async () => { + createComponent({ securityInventoryFiltering: true }); + findFilteredSearch().vm.$emit('onInput', [ + { + type: 'filtered-search-term', + value: { data: '' }, + }, + ]); + await nextTick(); + expect(findFilteredSearch().props('tokens')).toMatchObject( + expect.arrayContaining([ + expect.objectContaining( + { + type: 'vulnerability-count-', + title: 'Vulnerability count', + customKeyToken: { component: HeaderOptions }, + }, + { + type: 'vulnerability-count-critical', + title: 'Vulnerability count critical', + token: GlFilteredSearchToken, + unique: true, + optionComponent: customTitleOption, + operators: [ + ...OPERATORS_LESS_THAN_OR_EQUAL, + ...OPERATORS_IS, + ...OPERATORS_BIGGER_THAN_OR_EQUAL, + ], + customKeyToken: { + title: 'critical', + }, + }, + ), + ]), + ); + }); + }); }); diff --git a/ee/spec/frontend/security_inventory/components/tokens/custom_title_option_spec.js b/ee/spec/frontend/security_inventory/components/tokens/custom_title_option_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..34faeac1a5e46ac77977974bc5eb1e62251be14d --- /dev/null +++ b/ee/spec/frontend/security_inventory/components/tokens/custom_title_option_spec.js @@ -0,0 +1,53 @@ +import { shallowMount } from '@vue/test-utils'; +import CustomTitleOption from 'ee/security_inventory/components/tokens/custom_title_option.vue'; + +describe('CustomTitleOption', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(CustomTitleOption, { + propsData: { + option: { title: 'Original Title', customKeyToken: { title: 'Custom Title' } }, + ...props, + }, + }); + }; + + describe('component rendering', () => { + it('renders title when available', () => { + const option = { + title: 'Original Title', + }; + createComponent({ option }); + expect(wrapper.text()).toBe('Original Title'); + }); + + it('renders customKeyToken title when available', () => { + createComponent(); + expect(wrapper.text()).toBe('Custom Title'); + }); + + it('renders customKeyToken when title not exists', () => { + const option = { + customKeyToken: { title: 'Custom Title' }, + }; + createComponent({ option }); + expect(wrapper.text()).toBe('Custom Title'); + }); + + it('renders empty string when no title is provided', () => { + const option = {}; + createComponent({ option }); + expect(wrapper.text()).toBe(''); + }); + + it('renders customKeyToken title when customKeyToken exists but title is undefined', () => { + const option = { + title: 'Original Title', + customKeyToken: { title: undefined }, + }; + createComponent({ option }); + expect(wrapper.text()).toBe('Original Title'); + }); + }); +}); diff --git a/ee/spec/frontend/security_inventory/components/tokens/header_options_spec.js b/ee/spec/frontend/security_inventory/components/tokens/header_options_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..072f288fb71621e51c8d271744ae879de9a0d6b4 --- /dev/null +++ b/ee/spec/frontend/security_inventory/components/tokens/header_options_spec.js @@ -0,0 +1,42 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlDropdownSectionHeader } from '@gitlab/ui'; +import HeaderOptions from 'ee/security_inventory/components/tokens/header_options.vue'; + +describe('HeaderOptions', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(HeaderOptions, { + propsData: { + option: { title: 'Original Title' }, + ...props, + }, + }); + }; + + const findDropdownSectionHeader = () => wrapper.findComponent(GlDropdownSectionHeader); + + describe('component rendering', () => { + it('renders GlDropdownSectionHeader', () => { + createComponent(); + expect(findDropdownSectionHeader().exists()).toBe(true); + }); + + it('renders title text', () => { + createComponent(); + expect(findDropdownSectionHeader().text()).toBe('Original Title'); + }); + + it('handles empty title', () => { + const option = { title: '' }; + createComponent({ option }); + expect(findDropdownSectionHeader().text()).toBe(''); + }); + + it('handles non-string title', () => { + const option = { title: 123 }; + createComponent({ option }); + expect(findDropdownSectionHeader().text()).toBe('123'); + }); + }); +}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c7967b153a2b2ecc7837777f3351faab8aefaeaa..2a7d588593611a5a65b87ffec6e3802404d186dd 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -56289,9 +56289,15 @@ msgstr "" msgid "SecurityInventoryFilter|Search projects…" msgstr "" +msgid "SecurityInventory|%{key}" +msgstr "" + msgid "SecurityInventory|Add projects to this group to start tracking their security posture." msgstr "" +msgid "SecurityInventory|Advanced SAST (SAST)" +msgstr "" + msgid "SecurityInventory|An error occurred while fetching subgroups and projects. Please try again." msgstr "" @@ -56301,18 +56307,27 @@ msgstr "" msgid "SecurityInventory|Basic SAST" msgstr "" +msgid "SecurityInventory|Basic SAST (SAST)" +msgstr "" + msgid "SecurityInventory|CS" msgstr "" msgid "SecurityInventory|Container scanning" msgstr "" +msgid "SecurityInventory|Container scanning (CS)" +msgstr "" + msgid "SecurityInventory|Container scanning (standard)" msgstr "" msgid "SecurityInventory|Container scanning for registry" msgstr "" +msgid "SecurityInventory|Container scanning for registry (CS)" +msgstr "" + msgid "SecurityInventory|DAST" msgstr "" @@ -56322,6 +56337,15 @@ msgstr "" msgid "SecurityInventory|Dependency scanning" msgstr "" +msgid "SecurityInventory|Dependency scanning (DS)" +msgstr "" + +msgid "SecurityInventory|Divider" +msgstr "" + +msgid "SecurityInventory|Dynamic Application Security Testing (DAST)" +msgstr "" + msgid "SecurityInventory|Dynamic application security testing (DAST)" msgstr "" @@ -56334,6 +56358,9 @@ msgstr "" msgid "SecurityInventory|IaC" msgstr "" +msgid "SecurityInventory|Infrastructure as Code (IaC)" +msgstr "" + msgid "SecurityInventory|Infrastructure as code scanning (IaC)" msgstr "" @@ -56352,6 +56379,9 @@ msgstr "" msgid "SecurityInventory|Pipeline secret detection" msgstr "" +msgid "SecurityInventory|Pipeline secret detection (SD)" +msgstr "" + msgid "SecurityInventory|Project vulnerabilities" msgstr "" @@ -56370,6 +56400,9 @@ msgstr "" msgid "SecurityInventory|Secret push protection" msgstr "" +msgid "SecurityInventory|Secret push protection (SD)" +msgstr "" + msgid "SecurityInventory|Security inventory" msgstr "" @@ -56379,6 +56412,9 @@ msgstr "" msgid "SecurityInventory|This group doesn't have any subgroups." msgstr "" +msgid "SecurityInventory|Tool coverage" +msgstr "" + msgid "SecurityInventory|Tool coverage: %{coverage}" msgstr "" @@ -56394,6 +56430,12 @@ msgstr "" msgid "SecurityInventory|View vulnerability report" msgstr "" +msgid "SecurityInventory|Vulnerability count" +msgstr "" + +msgid "SecurityInventory|Vulnerability count %{level}" +msgstr "" + msgid "SecurityInventory|You're already viewing this subgroup" msgstr "" @@ -74658,6 +74700,9 @@ msgstr[1] "" msgid "frontmatter" msgstr "" +msgid "greater than or equal" +msgstr "" + msgid "group" msgstr "" @@ -74951,6 +74996,9 @@ msgstr "" msgid "less than a minute" msgstr "" +msgid "less than or equal" +msgstr "" + msgid "level: %{level}" msgstr "" @@ -75903,6 +75951,9 @@ msgstr "" msgid "service accounts" msgstr "" +msgid "severity|Any" +msgstr "" + msgid "severity|Blocker" msgstr ""