dependencyiq 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,374 @@
1
+ # DependencyIQ
2
+
3
+ **A multi-language dependency-vulnerability agent for GitLab — built on GitLab Orbit and the GitLab Duo Agent Platform.**
4
+
5
+ DependencyIQ does not just tell you a package is vulnerable. It tells you whether that vulnerability is *real risk for your code*, what breaks if you upgrade, whether the dependency is dead weight that should be deleted, and — when you ask it to — it applies the fix and opens the merge request itself. The intelligence is grounded in GitLab Orbit's import graph, not in guesses.
6
+
7
+ ---
8
+
9
+ ## Table of contents
10
+
11
+ - [What it is](#what-it-is)
12
+ - [The problem it solves](#the-problem-it-solves)
13
+ - [How it works end to end](#how-it-works-end-to-end)
14
+ - [Architecture](#architecture)
15
+ - [The engine (`src/`)](#the-engine-src)
16
+ - [Risk scoring](#risk-scoring)
17
+ - [Orbit blast-radius analysis](#orbit-blast-radius-analysis)
18
+ - [Remediation logic](#remediation-logic)
19
+ - [The GitLab Duo integration](#the-gitlab-duo-integration)
20
+ - [Distribution: the `dependencyiq` npm package](#distribution-the-dependencyiq-npm-package)
21
+ - [CI/CD pipeline](#cicd-pipeline)
22
+ - [The live dashboard](#the-live-dashboard)
23
+ - [Configuration (`AGENTS.md`)](#configuration-agentsmd)
24
+ - [Using it in your own project](#using-it-in-your-own-project)
25
+ - [CLI reference](#cli-reference)
26
+ - [Key design decisions and why](#key-design-decisions-and-why)
27
+ - [Testing](#testing)
28
+ - [Requirements](#requirements)
29
+ - [License](#license)
30
+
31
+ ---
32
+
33
+ ## What it is
34
+
35
+ DependencyIQ is one tested engine exposed through several surfaces:
36
+
37
+ - A **command-line engine** (`dependencyiq`, published to npm) that scans, scores, plans, fixes, and reports.
38
+ - **Custom Chat Agents** on the GitLab Duo Agent Platform — an interactive control surface for triage and remediation.
39
+ - An **ambient Custom Flow** — an autonomous, event-triggered pipeline that runs the engine in CI/CD and opens a remediation merge request.
40
+ - An **Agent Skill** — a slash-command / natural-language entry point in Duo Chat.
41
+ - A **live dashboard** published to GitLab Pages.
42
+
43
+ Every surface calls the same engine. The logic exists once.
44
+
45
+ ## The problem it solves
46
+
47
+ Every scanner can say "this package has a CVE." Almost none can answer what actually matters next:
48
+
49
+ - **Is this real risk?** A high CVSS score on a package that only your test fixtures import is not the same as one your public API depends on.
50
+ - **What breaks if we upgrade?** One file, or forty, with a breaking change baked into a major version bump?
51
+ - **Can we just delete it?** If nothing in your codebase actually imports a package, patching its CVE is wasted effort — removing it is the fix.
52
+ - **Across every language at once.** A single repository can mix npm, PyPI, Go, Maven, and more; each needs its own toolchain.
53
+
54
+ Answering these by hand means grepping the codebase, reading changelogs, and writing the fix yourself, for every vulnerable package, in every ecosystem. DependencyIQ does it instead, grounded in real import-graph evidence.
55
+
56
+ ## How it works end to end
57
+
58
+ ```
59
+ Scan ─► Score ─► Plan ─► Fix ─► Verify ─► Track ─► Report
60
+ │ │ │ │ │ │ │
61
+ OSV- Orbit impact per- tests GitLab summary
62
+ Scanner blast report eco run issues/ + clean
63
+ (any radius + migra system + MR notes comment
64
+ eco) + risk -tion fixers pipeline + plans
65
+ score plan tools
66
+ ```
67
+
68
+ 1. **Scan** — OSV-Scanner reads every manifest/lockfile present and reports known vulnerabilities across all ecosystems in one pass.
69
+ 2. **Score** — each finding is combined with real Orbit exposure data and a usage count, minus a test-coverage discount, into a 0–100 risk score.
70
+ 3. **Plan** — for the top finding, an Upgrade Impact Report estimates affected files, semver distance, breaking-change risk, and effort, and produces an ordered migration plan.
71
+ 4. **Fix** — the appropriate per-ecosystem fixer edits the manifest (version bump, transitive override, or removal) and commits the change.
72
+ 5. **Verify** — the test suite runs; failures are surfaced and the remediation is marked blocked if anything breaks.
73
+ 6. **Track** — the work becomes visible GitLab artifacts: a tracking issue, MR notes carrying the evidence, and a task plan for follow-ups.
74
+ 7. **Report** — a concise, honest summary is posted where the run was triggered.
75
+
76
+ ## Architecture
77
+
78
+ ```
79
+ ┌──────────────────────────────────────────────────────────────┐
80
+ │ Entry points │
81
+ │ Chat Agents Ambient Flow Agent Skill CLI │
82
+ │ (interactive) (autonomous CI) (Duo Chat) (terminal) │
83
+ └───────────────┬──────────────┬─────────────┬──────────────┬──┘
84
+ └──────────────┴─────────────┴──────────────┘
85
+
86
+
87
+ ┌───────────────────────────────────────┐
88
+ │ The engine — `dependencyiq` CLI │
89
+ │ (src/agent.js + modules) │
90
+ └───────────────────┬───────────────────┘
91
+
92
+ ┌──────────────┬──────────┴───────────┬───────────────┐
93
+ ▼ ▼ ▼ ▼
94
+ OSV-Scanner GitLab Orbit GitLab REST Package
95
+ (scan) (import graph) (commits, MRs, registries
96
+ issues, vulns) (freshness)
97
+ ```
98
+
99
+ Directory map:
100
+
101
+ ```
102
+ src/ # the engine
103
+ ├── agent.js # CLI entry point and orchestrator
104
+ ├── scanners/
105
+ │ ├── osvScanner.js # multi-ecosystem vulnerability scan
106
+ │ ├── cvss.js # CVSS v3 base score from a vector string
107
+ │ ├── manifestParser.js # lists every direct dependency
108
+ │ ├── dependencyTreeBuilder.js # direct-vs-transitive resolution
109
+ │ ├── ecosystemFixers.js # per-ecosystem bump / override / remove
110
+ │ └── supplyChainTrustSignals.js # maintainer-compromise signals
111
+ ├── orbitClient.js # REST client for the Orbit API
112
+ ├── blastRadius.js # single-project exposure analysis
113
+ ├── riskCalculator.js # the 0–100 risk score + dead-dep detection
114
+ ├── impactReport.js # affected files + semver + effort estimate
115
+ ├── upgradeImpactSimulator.js # three remediation strategies
116
+ ├── strategyGenerator.js # markdown templates (plans, MR descriptions)
117
+ ├── freshnessChecker.js # registry lookups + stability-aware version
118
+ ├── freshnessPolicy.js # enforces AGENTS.md freshness thresholds
119
+ ├── configLoader.js # parses the AGENTS.md config block
120
+ ├── prGenerator.js # commits the fix + opens the merge request
121
+ ├── remoteFixer.js # patch a manifest via the API (no checkout)
122
+ ├── crossProjectFanOut.js # group-wide emergency response
123
+ ├── mrReviewer.js # safety-review incoming dependency MRs
124
+ ├── executiveSummary.js # leadership-facing roll-up
125
+ ├── dashboardGenerator.js # static HTML dashboard
126
+ ├── fleet*.js # cross-project aggregation
127
+ ├── activityFetcher.js # live "agent activity" feed
128
+ ├── gitlabAuth.js # token resolution (GITLAB_TOKEN / CI_JOB_TOKEN)
129
+ └── httpRetry.js # shared retry/backoff for HTTP calls
130
+
131
+ .gitlab/duo/
132
+ ├── agent-config.yml # flow execution environment
133
+ ├── mcp.json # connects the Orbit MCP server
134
+ ├── agents/ # the Custom Chat Agent definitions
135
+ └── flows/ # the ambient Custom Flow definition
136
+
137
+ skills/dependency-analysis/SKILL.md # the Agent Skill
138
+ scripts/validate-config.js # CI guard for the Duo agent/flow YAML
139
+ .gitlab-ci.yml # the pipeline
140
+ docs/ # per-capability deep dives
141
+ ```
142
+
143
+ ## The engine (`src/`)
144
+
145
+ The engine is a Node.js application with a [Commander](https://github.com/tj/commander.js)-based CLI. It is **pure logic where it can be** — the content transforms (version bumps, removals, overrides), the risk math, the dependency-tree resolution, and the report templates are all deterministic, side-effect-free functions wrapped by thin I/O layers. That is what makes the behaviour testable and reproducible.
146
+
147
+ The orchestration lives in `agent.js::analyzeRepository()`: detect ecosystems → scan → per-vulnerability Orbit exposure → risk score → rank → (optionally) choose a target version, apply the fix, and open the MR. Every other module is a single responsibility called from there.
148
+
149
+ ## Risk scoring
150
+
151
+ The score is a plain weighted sum minus a discount — **no hidden multipliers** — so the per-finding breakdown shown on the dashboard always adds up to the final number.
152
+
153
+ ```
154
+ score = (cvss / 10 × 0.45) # raw severity
155
+ + (exposure × 0.35) # how exposed the affected files are
156
+ + (usage × 0.10) # how many files import the package
157
+ − (testCoverage × 0.10) # discount when usage is test-only
158
+ ```
159
+
160
+ Scaled to 0–100, then bucketed: **URGENT ≥ 80, HIGH ≥ 50, MEDIUM ≥ 20, LOW < 20** (thresholds are configurable in `AGENTS.md`).
161
+
162
+ - `cvss` is the real CVSS base score when OSV provides one; otherwise it is **computed from the CVSS v3 vector string** (`src/scanners/cvss.js`); only as a last resort does it fall back to a coarse severity-bucket midpoint.
163
+ - `exposure` comes from a real Orbit query. **When Orbit is unavailable it contributes exactly 0** — the score becomes purely CVSS-driven rather than carrying a fabricated baseline. Which case applied is reported as `exposureDataSource`.
164
+ - File classification (public-API / internal / test) honours `public_api_paths` / `test_paths` from `AGENTS.md`, falling back to built-in heuristics.
165
+
166
+ **Decision — why a transparent linear model.** A black-box score that a developer cannot reconstruct is a score they will not trust. Every contribution is shown on the dashboard and in the prefilled issue description, and the parts always sum to the total. Severity is a *signal*, not the answer; exposure is what turns a CVE into a priority.
167
+
168
+ ## Orbit blast-radius analysis
169
+
170
+ The differentiator is `src/blastRadius.js` over `src/orbitClient.js`: it asks GitLab Orbit's knowledge graph which files actually import a vulnerable package (`ImportedSymbol → File` relationships) and whether that code is public-API, internal, or test-only. This is what separates "vulnerable" from "vulnerable *and reachable from your public surface*."
171
+
172
+ - If Orbit confirms **zero importers anywhere**, the recommendation flips from "patch" to **remove** — you do not upgrade a dependency nothing uses; you delete it.
173
+ - The Orbit query DSL is shape-sensitive: single-node lookups use `query_type: "search"` with a singular `node`; joins between entities use `query_type: "traversal"` with `nodes` and `relationships`. The client enforces the correct shape.
174
+ - **Honest degradation:** every Orbit interaction can fail (graph not yet indexed, transient error). When it does, the analysis says so explicitly and proceeds with `exposure = 0` rather than inventing importers. The same rule applies everywhere in the engine — see [Key design decisions](#key-design-decisions-and-why).
175
+
176
+ For chat sessions, the Orbit MCP server is connected through `.gitlab/duo/mcp.json` so the agents can query the graph directly.
177
+
178
+ ## Remediation logic
179
+
180
+ `src/scanners/ecosystemFixers.js` chooses one of three actions per finding, then `src/prGenerator.js` commits it and opens the MR. The fixers never regenerate lockfiles themselves — they print the exact follow-up command (`npm install`, `go mod tidy`, …) which CI runs before the commit.
181
+
182
+ | Situation | Action | Mechanism |
183
+ |---|---|---|
184
+ | Direct dependency with a fixed version | **Bump** the manifest line | per-ecosystem text transform |
185
+ | **Transitive** npm dependency (no direct line to change) | **Override** | adds an `overrides` pin forcing `>= floor` |
186
+ | Orbit confirms the package is unused | **Remove** | deletes the dependency entry |
187
+
188
+ **Decision — transitive vulnerabilities must still produce a fix.** A vulnerable package you do not declare directly is the most common and most ignored case. The fixer first tries a direct bump; if there is no direct line to change, it **falls back to an npm `overrides` pin** rather than giving up with no change. That guarantees a transitive vulnerability still yields a real, committable diff and therefore a real merge request — not a dead end.
189
+
190
+ **Decision — choose the least-disruptive secure version.** For an upgrade, the engine does not blindly take OSV's minimum fixed version. It picks the least-disruptive *settled* version at or above that floor, preferring to stay within the current major where possible, and flags explicitly when a fix is forced to cross a major boundary.
191
+
192
+ **Safety:** the engine applies fixes and opens MRs **only when asked** (`--fix --create-pr`), and it never merges. The merge request is the human approval gate.
193
+
194
+ ## The GitLab Duo integration
195
+
196
+ The engine is exposed to the GitLab Duo Agent Platform through four artifact types under `.gitlab/duo/`.
197
+
198
+ ### Execution environment — `agent-config.yml`
199
+
200
+ Read by GitLab from the project's default branch, this defines the environment every flow runs in:
201
+
202
+ - **`image: node:20`** — the Duo runtime CLI requires Node ≥ 20.17.
203
+ - **`setup_script`** installs the engine as a global CLI (`npm install -g dependencyiq`) and downloads the OSV-Scanner binary. Because the engine is *installed* rather than read from the checked-out repository, the flow scans whatever project it runs in.
204
+ - **`network_policy`** allow-lists the registries the engine needs.
205
+
206
+ ### The Custom Flow (ambient) — `flows/vulnerability-analysis-flow.yaml`
207
+
208
+ `components` are nodes, `routers` are edges, `flow.entry_point` is the start. It runs `environment: ambient` — a real CI job — so unlike a chat session it can execute the tested engine.
209
+
210
+ The entry component is a **triage router**. It inspects the trigger and answers with a single word, and a **conditional router** (`condition.input` + `routes`, the flow-registry mechanism for branching) maps that word to one of three routes:
211
+
212
+ ```
213
+ triage_router ─┬─ analyze → discover_assess → plan → remediate → verify → track → report → end
214
+ ├─ guardian → guardian_review ───────────────────────────────────────────────→ end
215
+ └─ freshness → freshness_check ───────────────────────────────────────────────→ end
216
+ ```
217
+
218
+ - **`analyze`** runs the full six-stage remediation pipeline. Each stage runs an exact `dependencyiq …` command and is instructed to *report only command output and never invent findings*. The analyze route also accepts a **target branch**: if the trigger names one, stage 1 checks it out (`git fetch && git checkout`) before scanning, and degrades safely to the current branch if the checkout fails.
219
+ - **`guardian_review`** runs `dependencyiq review-mr --post` to safety-review an incoming dependency MR.
220
+ - **`freshness_check`** runs `dependencyiq freshness` for a tech-debt policy report.
221
+ - The final **report** stage posts one clean, professional summary comment on the triggering issue (not just the run log), constrained to the actual findings of this run.
222
+
223
+ There is deliberately **no human-approval node**: an ambient flow cannot pause for chat, so the MR that the analyze route opens (but never merges) is the approval gate. `scripts/validate-config.js` graph-checks the flow on every CI run — orphan components, dangling router targets, prompt/component mismatches, and that the pipeline reaches `end`.
224
+
225
+ **Decision — one router, many intents.** Rather than a separate flow per task, a single ambient flow routes by the triggering context. This keeps one trigger surface (the flow's service account) serving analyse, MR review, and freshness, while the routes themselves remain independent.
226
+
227
+ ### The Custom Chat Agents — `agents/*.yaml`
228
+
229
+ Interactive, conversational surfaces. Each defines a `name`, `description`, `public` visibility, a staged `system_prompt`, and a `tools` list:
230
+
231
+ - **DependencyIQ** — single-repository triage. Follows a staged operator protocol: check Orbit health, get the vulnerability signal, read manifests, run the blast-radius query, show the decision-trail math, then stop at an explicit approval gate before committing a simple version-pin fix.
232
+ - **DependencyIQ Emergency Response** — organisation-wide incident triage. Given a compromised package, it queries Orbit's group graph for every project that imports it and classifies exposure. (Kept as a chat agent rather than an ambient route because it is high-stakes and parameterised — it should be run deliberately, not autonomously.)
233
+ - **DependencyIQ Freshness** — read-only. It explains what the `AGENTS.md` freshness policy will enforce and which declared versions are pinned, then hands off to the CLI/flow for the registry-backed answer.
234
+ - **DependencyIQ Guardian** — interactive safety review of an incoming dependency merge request, mirroring the `mr_safety_review` CI logic before the pipeline has even run.
235
+
236
+ **Decision — what a chat agent may and may not do.** Web-UI chat cannot run shell commands, so the chat agents never claim to have run a scanner; they query Orbit and read manifests, and can commit a *simple* version-pin edit because the Commits API can create a branch atomically. Anything heavier — deterministic per-ecosystem fixers, high-volume runs — is the flow's or CLI's job. Each agent's prompt states this boundary as a hard rule.
237
+
238
+ ### The Agent Skill — `skills/dependency-analysis/SKILL.md`
239
+
240
+ Registers `/dependency-analysis` plus natural-language trigger keywords in Duo Chat.
241
+
242
+ ### Orbit connection — `mcp.json`
243
+
244
+ Registers the Orbit MCP server so chat sessions get the Orbit graph tools.
245
+
246
+ ## Distribution: the `dependencyiq` npm package
247
+
248
+ The engine is published to npm as `dependencyiq`, with a `bin` entry exposing the `dependencyiq` command.
249
+
250
+ **Decision — install the engine, do not vendor it.** Earlier the flow invoked `node src/agent.js`, which required the engine's source to live inside the target repository. That couples the tool to one repo. Publishing the engine as a package and installing it in `setup_script` decouples them: any project can run the flow without carrying the engine's source. For a security tool this is also the *correct* trust model — the engine runs inside the customer's own CI, so their dependency data never leaves their environment. The package ships only `src/` (no tests, no tooling).
251
+
252
+ ## CI/CD pipeline
253
+
254
+ `.gitlab-ci.yml` both dog-foods the engine on this repository and ships the dashboard.
255
+
256
+ | Stage | Job | Purpose |
257
+ |---|---|---|
258
+ | validate | `validate_config` | graph-checks the Duo agent/flow YAML and the `AGENTS.md` block |
259
+ | lint | `lint` | ESLint (non-blocking) |
260
+ | test | `test` | Jest with coverage |
261
+ | security | `npm_audit` | npm advisory database, a second signal alongside OSV |
262
+ | security | `mr_safety_review` | on MRs touching `package.json`, posts an approve/review/block verdict |
263
+ | analyze | `analyze_vulnerabilities` | full scan + Orbit risk scoring, uploads a report artifact |
264
+ | analyze | `freshness_check` | tech-debt drift report (non-blocking) |
265
+ | remediate | `remediate_top_vulnerability` | manual-gate job that applies the top fix and opens an MR |
266
+ | deploy | `pages` | publishes the dashboard to GitLab Pages on the default branch |
267
+ | publish | `catalog_publish` | on a semver tag, publishes the agents/flow to the AI Catalog |
268
+
269
+ **Decision — tokens.** Same-project read calls fall back to the automatic `CI_JOB_TOKEN`, so the common path needs zero manual setup. A broader-scoped `GITLAB_TOKEN` is required only where it genuinely is: opening real merge requests and any cross-project work. The `catalog_publish` job is gated to semver tags only, so branch and merge-request pipelines are never affected by it.
270
+
271
+ ## The live dashboard
272
+
273
+ The `pages` job runs `dependencyiq dashboard` and publishes a self-contained, interactive HTML report (no backend) to GitLab Pages:
274
+
275
+ - A **decision trail** per finding — the weighted score breakdown, the real evidence files grouped by exposure (public-API / internal / test), and that finding's share of the project's total flagged risk.
276
+ - **Direct-vs-transitive badges** with the resolved dependency chain, from a real BFS over the lockfile.
277
+ - An **executive summary** — remediation engineer-hours, how many findings are urgent/high, how many are likely-breaking major bumps, how many unused dependencies can be removed.
278
+ - One-click "create issue" links and a copy-paste fix command per row.
279
+
280
+ ## Configuration (`AGENTS.md`)
281
+
282
+ `AGENTS.md` carries a fenced `yaml` block parsed by `src/configLoader.js` and merged over defaults. It is real configuration, not documentation:
283
+
284
+ ```yaml
285
+ risk_thresholds: # the URGENT/HIGH/MEDIUM/LOW cut-offs for the 0–100 score
286
+ urgent: 80
287
+ high: 50
288
+ medium: 20
289
+ low: 0
290
+ excluded_packages: # reported, but never proposed for auto-fix
291
+ - "aws-sdk"
292
+ public_api_paths: # files weighted as public-facing exposure
293
+ - "src/api/**"
294
+ test_paths: # files that count toward the test-coverage discount
295
+ - "test/**"
296
+ freshness_policy: # tech-debt thresholds, applied to every direct dependency
297
+ max_minor_versions_behind: 2
298
+ max_days_behind: 180
299
+ stability_window_days: 14
300
+ ```
301
+
302
+ **Decision — configuration is data the code reads live.** Thresholds and path hints change behaviour on the next run with no code change, and the freshness policy is enforced separately from CVE urgency so tech-debt drift is visible even for packages with no known vulnerability.
303
+
304
+ ## Using it in your own project
305
+
306
+ Because the engine is a published package, adopting DependencyIQ does not require copying its source.
307
+
308
+ 1. Enable **GitLab Duo** and **GitLab Orbit** on your group, with hosted runners available.
309
+ 2. Add `.gitlab/duo/agent-config.yml` to your repository:
310
+ ```yaml
311
+ image: node:20
312
+ network_policy:
313
+ include_recommended_allowed: true
314
+ allowed_domains: [registry.npmjs.org, github.com, objects.githubusercontent.com, api.github.com]
315
+ setup_script:
316
+ - npm install -g dependencyiq
317
+ - curl -fsSL -o /usr/local/bin/osv-scanner "https://github.com/google/osv-scanner/releases/download/v1.9.2/osv-scanner_linux_amd64"
318
+ - chmod +x /usr/local/bin/osv-scanner
319
+ ```
320
+ 3. Register the flow (and any agents) from the AI Catalog and enable its triggers.
321
+ 4. Optionally add an `AGENTS.md` config block to tune thresholds and path hints.
322
+
323
+ The flow then scans and remediates **your** repository — the engine installs itself.
324
+
325
+ ## CLI reference
326
+
327
+ ```bash
328
+ npm install -g dependencyiq
329
+
330
+ dependencyiq analyze . --fix --create-pr # scan, score, fix, open an MR
331
+ dependencyiq analyze . --impact # Upgrade Impact Report + migration plan
332
+ dependencyiq review-mr . --post # safety-review an incoming dependency MR
333
+ dependencyiq freshness . # tech-debt freshness policy check
334
+ dependencyiq dashboard . # static HTML report → public/index.html
335
+ dependencyiq emergency <group> <package> --dry-run # org-wide incident triage
336
+ ```
337
+
338
+ When run via the flow or chat agents, GitLab injects the project id and tokens under composite identity; no manual token setup is needed there. For a local run, set `GITLAB_TOKEN` (api scope) and `GITLAB_PROJECT_ID`.
339
+
340
+ ## Key design decisions and why
341
+
342
+ A consolidated list of the choices that shape everything else.
343
+
344
+ 1. **One engine, many surfaces.** The CLI, chat agents, flow, and skill are different entry points into one tested codebase, never copies of the logic. New surfaces add reach, not duplication.
345
+ 2. **Never fabricate.** Every feature degrades honestly. If Orbit is unavailable, exposure contributes 0 and the report says so; if a CVSS score is missing, it is computed from the vector or marked coarse; the report stage may only restate this run's real findings. A plausible-looking invented number is worse than an honest gap.
346
+ 3. **Run the tested engine, do not approximate it.** The flow executes `dependencyiq …` via `run_command`; the flow's behaviour *is* the engine's behaviour. The agents do not re-derive scanning logic from prose.
347
+ 4. **Exposure over severity.** Risk is driven by what your code actually imports (Orbit), not raw CVSS. A critical CVE in a test-only dependency outranks nothing.
348
+ 5. **Transparent scoring.** A plain weighted sum with no hidden multipliers, shown in full on the dashboard, so the score is auditable and therefore trusted.
349
+ 6. **Transitive vulnerabilities still get a fix.** The fixer falls back to an `overrides` pin when there is no direct line to change, so the most-ignored class of vulnerability still produces a real MR.
350
+ 7. **Remove dead weight instead of patching it.** Zero Orbit importers flips the recommendation from upgrade to removal.
351
+ 8. **Never merge.** The engine opens MRs but never merges; the MR is the human gate. The ambient flow has no approval node by design.
352
+ 9. **One ambient flow, routed by intent.** A triage router dispatches to analyze / guardian / freshness, keeping one trigger surface for several jobs.
353
+ 10. **Install the engine, don't vendor it.** Publishing `dependencyiq` to npm decouples the tool from any one repository and keeps analysis inside the customer's own CI.
354
+ 11. **Configuration is live data.** Thresholds, path hints, and the freshness policy are read from `AGENTS.md` on every run.
355
+
356
+ ## Testing
357
+
358
+ ```bash
359
+ npm test # Jest with coverage
360
+ npm run lint # ESLint
361
+ ```
362
+
363
+ The pure transforms, the risk math, the dependency-tree resolution, the CVSS computation, the supply-chain signals, the MR-review verdicts, and the dashboard generation are all covered by unit tests.
364
+
365
+ ## Requirements
366
+
367
+ - GitLab Premium/Ultimate with **GitLab Orbit** and the **Duo Agent Platform** enabled.
368
+ - **OSV-Scanner** on `PATH` (or set `OSV_SCANNER_PATH`).
369
+ - For local API operations: a `GITLAB_TOKEN` with `api` scope.
370
+ - No external AI API key — reasoning inside the agents and flow uses GitLab's managed model.
371
+
372
+ ## License
373
+
374
+ MIT — see [LICENSE](LICENSE).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dependencyiq",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Multi-language dependency vulnerability agent: OSV-Scanner + GitLab Orbit blast-radius analysis + automated MRs",
5
5
  "main": "src/agent.js",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -72,7 +72,7 @@ async function analyzeRepository(repoPath, projectId, options = {}) {
72
72
 
73
73
  console.log('\n📊 Analyzing blast radius with GitLab Orbit...');
74
74
  for (const vuln of vulnerabilities) {
75
- const exposure = await analyzeExposure(projectId, vuln.package, pathHints);
75
+ const exposure = await analyzeExposure(projectId, vuln.package, vuln.ecosystem, pathHints);
76
76
 
77
77
  vuln.riskScore = calculateRiskScore(vuln, {
78
78
  affectedFilesCount: exposure.affectedFilesCount,
@@ -86,7 +86,7 @@ async function analyzeRepository(repoPath, projectId, options = {}) {
86
86
  vuln.affectedFiles = exposure.affectedFiles;
87
87
  vuln.exposure = exposure;
88
88
  vuln.recommendation = vuln.riskScore.recommendation;
89
- vuln.dependencyChain = resolveDependencyChain(repoPath, vuln.ecosystem, vuln.package);
89
+ vuln.dependencyChain = resolveDependencyChain(repoPath, vuln.ecosystem, vuln.package, vuln.currentVersion);
90
90
  }
91
91
 
92
92
  console.log('\n🎯 Ranking by actual risk...');
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  const orbitClient = require('./orbitClient');
10
+ const { hasAnyToken } = require('./gitlabAuth');
10
11
 
11
12
  // Built-in heuristic defaults, used when AGENTS.md doesn't specify
12
13
  // public_api_paths / test_paths. A project with a non-standard layout
@@ -39,7 +40,12 @@ let orbitReachable = null; // cached per-process: null = unknown, true/false onc
39
40
 
40
41
  async function isOrbitReachable() {
41
42
  if (orbitReachable !== null) return orbitReachable;
42
- if (!process.env.GITLAB_TOKEN) {
43
+ // CI_JOB_TOKEN (auto-present in every CI job) is sufficient for this
44
+ // same-project read call — see gitlabAuth.js's getAuthHeaders, which
45
+ // already falls back to it. Requiring GITLAB_TOKEN specifically here
46
+ // reported Orbit as unavailable in every normal CI/Flow run that never
47
+ // set that CI/CD variable, even when Orbit was actually healthy.
48
+ if (!hasAnyToken()) {
43
49
  orbitReachable = false;
44
50
  return false;
45
51
  }
@@ -85,10 +91,46 @@ function classifyFiles(files, hints = {}) {
85
91
  * @returns {Promise<Object>} exposure analysis with a `source` field
86
92
  * ('orbit' | 'unavailable') so callers know how much to trust it.
87
93
  */
88
- async function analyzeExposure(projectId, packageName, hints = {}) {
94
+ // OSV-Scanner ecosystem -> the `language` Orbit tags File nodes with. Used
95
+ // only to check whether Orbit indexed source in this package's language for
96
+ // the project (see below). An unmapped ecosystem falls back to "any file".
97
+ const ECOSYSTEM_LANGUAGE = {
98
+ PyPI: 'python',
99
+ npm: 'javascript',
100
+ Maven: 'java',
101
+ Go: 'go',
102
+ RubyGems: 'ruby',
103
+ 'crates.io': 'rust',
104
+ NuGet: 'c#',
105
+ Packagist: 'php',
106
+ };
107
+
108
+ async function analyzeExposure(projectId, packageName, ecosystem, hints = {}) {
89
109
  if (projectId && await isOrbitReachable()) {
90
110
  try {
91
111
  const files = await orbitClient.findPackageImporters(projectId, packageName);
112
+
113
+ // Zero importers is ambiguous. It means "safe to remove" ONLY if Orbit
114
+ // actually indexed this project's source IN THIS PACKAGE'S LANGUAGE.
115
+ // Orbit indexes the default branch only, so scanning a Python feature
116
+ // branch of a project whose default branch is JavaScript returns zero
117
+ // Python importers even though the index is "present" — that is
118
+ // unknown, not unused. The language-scoped check below avoids the trap
119
+ // where a JS default branch makes a Python package look falsely unused.
120
+ const language = ECOSYSTEM_LANGUAGE[ecosystem];
121
+ if (files.length === 0 && !(await orbitClient.isProjectIndexed(projectId, language))) {
122
+ return {
123
+ source: 'unavailable',
124
+ affectedFiles: [],
125
+ affectedFilesCount: 0,
126
+ isInPublicAPI: false,
127
+ isInTests: false,
128
+ languages: [],
129
+ recommendation: 'upgrade',
130
+ warning: 'GitLab Orbit is reachable but has not indexed this branch (Orbit indexes the default branch only), so blast radius is unknown here — defaulted to upgrade rather than guessing the dependency is unused. Scan the default branch for real importer data.',
131
+ };
132
+ }
133
+
92
134
  const { isInPublicAPI, isInTests } = classifyFiles(files, hints);
93
135
  const categorizedFiles = files.map(f => ({ ...f, category: categorizeFile(f.path, hints) }));
94
136
  return {