
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:
- axios itself got supply-chain-compromised — for real, at scale. On March 31, 2026, Microsoft disclosed that two axios versions (1.14.1 and 0.30.4) had been published with a hidden dependency that silently installed remote-access trojans during npm install, attributed to the North Korean state actor Sapphire Sleet. axios has over 70 million weekly downloads, and because most projects pin it loosely (^1.14.0), the malicious update propagated automatically into builds with no action from anyone. Microsoft's own mitigation guidance: pin exact versions, stop relying on ^/~ ranges, and rotate credentials. (Microsoft Security Blog)
- The litellm compromise showed exactly how invisible "transitive" really is. Attackers compromised a CI/CD credential with publishing rights and pushed malicious litellm versions across PyPI, npm, Docker Hub, GitHub Actions, and OpenVSX in one coordinated campaign — a three-stage payload doing credential harvesting, Kubernetes lateral movement, and a persistent backdoor. It wasn't caught by any scanner. It was found by accident, by a researcher who happened to notice litellm had been pulled in as a transitive dependency of an unrelated MCP plugin he was testing. (Trend Micro)
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:
- Usage — which files actually import this package?
- Reachability — is the vulnerable surface reachable from code that runs, not just present in a lockfile?
- Exposure — public API, internal logic, or only tests?
- Blast radius — how many files, services, repositories depend on it?
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:
- Duo Agent Platform (Custom Agents + Custom Flows) — Custom Agents are interactive chat operators; Custom Flows are ambient, event-triggered pipelines that run in real CI/CD with no human needed to click “run.”
- Orbit MCP server, wired into chat via .gitlab/duo/mcp.json — agents query the import graph directly, no custom integration code.
- GitLab’s managed model for reasoning inside the agents and flow — no external AI API key.
- CI/CD + GitLab Pages — the same pipeline that lints and tests the engine also dogfoods it against itself and publishes a live, interactive dashboard with zero extra hosting.
- Composite identity / CI_JOB_TOKEN — same-project reads need zero manual token setup; a broader GITLAB_TOKEN is only required where it genuinely is (opening MRs, cross-project work).
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.