Vulnerability Triage
ASecurityParse, 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 ...
- 10 stars
- 0 votes
- 0 copies
- 3 views
- Added September 10, 2026
Security analysis
100/100Pro scans all 5 files and shows the line behind each finding
npx -y skills add aws-samples/sample-ramp-aidlc-mod-starter-packs --skill vulnerability-triage --agent claude-codeAre you the author of Vulnerability Triage?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/aws-samples-vulnerability-triage)---
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)
1. **Parse & normalize** — read the report, detect its type, extract findings into a uniform shape.
2. **Group & deduplicate** — collapse the same underlying vulnerability appearing many times.
3. **Prioritize** — score each *group* by real risk (severity + exploitability + reachability + exposure), not by raw count.
4. **Disposition** — for each group decide **FIX**, **FALSE POSITIVE (justified)**, or **ACCEPT/DEFER (risk-accepted)**, each with evidence.
5. **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:
```jsonc
{
"version": "15.x.x",
"scan": { "scanner": { "id": "gemnasium", "name": "Gemnasium" }, "type": "dependency_scanning" },
"vulnerabilities": [
{
"id": "…", // stable per-finding UUID (newer schemas)
"category": "dependency_scanning",
"name": "Prototype pollution in lodash",
"severity": "High", // Critical|High|Medium|Low|Info|Unknown
"solution": "Upgrade to lodash 4.17.21",
"identifiers": [ // ← the key to grouping
{ "type": "cve", "name": "CVE-2021-23337", "value": "CVE-2021-23337" },
{ "type": "gemnasium", "name": "GHSA-…", "value": "…" }
],
"location": { // shape varies by scanner (see reference)
"file": "package-lock.json",
"dependency": { "package": { "name": "lodash" }, "version": "4.17.19" }
},
"links": [ { "url": "https://…" } ],
"flags": [ { "type": "flagged-as-likely-false-positive" } ] // SAST only
}
],
"remediations": [ /* dependency scanning may include auto-fix patches */ ]
}
```
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](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:
1. **CVE id** (`identifiers[].type == "cve"`) — the strongest cross-scanner key.
2. **GHSA / advisory id** when no CVE exists.
3. **`(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](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](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](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**:
```markdown
| 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.
Files in this skill
- SKILL.md
- references/advisory-sources.md
- references/false-positive-justification.md
- references/gitlab-report-schemas.md
- references/severity-and-prioritization.md
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.