| name | vulnerability-triage |
| description | Parse, normalize, group, and prioritize security findings from GitLab vulnerability reports (JSON), and produce evidence-backed dispositions (fix vs. justified false positive). Triggers on phrases like: vulnerability report, gl-dependency-scanning-report.json, gl-sast-report.json, GitLab security report, CVE, CVSS, EPSS, KEV, triage findings, group vulnerabilities, deduplicate findings, false positive, is this exploitable, reachability, severity, prioritize remediation. Use whenever a GitLab (or GitLab-schema) security report is being analyzed, even if the user just pastes a JSON report or asks 'which of these do we actually need to fix?' |
Vulnerability Triage
Turn a raw GitLab security report into a prioritized, deduplicated, dispositioned backlog a team can act on. This is the analysis brain of the remediation pipeline: it decides what matters, why, and whether each finding is real — before any code is changed.
Where this runs: this is the logic the pipeline's triage stage automates (encoded into a Lambda/Bedrock step). It is equally valid run interactively when designing or validating that stage.
Why this skill exists
The manual process it replaces is slow because a human opens a JSON report with hundreds of entries, mentally de-duplicates the same CVE appearing across dozens of dependency paths, guesses at real-world risk, and writes ad-hoc justifications for false positives. Each of those steps is mechanical or rule-based enough to do consistently and fast — but only if you understand the report schema and apply a defensible prioritization and disposition standard. That is exactly what this skill supplies.
The triage pipeline (do these in order)
- Parse & normalize — read the report, detect its type, extract findings into a uniform shape.
- Group & deduplicate — collapse the same underlying vulnerability appearing many times.
- Prioritize — score each group by real risk (severity + exploitability + reachability + exposure), not by raw count.
- Disposition — for each group decide FIX, FALSE POSITIVE (justified), or ACCEPT/DEFER (risk-accepted), each with evidence.
- Emit the triage backlog — a table the remediation and MR skills consume.
Do not skip grouping before prioritizing. Scoring 200 ungrouped rows wastes effort and produces a backlog nobody can act on; scoring 12 groups is tractable and honest.
Step 1 — Parse & normalize
GitLab writes one JSON file per scanner, each following the GitLab security report schema. The common shape:
{
"version": "15.x.x",
"scan": { "scanner": { "id": "gemnasium", "name": "Gemnasium" }, "type": "dependency_scanning" },
"vulnerabilities": [
{
"id": "…",
"category": "dependency_scanning",
"name": "Prototype pollution in lodash",
"severity": "High",
"solution": "Upgrade to lodash 4.17.21",
"identifiers": [
Normalize every finding to this internal record so downstream steps are scanner-agnostic:
{ key, title, scanner, category, severity, package, version, filePath, cve[], cwe[], ghsa[], fixedVersion, links[], gitlabFlags[], rawId }
Report types and where their fields live differ. Before parsing an unfamiliar report, read references/gitlab-report-schemas.md — it maps location, identifiers, and severity for Dependency Scanning, SAST, Container Scanning, Secret Detection, and the CycloneDX SBOM variant.
Step 2 — Group & deduplicate
The same vulnerability shows up repeatedly. Collapse it using a stable group key, chosen in this priority order:
- CVE id (
identifiers[].type == "cve") — the strongest cross-scanner key.
- GHSA / advisory id when no CVE exists.
(package name + vulnerability name) as a last resort when neither id is present.
Group so that "CVE-2021-23337 in lodash" is one item carrying a list of every affected path/version/manifest, not 30 items. Record per group: all affected package@version locations, the manifests they came from, and whether they are direct or transitive dependencies (this heavily informs remediation later).
Watch for genuine distinctions the naive key would merge or split:
- Same CVE, different package → different groups (a CVE can affect multiple packages).
- Same package, different CVEs → different groups.
- Same CVE across prod vs. dev dependencies → keep as one group but tag exposure; dev-only changes the priority.
Step 3 — Prioritize by real risk
Sort the backlog by a defensible composite, not by GitLab severity alone. Use four signals:
| Signal | Source | What it tells you |
|---|
| Severity / CVSS | report severity; CVSS vector in links/identifiers | Intrinsic impact if exploited |
| EPSS | FIRST.org EPSS score (0–1) | Probability it will be exploited in the wild in ~30 days |
| KEV | CISA Known Exploited Vulnerabilities catalog | Whether it is actively exploited now (hard override → top priority) |
| Reachability & exposure | your codebase + dependency graph | Whether the vulnerable code is actually invoked, and whether the component is internet-facing, dev-only, or unused |
A Critical CVSS finding in a dev-only, never-imported transitive dep can rank below a Medium finding in an internet-facing, reachable code path. Say so explicitly in the backlog rather than deferring to the scanner's label.
For CVSS vector interpretation, EPSS/KEV lookup mechanics, reachability heuristics for Node.js, and the exact priority formula, read references/severity-and-prioritization.md. To enrich a finding that lacks a fix version or has a suspicious severity (cross-check GHSA/OSV/NVD/npm advisories), read references/advisory-sources.md.
Step 4 — Disposition each group
Every group gets exactly one disposition, with evidence. This is the step your developers do by hand today; the value is doing it to a consistent, auditable standard.
- FIX — real, reachable or plausibly reachable, a remediation path exists. Hand to
dependency-remediation.
- FALSE POSITIVE (justified) — the finding does not actually apply. This requires evidence, not assertion. Valid justification classes (not exhaustive): vulnerable function never called / not reachable; only the dev/test toolchain is affected and never ships; the CVE applies to a different platform or configuration than yours; already mitigated by a compensating control; the scanner mis-identified the package or version.
- ACCEPT / DEFER (risk-accepted) — real but consciously deferred (e.g., no fix available yet, or effort outweighs low risk this cycle). Must name who accepts it and a revisit trigger.
A false-positive claim without evidence is not a disposition — it is a guess, and it will fail review. Use the disposition taxonomy and the copy-paste justification template in references/false-positive-justification.md.
Step 5 — Emit the triage backlog
Produce a single table (and persist it to the workflow's analysis output). One row per group:
| ID | CVE / Advisory | Package | Affected paths | Direct/Transitive | Severity | EPSS | KEV | Reachable? | Priority | Disposition | Evidence / Justification | Fixed in |
|----|----------------|---------|----------------|-------------------|----------|------|-----|-----------|----------|-------------|--------------------------|----------|
| V-01 | CVE-2021-23337 | lodash | 3 (package-lock) | Transitive | High | 0.12 | No | Yes (util import) | P1 | FIX | Reachable via `_.template` in report builder | 4.17.21 |
| V-02 | CVE-2020-8203 | lodash | 1 (dev) | Transitive (dev) | High | 0.04 | No | No (build-only) | P4 | FALSE POSITIVE | Dev-only, `devDependencies`, never bundled — see justification | 4.17.20 |
This table is the contract handed to dependency-remediation (for FIX rows) and gitlab-mr (for the MR body and any GitLab write-back). Keep the IDs stable so the plan, the code changes, and the MR all reference the same groups.
Quick reference
| Task | Do this |
|---|
| Unknown report type | Detect via scan.type; map fields per references/gitlab-report-schemas.md |
| Dedupe | Group by CVE → GHSA → (package+name) |
| "Is this real?" | Reachability + exposure → disposition with evidence |
| Missing fix version | Enrich via GHSA/OSV/NVD (references/advisory-sources.md) |
| Actively exploited? | Check CISA KEV — if listed, escalate to top priority |
| Justify a false positive | Use the evidence template (references/false-positive-justification.md) |
Common mistakes
- Prioritizing by count. 50 hits of one dev-only CVE is still one low-priority group. Group first.
- Trusting
severity blindly. It's intrinsic, not contextual. Layer EPSS + KEV + reachability.
- Asserting "false positive" without evidence. Every FP needs a named justification class and a concrete reason tied to this codebase.
- Losing the direct/transitive distinction. It determines whether remediation is a simple bump or an override — capture it during grouping, not later.
- Merging distinct CVEs because they share a package, or splitting one CVE across paths. The group key is the vulnerability identity, not the location.