[Frontend]: Add KEV filter rendering to a policy drawer
<!--Implementation issues are used break-up a large piece of work into small, discrete tasks that can move independently through the build workflow steps. They're typically used to populate a Feature Epic. Once created, an implementation issue is usually refined in order to populate and review the implementation plan and weight. Example workflow: https://about.gitlab.com/handbook/engineering/development/threat-management/planning/diagram.html#plan--> ## Why are we doing this work See parent [epic](https://gitlab.com/groups/gitlab-org/-/epics/16311) for more details ## Relevant links 1. Design https://gitlab.com/gitlab-org/gitlab/-/issues/556415 ## Non-functional requirements <!--Add details for required items and delete others.--> - [ ] Documentation: - [ ] Feature flag: `security_policies_kev_filtering` - [ ] Performance: - [ ] Testing: Unit tests ## Implementation plan ### Context With the `securityPoliciesKevFilter` feature flag enabled, scan result policy rules now store scanners as **objects** (e.g. `{ type: 'sast', vulnerabilities_allowed: 0, severity_levels: [...], vulnerability_states: [...], vulnerability_attributes: { known_exploited: true, epss_score: { operator: '>=', value: 50 } } }`) instead of plain **strings** (e.g. `'sast'`). Each scanner object carries its own per-scanner filter settings, while the rule-level properties (`severity_levels`, `vulnerability_states`, `vulnerability_age`, `vulnerability_attributes`) serve as **global settings** that apply across all scanners. The policy drawer currently only handles the legacy string-based scanner format. We need to update it to render: 1. A **global settings paragraph** for rule-level criteria (severity, status, age, attributes) 2. A **per-scanner paragraph** for each scanner object's individual criteria ### Key files | File | Role | |------|------| | `ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js` | Humanization logic (`humanizeRule`, `humanizeRules`, `humanizeScanners`) | | `ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/details_drawer.vue` | Template rendering of humanized rules | | `ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js` | `createHumanizedScanners()` — maps scanner keys to display names | | `ee/app/assets/javascripts/security_orchestration/components/policy_editor/scan_result/rule/scanners/global_settings.vue` | Reference for global settings structure | | `ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/utils_spec.js` | Utils tests | | `ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/details_drawer_spec.js` | Drawer component tests | ### Scanner data structure (new format) ```yaml rules: - type: scan_finding branch_type: protected # Global settings (rule-level) — apply to all scanners severity_levels: [critical, high] vulnerability_states: [newly_detected] vulnerability_age: { operator: greater_than, value: 30, interval: day } vulnerability_attributes: { fix_available: true } vulnerabilities_allowed: 0 # Per-scanner settings — each scanner is an object scanners: - type: dependency_scanning vulnerabilities_allowed: 5 severity_levels: [critical] vulnerability_attributes: known_exploited: true epss_score: { operator: '>=', value: 50 } - type: sast vulnerabilities_allowed: 0 severity_levels: [] vulnerability_states: [] ``` ### Humanized output example Given this YAML input: ```yaml rules: - type: scan_finding branch_type: default scanners: - type: sast vulnerabilities_allowed: 0 severity_levels: [critical, high] vulnerability_states: [newly_detected] vulnerability_attributes: false_positive: false - type: secret_detection vulnerabilities_allowed: 0 severity_levels: [critical, high] vulnerability_states: [newly_detected] - type: dependency_scanning vulnerabilities_allowed: 0 severity_levels: [critical, high] vulnerability_states: [newly_detected] vulnerability_attributes: false_positive: false fix_available: true known_exploited: true epss_score: { operator: greater_than_or_equal, value: 0.1 } - type: container_scanning vulnerabilities_allowed: 0 severity_levels: [critical, high] vulnerability_states: [newly_detected] vulnerability_attributes: false_positive: false fix_available: true known_exploited: true epss_score: { operator: greater_than_or_equal, value: 0.1 } ``` The drawer should render: > **When SAST, Secret Detection, Dependency Scanning or Container Scanning scanners find any vulnerability in an open merge request targeting the default branch.** > > **SAST** > - Severity is critical or high. > - Vulnerabilities are newly detected and need triage. > - Vulnerabilities are not false positives. > > **Secret Detection** > - Severity is critical or high. > - Vulnerabilities are newly detected and need triage. > > **Dependency Scanning** > - Severity is critical or high. > - Vulnerabilities are newly detected and need triage. > - Vulnerabilities are not false positives and have a fix available. > - Vulnerabilities are in the KEV catalog. > - Vulnerabilities have EPSS score >= 0.1. > > **Container Scanning** > - Severity is critical or high. > - Vulnerabilities are newly detected and need triage. > - Vulnerabilities are not false positives and have a fix available. > - Vulnerabilities are in the KEV catalog. > - Vulnerabilities have EPSS score >= 0.1. The corresponding `humanizeRule` return object for this rule would be: ```js { summary: 'When SAST, Secret Detection, Dependency Scanning or Container Scanning scanners find any vulnerability in an open merge request targeting the default branch.', criteriaList: [], // no rule-level global criteria (all criteria are per-scanner) criteriaMessage: '', branchExceptions: [], scannerDetails: [ { name: 'SAST', criteriaList: [ 'Severity is critical or high.', 'Vulnerabilities are newly detected and need triage.', 'Vulnerabilities are not false positives.', ], }, { name: 'Secret Detection', criteriaList: [ 'Severity is critical or high.', 'Vulnerabilities are newly detected and need triage.', ], }, { name: 'Dependency Scanning', criteriaList: [ 'Severity is critical or high.', 'Vulnerabilities are newly detected and need triage.', 'Vulnerabilities are not false positives and have a fix available.', 'Vulnerabilities are in the KEV catalog.', 'Vulnerabilities have EPSS score >= 0.1.', ], }, { name: 'Container Scanning', criteriaList: [ 'Severity is critical or high.', 'Vulnerabilities are newly detected and need triage.', 'Vulnerabilities are not false positives and have a fix available.', 'Vulnerabilities are in the KEV catalog.', 'Vulnerabilities have EPSS score >= 0.1.', ], }, ], } ``` **Note:** When rule-level global settings (e.g., `severity_levels`, `vulnerability_states`) are also present alongside per-scanner overrides, they render in the `criteriaList` at the top level (before scanner details), and individual scanners only show their own overrides. ### Tasks #### 1. Update `createHumanizedScanners` to handle scanner objects **File:** `ee/app/assets/javascripts/security_orchestration/components/policy_editor/utils.js` The current function only handles strings: ```js export const createHumanizedScanners = (scanners = []) => scanners.map((scanner) => { return RULE_MODE_SCANNERS[scanner] || scanner; }); ``` Update to handle both formats: ```js export const createHumanizedScanners = (scanners = []) => scanners.map((scanner) => { const key = typeof scanner === 'object' ? scanner.type : scanner; return RULE_MODE_SCANNERS[key] || key; }); ``` This ensures `humanizeScanners()` in the drawer still produces correct scanner name strings like `"SAST"`, `"Dependency Scanning"` regardless of input format. #### 2. Update `humanizeRule` for the `SCAN_FINDING` branch to support per-scanner rendering **File:** `ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js` When the scanners are **objects** (new format), `humanizeRule` should return additional data: - **`globalCriteriaList`** — criteria derived from the **rule-level** properties (current `criteriaList` behavior: severity, vulnerability states, age, attributes) - **`scannerDetails`** — an array of per-scanner humanized objects, each containing: - `name` — human-readable scanner name (e.g. `"Dependency Scanning"`) - `criteriaList` — array of per-scanner criteria strings (vulnerabilities allowed, severity, vulnerability attributes like `known_exploited`, `epss_score`, etc.) Detect object scanners via: ```js const hasObjectScanners = rule.scanners?.some((s) => typeof s === 'object'); ``` When `hasObjectScanners` is true, build per-scanner criteria. Each scanner object can have: - `vulnerabilities_allowed` — humanize as "N vulnerabilities allowed" - `severity_levels` — humanize as "Severity is critical, high" - `vulnerability_states` — humanize as "Vulnerabilities are newly detected" - `vulnerability_age` — humanize age - `vulnerability_attributes` — humanize attributes including new ones: - `known_exploited: true` → "are in the KEV catalog" / `false` → "are not in the KEV catalog" - `epss_score: { operator: '>=', value: 50 }` → "have EPSS score >= 50" - `fix_available`, `false_positive` (already supported) The returned object shape should change to include: ```js { summary: '...', // same as before (scanner names + branch text) criteriaList: [...], // global criteria from rule-level props criteriaMessage: '...', // same as before branchExceptions: [...], // same as before scannerDetails: [ // NEW: per-scanner criteria { name: 'Dependency Scanning', criteriaList: ['Severity is critical.', ...] }, { name: 'SAST', criteriaList: [] }, ], } ``` When scanners are plain strings (legacy format), `scannerDetails` should be `undefined`/absent, preserving backward compatibility. #### 3. Add humanization for `known_exploited` and `epss_score` vulnerability attributes **File:** `ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/utils.js` The existing `humanizeVulnerabilityAttributes()` (line 157) only handles `fix_available` and `false_positive`. Extend the `sentenceMap` to include: ```js const sentenceMap = { [FIX_AVAILABLE]: new Map([...]), [FALSE_POSITIVE]: new Map([...]), [KNOWN_EXPLOITED]: new Map([ [true, s__('SecurityOrchestration|are in the KEV catalog')], [false, s__('SecurityOrchestration|are not in the KEV catalog')], ]), }; ``` Add a separate handler for `epss_score` (since it's not a boolean but an object `{ operator, value }`): ```js if (vulnerabilityAttributes.epss_score) { const { operator, value } = vulnerabilityAttributes.epss_score; sentence.push(sprintf(s__('SecurityOrchestration|have EPSS score %{operator} %{value}'), { operator, value })); } ``` Import `KNOWN_EXPLOITED` and `EPSS_SCORE` from the scan_filters constants. #### 4. Update the `details_drawer.vue` template to render per-scanner details **File:** `ee/app/assets/javascripts/security_orchestration/components/policy_drawer/scan_result/details_drawer.vue` Update the humanized rule iteration (lines 181-220) to also destructure and render `scannerDetails`: ```vue <div v-for="( { summary, branchExceptions, licenses, criteriaMessage, criteriaList, denyAllowList, scannerDetails }, idx ) in humanizedRules" :key="idx" class="gl-pt-5" > <!-- existing summary, licenses, branch exceptions rendering --> <gl-sprintf :message="summary"> ... </gl-sprintf> ... <!-- Global criteria (rule-level) --> <p v-if="criteriaMessage">{{ capitalizedCriteriaMessage(criteriaMessage) }}</p> <ul class="gl-m-0"> <li v-for="(criteria, criteriaIdx) in criteriaList" :key="criteriaIdx" class="gl-mt-2"> {{ criteria }} </li> </ul> <!-- Per-scanner criteria (NEW) --> <div v-if="scannerDetails" class="gl-mt-3"> <div v-for="(scanner, scannerIdx) in scannerDetails" :key="scannerIdx" class="gl-mt-3"> <p class="gl-mb-2 gl-font-bold">{{ scanner.name }}</p> <ul v-if="scanner.criteriaList.length" class="gl-m-0"> <li v-for="(criteria, cIdx) in scanner.criteriaList" :key="cIdx" class="gl-mt-2" > {{ criteria }} </li> </ul> </div> </div> <settings :settings="settings" /> ... </div> ``` #### 5. Gate new rendering behind `securityPoliciesKevFilter` feature flag The feature flag should control whether we interpret scanner objects. Two approaches (pick one): **Option A (preferred — logic in utils.js):** In `humanizeRule()`, check `window.gon.features.securityPoliciesKevFilter` before entering the per-scanner code path. When the flag is off, treat object scanners the same as string scanners (extract `.type` only). **Option B (component-level):** In `details_drawer.vue`, inject `glFeatures` and conditionally render `scannerDetails` only when the flag is on. Recommendation: Use **both** — the utils function should gracefully handle both formats regardless, while the template conditionally shows per-scanner details behind the flag. #### 6. Add unit tests for `utils.js` **File:** `ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/utils_spec.js` Add tests for `humanizeRules` with: - Scanner objects with per-scanner `vulnerabilities_allowed`, `severity_levels`, `vulnerability_attributes` (including `known_exploited`, `epss_score`) - Mixed format scanners (some string, some object) — should gracefully handle - Empty scanner objects (no per-scanner overrides) - `known_exploited` and `epss_score` humanization in `humanizeVulnerabilityAttributes` #### 7. Add unit tests for `details_drawer_spec.js` **File:** `ee/spec/frontend/security_orchestration/components/policy_drawer/scan_result/details_drawer_spec.js` Add tests for: - Rendering per-scanner details when `securityPoliciesKevFilter` is enabled and scanners are objects - Not rendering per-scanner details when the feature flag is disabled - Not rendering per-scanner details when scanners are in the legacy string format - Rendering global criteria alongside per-scanner criteria #### 8. Update `createHumanizedScanners` tests **File:** `ee/spec/frontend/security_orchestration/components/policy_editor/utils_spec.js` Add test cases for `createHumanizedScanners` receiving scanner objects. <!--Workflow and other relevant labels # ~"group::" ~"Category:" ~"GitLab Ultimate" Other settings you might want to include when creating the issue. # /assign @ # /epic &--> ## Verification steps 1. Check out the corresponding branch 2. Enable the `security_policies_kev_filter` feature flag 3. Create/edit a scan result policy with the V2 rule builder (per-scanner settings enabled) 4. Open the policy drawer for the saved policy 5. Verify: - Global settings (severity, vulnerability states, age, attributes) are shown as a paragraph - Each scanner (e.g., Dependency Scanning, SAST) has its own paragraph with per-scanner criteria - `known_exploited` and `epss_score` attributes are rendered correctly 6. Disable the feature flag and verify the drawer falls back to the legacy rendering (scanner names only, no per-scanner details) ## MR Breakdown for Easier Review !226175 spans 4 logical layers. Splitting into sequential MRs allows each to be reviewed in isolation. After all 4 are merged, !226175 is rebased — already-merged hunks drop cleanly. **Sequential order:** ### MR 1/4 — Add API Fuzzing & Coverage Fuzzing to scanner registry (independent) **Files:** `policy_editor/constants.js`, `policy_editor/utils.js`, `policy_editor/utils_spec.js`, `optimized_scan_selector_spec.js` - Add `REPORT_TYPE_API_FUZZING` and `REPORT_TYPE_COVERAGE_FUZZING` to `RULE_MODE_SCANNERS` - Fix `createHumanizedScanners` to handle object-format scanners by extracting `.type` Small, self-contained. Unblocks MR 3. --- ### MR 2/4 — Simplify warn mode text in policy approvals (independent) **Files:** `policy_drawer/scan_result/policy_approvals.vue`, `policy_approvals_spec.js` - Update `warnModeText` to a static string (removes dynamic approver/count interpolation) Fully independent — can be merged in any order relative to MR 1. --- ### MR 3/4 — Extend humanization for KEV and EPSS (depends on MR 1) **Files:** `policy_drawer/scan_result/utils.js`, `utils_spec.js` - Add `KNOWN_EXPLOITED` mapping to `humanizeVulnerabilityAttributes` - Add `humanizeEpssScore` for operator + percentage rendering - Add `buildScannerDetails` → returns `[{ name, criteriaList }]` for object-format scanners, gated by `securityPoliciesKevFilter` FF Pure logic layer — no template changes. Easiest to review in isolation. --- ### MR 4/4 — Render per-scanner sections in policy drawer (depends on MR 3) **Files:** `policy_drawer/scan_result/details_drawer.vue`, `details_drawer_spec.js`, `locale/gitlab.pot` - Destructure `scannerDetails` from `humanizedRules` - Render per-scanner name heading + criteria bullet list - Update locale strings --- **After MRs 1–4 merge:** rebase !226175 — the four extracted hunks resolve automatically, leaving only any remaining incremental changes.
issue