Tool: resolve_vuln_ids
Resolve an advisory id (GHSA-…, GO-…, ALAS-…) to its CVE, from k9's curated catalog of VulnCheck NVD++ and OSV.dev alias data.
Ask your agent:
Resolve these advisory ids to CVEs before scoring them.
In practice your agent should call this itself: the score_risk tool and the scoring workflow both tell it to resolve non-CVE ids first.
Exploitation data is keyed by CVE. KEV tells you whether a CVE is exploited in the wild and EPSS predicts whether it will be, but both are indexed by CVE and neither knows what a GHSA id is. So an alert that names only a GHSA carries no threat evidence until you resolve it, and score_risk scores it with threat: unknown even when the exploitation data exists.
Prefer this tool over fetching alias data from the internet. It resolves from a catalog k9 controls, and it works in egress-restricted environments where a call to a public advisory site would be blocked.
Request
resolve_vuln_ids(vuln_ids)
vuln_ids: a list of vulnerability ids, max 100 per call. Case- and whitespace-insensitive.
Mixed input is fine. An id that is already a CVE comes back with cve set to itself, so you can resolve a whole backlog without sorting it by id type first.
{
"vuln_ids": [
"ghsa-jfh8-c2jp-5v3q",
"CVE-2021-44228",
"GHSA-537c-gmf6-5ccf",
"GHSA-NOPE-NOPE-NOPE"
]
}
Response
A dictionary keyed by your input id, echoed exactly as you sent it. Every id you send comes back, so you do not have to diff your input against the output to find what was dropped.
| Field | Type | Description |
|---|---|---|
vuln_id |
string | The id, as you sent it. |
in_catalog |
bool | Whether k9's catalog knows this id. |
cve |
string | The resolved CVE. Set to the id itself when you sent a CVE. Absent only when the advisory genuinely has no CVE. |
aliases |
list of strings | The other known ids for the same vulnerability. The id you asked about and its cve are excluded, since you already have both. Absent when there are none. |
data_age_seconds |
int | Seconds since the catalog was last loaded. The catalog refreshes daily, so under 86400 is fresh. |
{
"ghsa-jfh8-c2jp-5v3q": {
"vuln_id": "ghsa-jfh8-c2jp-5v3q",
"in_catalog": true,
"cve": "CVE-2021-44228",
"aliases": ["PYSEC-2021-999"],
"data_age_seconds": 41022
},
"CVE-2021-44228": {
"vuln_id": "CVE-2021-44228",
"in_catalog": true,
"cve": "CVE-2021-44228",
"aliases": ["GHSA-JFH8-C2JP-5V3Q", "PYSEC-2021-999"],
"data_age_seconds": 41022
},
"GHSA-537c-gmf6-5ccf": {
"vuln_id": "GHSA-537c-gmf6-5ccf",
"in_catalog": true,
"data_age_seconds": 41022
},
"GHSA-NOPE-NOPE-NOPE": {
"vuln_id": "GHSA-NOPE-NOPE-NOPE",
"in_catalog": false,
"data_age_seconds": 41022
}
}
Two details in the first entry: the key is ghsa-jfh8-c2jp-5v3q in the lower case you sent, even though the catalog indexes it upper-case, and its aliases skip both the GHSA you asked about and the CVE it resolved to, leaving only the third id for the same vulnerability.
Response keys echo your input
The response is keyed by the id as you sent it, not by a normalized form, so response[your_id] always works.
This matters most for GHSA, which GitHub publishes in mixed case (GHSA-h8r8-wccr-v5f2). Lookup is case-insensitive, so ghsa-h8r8-wccr-v5f2 and GHSA-H8R8-WCCR-V5F2 return the same answer, but the key you read back is the string you passed in.
Values in aliases are the exception: they come back in the catalog's upper-case form (GHSA-H8R8-WCCR-V5F2), which is not the casing GitHub publishes. If you build a GitHub URL or API call from an alias, lower-case the GHSA- suffix first.
Telling "no CVE" apart from "unknown id"
A missing cve has two different causes with different remedies, so check in_catalog before concluding anything from it.
in_catalog |
cve |
Meaning | What to do |
|---|---|---|---|
true |
present | Resolved. | Use the CVE for lookup_kev, lookup_epss, and as vuln_id in score_risk. |
true |
same as input | You sent a CVE; nothing to resolve. | Use it as-is. |
true |
absent | The advisory has no CVE. Some do not, a vulnerability in a bundled dependency being the common case. | Score it anyway with the advisory id. See below. |
false |
absent | k9's catalog does not know this id. It may be very new, or from an ecosystem the catalog does not cover. | Score it anyway with the advisory id. |
A missing CVE is never a reason to skip a finding
Do not block triage on an unresolvable id. An advisory with no CVE still scores: it carries threat: unknown, and the verdict rests on exposure, impact, and reachability instead. Those are the factors you analyze yourself, and they are often decisive on their own.
GHSA-537c-gmf6-5ccf is a real example, a vulnerability in OpenSSL as bundled by pyca/cryptography, which has no CVE of its own. It resolves as in_catalog: true with no cve, and it scores normally.
Dropping such findings would shrink your backlog by exactly the alerts carrying the least evidence, which inverts the point of triage.
Errors
Sending more than 100 ids raises an error naming the count. Split the batch and call again.
Related
- Score findings once you have their CVEs: score_risk.
- Set up the server: Configure the k9 MCP server.