Cut Dependabot noise with AI-powered symbol-level triage

Dependabot is good at telling you that a package version is vulnerable. It is much less useful at telling you whether that alert should jump to the front of the queue.
That gap is where most triage time disappears.
Security teams end up doing the same repetitive work over and over: open the advisory, identify the affected function, search the repository, and figure out whether the vulnerable function is actually used. On a handful of repositories, that is annoying. Across hundreds, it becomes operational drag.
depcut is built to automate exactly that middle step.
It shifts the question from package presence to observable vulnerable function usage. A vulnerable dependency is useful context, but weak evidence on its own. What teams actually need for triage is the next question: does this repository import the vulnerable symbol at all? If the answer is no, that alert can usually be deprioritized. If the answer is yes, it deserves immediate attention.
The thesis: symbol-level triage is a useful product on its own
Many security tools produce alerts at the package/version layer. That is a reasonable place to start, but it is a weak place to stop.
From an operations perspective, what teams usually need next is one level deeper:
- not just whether a package is present
- but whether the vulnerable function is actually referenced
That extra layer is often enough to separate obviously urgent work from alerts that can be deprioritized or reviewed later.
depcut automates that step.
Three states matter more than a binary answer
One of the most important product decisions in depcut is that the output is not binary.
Every alert lands in one of three states:
REACHABLE: the vulnerable symbol was found through a supported import patternUNREACHABLE: the package is not present in the relevant dependency graph, or the symbol is not importedINDETERMINATE: the package is imported, but the tool cannot statically confirm use of the vulnerable symbol
INDETERMINATE exists to avoid two bad failure modes:
- declaring something safe when the evidence is insufficient
- declaring something definitively reachable when the signal is only weak or ambiguous
In practice, REACHABLE raises priority, UNREACHABLE lowers it, and INDETERMINATE isolates the smaller set of cases that still deserve manual review.
How it works
In its current form, depcut scans a single repository via --repo owner/name. It already emits full JSON results and can also write SARIF 2.1.0 for code scanning workflows. The SARIF view intentionally includes only actionable results, REACHABLE and INDETERMINATE, and excludes UNREACHABLE findings that have already been triaged out.
At a high level, the tool performs the following steps:
- fetch open Dependabot alerts for the repository
- deduplicate alerts by GHSA advisory and affected package
- extract vulnerable symbols once per advisory/package pair
- download the repository tarball from GitHub
- index JS/TS imports with tree-sitter
- build the dependency graph from the correct lockfile
- match package and symbol usage
- emit JSON or SARIF with evidence and confidence
Symbol extraction: GHSA first, LLM when it actually adds leverage
The hardest part of this problem is not import matching. It is identifying which public symbols are actually relevant for a given advisory.
depcut uses a two-layer extraction pipeline:
- GHSA
vulnerable_functions, when GitHub exposes them - LLM fallback only when GHSA does not provide usable symbol data
This matters for both accuracy and cost, but it is also where the AI angle becomes genuinely useful rather than cosmetic.
When GitHub already exposes structured vulnerable function data, that is the correct source to prefer. It is deterministic, free, and easy to cache.
That structured data is consumed in a package-scoped way. If a single GHSA affects more than one package, depcut filters vulnerable_functions for the package attached to the alert instead of mixing symbol sets across packages.
The LLM is only used when that structured source is missing or insufficient for the specific advisory/package pair being processed. That keeps the model inside a narrow task boundary: it does not need to understand the whole repository, only infer the vulnerable symbol set for one affected package that lacks usable metadata.
Without an LLM fallback, symbol-level triage stalls on the majority of advisories that do not expose usable function metadata. With the fallback, unstructured advisory context turns into a concrete symbol list that the matcher can use.
The cache follows the same logic. Extracted symbols are stored in SQLite per advisory/package pair and reused across runs, which makes repeated scans much cheaper than the first one. In practice, the model call is a bootstrap cost, not a permanent per-run tax.
Costs
The cost question is the most common one when it comes to AI-powered tools, and it is worth addressing head-on.
A full cold run can be done for a few dollars, and it can land below one dollar if advisory deduplication is high and most advisories are single-package or already include usable GHSA metadata. The more important point is what happens after the first run:
- SQLite caches extracted symbol sets locally per advisory/package pair
- calls to the LLM are only made for advisory/package pairs that have not been seen before
So the long-run cost is better than the cold-start. The model is only a bootstrap cost to get the symbol sets for advisories that lack structured metadata. Once those are cached, subsequent runs are much cheaper.
The current scope
Today the implemented scope is:
- single-repository scanning via
--repo owner/name - npm support
- JS/TS parsing
- JSON and SARIF output
- local SQLite cache
- LLM fallback for advisories with no usable
vulnerable_functions
Conclusion
v0 covers what I needed first: one repo, npm, JS/TS. It is enough to prove that the three-state output (REACHABLE / UNREACHABLE / INDETERMINATE) is a more useful triage primitive than the usual package-level alert. In the future, I will add more package managers and languages. If you want to try it, the repo is here: link.