95% of Vulnerabilities Hide in Dependencies You Never Chose — Here’s How I Built a Tool to Find (And Fix) Them

1. The problem: your dependencies are invisible, and that’s where the industry keeps getting hit

It started with something small. A scanner flagged axios in one of my projects — moderate CVSS, not urgent. Except axios wasn't a direct dependency; it was pulled in transitively, and a different package in the same tree pulled in a different version of it, nested differently in the lockfile. Now I had two upgrade paths for the same CVE, in two places, with no way to know if bumping one would cascade and break something depending on the other.

I spent hours on it. Grepping the codebase, reading changelogs, trying to figure out which bump wouldn’t break something else — for a moderate-risk package that, it turned out, wasn’t even reachable from my public API surface.

Then I read about chalk — eighteen compromised packages, downloaded over 2.6 billion times a week, almost all of it sitting invisible four layers beneath people's test runners. And Log4Shell: ~70,000 projects used Log4j directly, but nearly 174,000 used it transitively — and a full year later, 72% of organizations were still exposed. Industry estimates put roughly 95% of known vulnerabilities in transitive dependencies — the exact layer no developer looks at and no scanner can reason about.

And it’s not a problem the industry has finished learning, either. While I was actually building this, two things happened that made the point for me better than I could:

Both of those are the exact shape of my axios afternoon, just at the scale that actually breaks organizations: a package nobody chose directly, three or four layers down, that a scanner can tell you is present but never tell you is reachable — until someone finds out the hard way.

My axios afternoon wasn't a personal problem. It was the industry's problem at small scale.

Run a scanner against a real repo and you get a wall of findings, every one saying the same thing: this is vulnerable. What it can’t tell you: is this code actually reachable? Does anything import it? Is it behind a public API or buried in a test fixture? The industry optimized detection. It never optimized decision-making. Severity without codebase context is incomplete information — a critical CVE in dead code is a deletion, not an emergency; a moderate one next to a public API is the real threat. A CVSS score alone can’t tell those apart.

2. The insight: GitLab Orbit is the missing context layer

GitLab Orbit is a structured, queryable knowledge graph of your repositories — source code, dependencies, ownership, merge requests, pipelines, security findings — spanning the whole SDLC. For dependency risk, it can answer exactly the questions I was manually grepping for:

The payoff is a reversal a severity score alone can never produce:

lodash · CVSS 9.8                    axios · CVSS 6.5
imported by 0 files imported by 17 files
reachable from 0 APIs reachable from 3 public APIs
→ DEAD CODE. Remove it. → URGENT. Remediate now.

The “critical” finding turns out to be unused. The “moderate” one nobody would look at twice is the real emergency. That reversal is only possible because something understands the graph.

The GitLab pieces I leaned on to make this real, not theoretical:

3. What already exists — and the honest version of the gap

This isn’t a novel problem, and I’d rather be upfront about that than oversell it. Endor Labs does function-level reachability analysis, claiming up to 97% SCA alert-noise reduction — the closest technical analogue to Orbit’s exposure query, gated behind contact-sales pricing reportedly starting around $20k+/year. Apiiro’s “Risk Graph” connects code, developers, infra, and findings into runtime-reachability-aware prioritization — strong proof the graph-fused-into-triage approach works, as a separate platform with its own integration to maintain. OX Security does similar exploitability/reachability scoring. Datadog SCA already identifies the root package behind a transitive vulnerability and recommends bumping it. And — correcting something I initially got wrong — Snyk already has reachability analysis (including a 2026 “Transitive AI Reachability” feature for exactly the deep-nested case that ate my afternoon), and Dependabot already auto-bumps a parent dependency to fix a transitive CVE when there’s no direct line to change.

So the gap isn’t “context-aware prioritization doesn’t exist.” It’s narrower: none of those are native to a graph you already pay for. They’re separately-licensed reachability products, each at $20k+/year and up, each a new vendor relationship and a second copy of your dependency data living somewhere else. If your org already runs GitLab Premium/Ultimate with Orbit enabled, this is the marginal cost of one more graph query — not a new line item.

4. What I built

DependencyIQ is an Orbit-powered Dependency Intelligence engine. One tested codebase (src/), exposed through a CLI, Custom Chat Agents, an ambient Custom Flow, an Agent Skill, and a live dashboard — every surface calls the same logic, so behavior never diverges by entry point.

Scan ─► Score ─► Plan ─► Fix ─► Verify ─► Track ─► Report
│ │ │ │ │ │ │
OSV- Orbit impact per- tests GitLab summary
Scanner blast report eco run issues/ + clean
(any radius + migra system + MR notes comment
eco) + risk -tion fixers pipeline + plans
score plan tools

The risk score is a plain, transparent weighted sum — no hidden multipliers, so the breakdown always adds up to the final number:

score = (cvss / 10 × 0.45)      # raw severity
+ (exposure × 0.35) # real Orbit exposure
+ (usage × 0.10) # how many files import it
− (testCoverage × 0.10) # discount when usage is test-only

For the top finding, the engine picks one of three concrete actions: bump a direct dependency to a settled, least-disruptive secure version; override a transitive one (npm overrides, since there's no direct line to change — the actual fix for the chalk/Log4Shell class of problem); or remove a package Orbit proves nothing imports. It opens the merge request with the Orbit evidence attached. It never merges — that gate stays human.

When Orbit can’t answer — not indexed, transient failure, not enabled for that project — exposure contributes exactly 0, and the report says so explicitly instead of guessing. That rule, never fabricate, always degrade honestly, turned out to matter more than any single feature.

5. How you can use this on your own project

The engine is published as a standalone npm package — adopting it doesn’t mean copying source into your repo.

Prerequisites: GitLab Premium/Ultimate with GitLab Orbit and the Duo Agent Platform enabled, hosted runners available.

Step 1 — add the execution environment. Create .gitlab/duo/agent-config.yml:

image: node:20
network_policy:
include_recommended_allowed: true
allowed_domains: [registry.npmjs.org, github.com, objects.githubusercontent.com, api.github.com]
setup_script:
- npm install -g dependencyiq
- curl -fsSL -o /usr/local/bin/osv-scanner "https://github.com/google/osv-scanner/releases/download/v1.9.2/osv-scanner_linux_amd64"
- chmod +x /usr/local/bin/osv-scanner

Step 2 — register the flow (and any agents) from the AI Catalog and enable its triggers. The flow runs dependencyiq … commands directly in CI, so its behavior is the engine's tested behavior.

Step 3 — (optional) tune it. Add a fenced yaml block to your AGENTS.md:

risk_thresholds:
urgent: 80
high: 50
medium: 20
low: 0
excluded_packages:
- "aws-sdk"
public_api_paths:
- "src/api/**"
test_paths:
- "test/**"
freshness_policy:
max_minor_versions_behind: 2
max_days_behind: 180

Step 4 — run it.

npm install -g dependencyiq
dependencyiq analyze   .  --fix --create-pr     # scan, score, fix, open an MR
dependencyiq analyze . --impact # impact report + migration plan
dependencyiq review-mr . --post # safety-review an incoming dependency MR
dependencyiq freshness . # tech-debt freshness policy check
dependencyiq dashboard . # static HTML report
dependencyiq emergency <group> <package> --dry-run # org-wide incident triage

When run via the flow or chat agents, GitLab injects the project id and tokens through composite identity automatically. For a local run, set GITLAB_TOKEN (api scope) and GITLAB_PROJECT_ID.

That’s it — the flow scans and remediates your repository. The engine installs itself; your dependency data never leaves your own CI.

Closing

DependencyIQ doesn’t out-engineer Endor Labs’ or Apiiro’s reachability analysis — they’ve solved harder versions of this problem than I have here. What it does is change the cost and ownership of context-aware triage for a GitLab-native team: from a $20k+/year separate product to the marginal cost of a query against a graph you already pay for. Severity is a signal, not the answer.

The insight wasn’t novel. Orbit is GitLab’s. OSV-Scanner is Google’s. The npm overrides mechanism existed before this. What's practical about DependencyIQ is the specific integration: asking Orbit about impact, feeding that into decisions, producing real fixes for transitive vulnerabilities, and keeping humans in the loop where it matters.

It’s not going to solve every supply-chain problem. It’s a response to a specific gap: when you have a vulnerability, you need to know quickly if it actually matters to your code. And if it does, you need a clear path to fix it without creating new problems.

That’s what this does.


DependencyIQ: Turning GitLab Orbit’s Knowledge Graph Into Dependency Decisions was originally published in System Weakness on Medium, where people are continuing the conversation by highlighting and responding to this story.