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:
What are the CVEs behind these advisory ids?
Scoring does not need this. score_risk resolves every alert through the same catalog itself, and reads the threat evidence from whichever of the advisory's CVEs carries the most, so send vuln_id exactly as your scanner reports it. When the evidence came from a different id, the verdict names it in threat_vuln_id.
Call this tool when you need the CVE. Two cases come up in practice:
- Reading the right advisory. Reachability analysis starts from the vulnerable symbol, and the advisory that names it is usually indexed under a CVE rather than the id your scanner reported.
- Comparing threat across an advisory's several CVEs.
cveis k9's canonical pick and the rest come back inaliases, so pass the whole set tolookup_kevandlookup_epsswhen you want to see them side by side.
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, so you can resolve a whole backlog without sorting it by id type first. Send a CVE and you normally get it back as its own cve. The exception is an advisory carrying several CVEs, where cve is k9's canonical pick for the advisory and the one you sent may come back under aliases instead. The pick is the same on every call.
Duplicates collapse before the limit is counted, so 120 ids covering 90 distinct ones is accepted. Two spellings of one id count as two, though: GHSA-abc and ghsa-abc both come back, because the response is keyed as you sent it.
{
"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 advisory's canonical CVE. Usually the id itself when you sent a CVE, except on an advisory carrying several, where one is canonical and the rest are aliases. 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. | Read the advisory under this CVE, and use it for lookup_kev and lookup_epss. |
true |
same as input | You sent the advisory's canonical 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. |
The two lookups differ in how they report a miss. lookup_kev returns every id you send, and an unlisted CVE comes back known_exploited: false. lookup_epss omits ids it has no score for, so a missing entry there means no EPSS coverage rather than a score of zero.
A missing CVE is never a reason to skip an alert
Do not block triage on an unresolvable id, and do not withhold it from score_risk either: pass the advisory id and let the server try. An advisory with genuinely 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 alerts would shrink your backlog by exactly the ones 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 alerts, which resolves advisory ids for you: score_risk.
- Set up the server: Configure the k9 MCP server.