[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