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 @@
+
+
+
+ {{ title }}
+
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 @@
+
+
+
+ {{ option.title }}
+
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 ""