[Frontend]: Add KEV filter rendering to a policy drawer
Why are we doing this work
See parent epic for more details
Relevant links
- Design #556415 (closed)
Non-functional requirements
- 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:
- A global settings paragraph for rule-level criteria (severity, status, age, attributes)
- 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)
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:
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:
{
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:
export const createHumanizedScanners = (scanners = []) =>
scanners.map((scanner) => {
return RULE_MODE_SCANNERS[scanner] || scanner;
});
Update to handle both formats:
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 (currentcriteriaListbehavior: 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 likeknown_exploited,epss_score, etc.)
-
Detect object scanners via:
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:
{
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:
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 }):
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:
<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(includingknown_exploited,epss_score) - Mixed format scanners (some string, some object) — should gracefully handle
- Empty scanner objects (no per-scanner overrides)
-
known_exploitedandepss_scorehumanization inhumanizeVulnerabilityAttributes
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
securityPoliciesKevFilteris 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.
Verification steps
- Check out the corresponding branch
- Enable the
security_policies_kev_filterfeature flag - Create/edit a scan result policy with the V2 rule builder (per-scanner settings enabled)
- Open the policy drawer for the saved policy
- 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_exploitedandepss_scoreattributes are rendered correctly
- 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 (merged) spans 4 logical layers. Splitting into sequential MRs allows each to be reviewed in isolation. After all 4 are merged, !226175 (merged) 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_FUZZINGandREPORT_TYPE_COVERAGE_FUZZINGtoRULE_MODE_SCANNERS - Fix
createHumanizedScannersto 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
warnModeTextto 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_EXPLOITEDmapping tohumanizeVulnerabilityAttributes - Add
humanizeEpssScorefor operator + percentage rendering - Add
buildScannerDetails→ returns[{ name, criteriaList }]for object-format scanners, gated bysecurityPoliciesKevFilterFF
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
scannerDetailsfromhumanizedRules - Render per-scanner name heading + criteria bullet list
- Update locale strings
After MRs 1–4 merge: rebase !226175 (merged) — the four extracted hunks resolve automatically, leaving only any remaining incremental changes.