@vyuhlabs/dxkit 2.2.1 → 2.3.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +40 -29
  3. package/dist/analyzers/bom/discovery.d.ts +38 -0
  4. package/dist/analyzers/bom/discovery.d.ts.map +1 -0
  5. package/dist/analyzers/bom/discovery.js +166 -0
  6. package/dist/analyzers/bom/discovery.js.map +1 -0
  7. package/dist/analyzers/bom/gather.d.ts +28 -0
  8. package/dist/analyzers/bom/gather.d.ts.map +1 -1
  9. package/dist/analyzers/bom/gather.js +98 -0
  10. package/dist/analyzers/bom/gather.js.map +1 -1
  11. package/dist/analyzers/bom/index.d.ts +49 -2
  12. package/dist/analyzers/bom/index.d.ts.map +1 -1
  13. package/dist/analyzers/bom/index.js +188 -12
  14. package/dist/analyzers/bom/index.js.map +1 -1
  15. package/dist/analyzers/bom/types.d.ts +33 -1
  16. package/dist/analyzers/bom/types.d.ts.map +1 -1
  17. package/dist/analyzers/licenses/index.d.ts +1 -1
  18. package/dist/analyzers/licenses/index.d.ts.map +1 -1
  19. package/dist/analyzers/licenses/index.js +22 -7
  20. package/dist/analyzers/licenses/index.js.map +1 -1
  21. package/dist/analyzers/security/detailed.d.ts.map +1 -1
  22. package/dist/analyzers/security/detailed.js +21 -8
  23. package/dist/analyzers/security/detailed.js.map +1 -1
  24. package/dist/analyzers/security/gather.d.ts.map +1 -1
  25. package/dist/analyzers/security/gather.js +76 -1
  26. package/dist/analyzers/security/gather.js.map +1 -1
  27. package/dist/analyzers/security/index.d.ts.map +1 -1
  28. package/dist/analyzers/security/index.js +20 -7
  29. package/dist/analyzers/security/index.js.map +1 -1
  30. package/dist/analyzers/tools/epss.d.ts +55 -0
  31. package/dist/analyzers/tools/epss.d.ts.map +1 -0
  32. package/dist/analyzers/tools/epss.js +133 -0
  33. package/dist/analyzers/tools/epss.js.map +1 -0
  34. package/dist/analyzers/tools/graphify.d.ts.map +1 -1
  35. package/dist/analyzers/tools/graphify.js +17 -7
  36. package/dist/analyzers/tools/graphify.js.map +1 -1
  37. package/dist/analyzers/tools/kev.d.ts +52 -0
  38. package/dist/analyzers/tools/kev.d.ts.map +1 -0
  39. package/dist/analyzers/tools/kev.js +95 -0
  40. package/dist/analyzers/tools/kev.js.map +1 -0
  41. package/dist/analyzers/tools/npm-registry.d.ts +43 -0
  42. package/dist/analyzers/tools/npm-registry.d.ts.map +1 -0
  43. package/dist/analyzers/tools/npm-registry.js +107 -0
  44. package/dist/analyzers/tools/npm-registry.js.map +1 -0
  45. package/dist/analyzers/tools/osv.d.ts +12 -0
  46. package/dist/analyzers/tools/osv.d.ts.map +1 -1
  47. package/dist/analyzers/tools/osv.js +45 -2
  48. package/dist/analyzers/tools/osv.js.map +1 -1
  49. package/dist/analyzers/tools/reachability.d.ts +60 -0
  50. package/dist/analyzers/tools/reachability.d.ts.map +1 -0
  51. package/dist/analyzers/tools/reachability.js +104 -0
  52. package/dist/analyzers/tools/reachability.js.map +1 -0
  53. package/dist/analyzers/tools/risk-score.d.ts +69 -0
  54. package/dist/analyzers/tools/risk-score.d.ts.map +1 -0
  55. package/dist/analyzers/tools/risk-score.js +86 -0
  56. package/dist/analyzers/tools/risk-score.js.map +1 -0
  57. package/dist/analyzers/tools/tool-registry.d.ts +10 -0
  58. package/dist/analyzers/tools/tool-registry.d.ts.map +1 -1
  59. package/dist/analyzers/tools/tool-registry.js +35 -20
  60. package/dist/analyzers/tools/tool-registry.js.map +1 -1
  61. package/dist/analyzers/xlsx/bom.d.ts.map +1 -1
  62. package/dist/analyzers/xlsx/bom.js +1 -2
  63. package/dist/analyzers/xlsx/bom.js.map +1 -1
  64. package/dist/cli.d.ts.map +1 -1
  65. package/dist/cli.js +41 -10
  66. package/dist/cli.js.map +1 -1
  67. package/dist/languages/capabilities/types.d.ts +6 -0
  68. package/dist/languages/capabilities/types.d.ts.map +1 -1
  69. package/dist/languages/csharp.d.ts.map +1 -1
  70. package/dist/languages/csharp.js +8 -0
  71. package/dist/languages/csharp.js.map +1 -1
  72. package/dist/languages/go.d.ts.map +1 -1
  73. package/dist/languages/go.js +24 -7
  74. package/dist/languages/go.js.map +1 -1
  75. package/dist/languages/python.d.ts.map +1 -1
  76. package/dist/languages/python.js +8 -0
  77. package/dist/languages/python.js.map +1 -1
  78. package/dist/languages/rust.d.ts.map +1 -1
  79. package/dist/languages/rust.js +9 -0
  80. package/dist/languages/rust.js.map +1 -1
  81. package/dist/languages/typescript.d.ts.map +1 -1
  82. package/dist/languages/typescript.js +23 -1
  83. package/dist/languages/typescript.js.map +1 -1
  84. package/package.json +1 -1
  85. package/templates/.claude/agents-available/dashboard-builder.md +7 -7
  86. package/templates/.claude/agents-available/dev-report.md +4 -4
  87. package/templates/.claude/agents-available/health-auditor.md +1 -1
  88. package/templates/.claude/agents-available/strategic-planner.md +7 -7
  89. package/templates/.claude/agents-available/vulnerability-scanner.md +3 -3
  90. package/templates/.claude/commands/dashboard.md +1 -1
  91. package/templates/.claude/commands/deps.md +1 -1
  92. package/templates/.claude/commands/dev-report.md +2 -2
  93. package/templates/.claude/commands/docs.md +1 -1
  94. package/templates/.claude/commands/export-pdf.md +3 -3
  95. package/templates/.claude/commands/health.md +3 -3
  96. package/templates/.claude/commands/plan.md +1 -1
  97. package/templates/.claude/commands/quality.md.template +2 -2
  98. package/templates/.claude/commands/stealth-mode.md +1 -1
  99. package/templates/.claude/commands/test-gaps.md +2 -2
  100. package/templates/.claude/commands/vulnerabilities.md +3 -3
package/CHANGELOG.md CHANGED
@@ -7,6 +7,121 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.3.0] - 2026-04-24
11
+
12
+ Minor release — turns the `bom` report from enumeration (1700+ rows
13
+ of noise) into a **decision doc** (top 10 triage queue ranked by
14
+ composite exploit-risk). Every `DepVulnFinding` now carries five
15
+ exploitability signals — CVSS, EPSS, CISA KEV, reachability,
16
+ composite `riskScore` — that consumers can read individually or as
17
+ the ranked `Risk` column. `licenses` + `vulnerabilities` renders
18
+ gain parity with the new bom surface so any dxkit command shows the
19
+ same triage-relevant data.
20
+
21
+ Nine sub-commits (Phase 10h.5) landed behind PRs #4 / #5 / #6 /
22
+ #7 / #8 / #9 / #10 / #11 through the hardened 2.2.1 pipeline —
23
+ the first full release cut where every commit flowed PR → CI-green →
24
+ merge → tag → CI-publishes without deviation.
25
+
26
+ ### Added — exploitability enrichers
27
+
28
+ - **EPSS** (`DepVulnFinding.epssScore`, 0.0–1.0) from FIRST.org's
29
+ `api.first.org/data/v1/epss`. Batched (≤100 CVEs/call), session-
30
+ cached, graceful offline fallback. Non-CVE primaries (GHSA /
31
+ RUSTSEC / GO / PYSEC) resolve via OSV.dev alias lookup — no
32
+ coverage gap across packs. (10h.5.1)
33
+
34
+ - **CISA KEV** (`DepVulnFinding.kev`, boolean) from the official
35
+ catalog at `cisa.gov/.../known_exploited_vulnerabilities.json`.
36
+ Single bulk fetch per process, O(1) lookup. Badge `⚠` in every
37
+ render. (10h.5.2)
38
+
39
+ - **Reachability** (`DepVulnFinding.reachable`, tri-state) — does
40
+ this repo's source actually import the vulnerable package?
41
+ Built from per-pack `ImportsResult`'s specifier extraction;
42
+ `specifierToPackage` handles TS scoped/bare, Python dotted
43
+ modules, Go 3-segment module paths. Coarse name-level
44
+ matching; undefined when no imports data available. (10h.5.3)
45
+
46
+ - **Composite riskScore** (`DepVulnFinding.riskScore`, 0–100) —
47
+ `clamp(cvss*10 × kev? × (1+2*epss) × reach?, 0, 100)`. Formula
48
+ documented in `src/analyzers/tools/risk-score.ts`. Null when
49
+ CVSS missing (no fabrication from side signals). (10h.5.4)
50
+
51
+ - **"This Week's Triage"** section at the top of every bom report —
52
+ top 10 advisories with riskScore ≥ 15, rationale composed from
53
+ most decisive signals (KEV → reachable → CVSS → EPSS), fix
54
+ column with "PROPOSAL:" prefix stripped. (10h.5.5)
55
+
56
+ ### Added — decision-doc UX
57
+
58
+ - **`bom --filter=top-level`** drops transitive rows (1700+ → ~150
59
+ on typical repos) while the `byTopLevelDep` rollup still reflects
60
+ full blast radius — "upgrading `@loopback/cli` resolves 29
61
+ advisories" survives when those 29 transitive rows are hidden.
62
+ `BomEntry.isTopLevel` + `summary.filter` + `summary.unfilteredTotalPackages`
63
+ ride the shape. (10h.5.0)
64
+
65
+ - **Nested-project aggregation** (default ON; `--no-nested` opts
66
+ out). `src/analyzers/bom/discovery.ts` walks the repo,
67
+ discovers every directory with a language manifest
68
+ (package.json, pyproject.toml/requirements.txt/setup.py/Pipfile,
69
+ go.mod, Cargo.toml, *.csproj/*.sln), runs per-root gather, and
70
+ merges with dedup on `(package, version)`. `BomEntry.sources`
71
+ unions the roots each package was found in; `isTopLevel`
72
+ OR-merges; vulns dedup on `(id, package, installedVersion)`.
73
+ Closes **D001a** — `bom platform/` previously missed
74
+ `platform/userserver/` entirely. Side-benefit: naturally
75
+ addresses **D003** (C# multi-project) since each `.csproj`
76
+ becomes its own root. (10h.5.0b)
77
+
78
+ - **`LicenseFinding.releaseDate`** populated from the npm registry
79
+ for every TS-ecosystem package. Closes **D006** — xlsx col 10
80
+ ("Component Release Date") was previously empty. Bundled with
81
+ the EPSS fetcher roundtrip. (10h.5.1)
82
+
83
+ - **`licenses` render** sorts top-level deps (⭐) first, transitive
84
+ below. Adds `Direct` + `Released` columns. Matches bom's
85
+ `--filter=top-level` ordering so cross-referencing the two
86
+ reports Just Works. (10h.5.6)
87
+
88
+ - **`vulnerabilities` render (main, not --detailed)** per-advisory
89
+ table now sorted by `riskScore` desc with `Risk` / `KEV` /
90
+ `Reach` / `EPSS` columns alongside the existing fields. (10h.5.6)
91
+
92
+ ### Fixed
93
+
94
+ - **D013** — graphify's shared Python venv moved from
95
+ `/tmp/graphify-venv` (subject to systemd-tmpfiles sweep + race
96
+ on first install) to `~/.cache/dxkit/tools-venv` (XDG persistent).
97
+ Also fixed `Date.now()` script-tempfile collision class in
98
+ graphify.ts via `fs.mkdtempSync`. Affects every Python-based
99
+ tool dxkit installs (graphify, semgrep, ruff, pip-audit,
100
+ pip-licenses, coverage). Legacy `/tmp/graphify-venv` path still
101
+ probed, so existing installations aren't forced into a
102
+ reinstall. (10f.2)
103
+
104
+ - **OSV.dev GHSA case-sensitivity** — `api.osv.dev/v1/vulns/<GHSA>`
105
+ expects lowercase; npm-audit emits uppercase. `osv.ts`
106
+ `DEFAULT_FETCHER` normalizes the alphabetic portion. Silently
107
+ broke alias resolution for every TS finding pre-2.3.0.
108
+
109
+ ### Changed — output directory
110
+
111
+ - **Reports moved from `.ai/reports/` to `.dxkit/reports/`**.
112
+ Separates tool output (regenerated each run, can be gitignored)
113
+ from AI-agent context (`.ai/sessions/`, `.ai/prompts/` —
114
+ human-authored, version-controlled). All CLI commands + every
115
+ scaffolded slash command / agent / template updated to the new
116
+ path. Existing `.ai/reports/*.md` files become orphans after
117
+ upgrade — acceptable since reports regenerate each run.
118
+
119
+ ### Process
120
+
121
+ - First full release cut through the 2.2.1-hardened publish
122
+ pipeline: 8 PRs, every one PR→CI→admin-squash-merge→main. Each
123
+ dog-fooded the pre-push CI-mirror hooks landed in PR #3.
124
+
10
125
  ## [2.2.1] - 2026-04-23
11
126
 
12
127
  Patch release hardening the publish pipeline after `v2.2.0`'s Publish
package/README.md CHANGED
@@ -15,8 +15,12 @@ Built so agent-written code has deterministic guardrails before it ships. Scores
15
15
  cd your-repo
16
16
  npx @vyuhlabs/dxkit tools install --yes # one-time: install cloc, gitleaks, etc.
17
17
  npx @vyuhlabs/dxkit health --detailed # 6-dimension score + remediation plan
18
- npx @vyuhlabs/dxkit vulnerabilities # secret + SAST + dep-audit scan
18
+ npx @vyuhlabs/dxkit vulnerabilities # secret + SAST + dep-audit (ranked by risk)
19
+ npx @vyuhlabs/dxkit bom --filter=top-level # Bill of Materials w/ "This Week's Triage"
19
20
  npx @vyuhlabs/dxkit test-gaps # import-graph + coverage-aware
21
+ npx @vyuhlabs/dxkit quality # slop + duplication + lint
22
+ npx @vyuhlabs/dxkit licenses # dependency license inventory
23
+ npx @vyuhlabs/dxkit dev-report # git activity + contributors
20
24
  ```
21
25
 
22
26
  **Scaffold AI tooling into a repo:**
@@ -32,31 +36,33 @@ The two modes are complementary. The analyzers run anywhere; the scaffolder writ
32
36
 
33
37
  ## Analyzer CLI (`vyuh-dxkit <command>`)
34
38
 
35
- Seven deterministic analyzers. Each emits a markdown report to `.ai/reports/` and optional structured JSON.
39
+ Seven deterministic analyzers. Each emits a markdown report to `.dxkit/reports/` and optional structured JSON.
36
40
 
37
- | Command | What it does | Runtime | Output |
38
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------- | ------------------------------------------ |
39
- | `health` | 6-dimension score (Testing, Quality, Docs, Security, Maint, DX) | 10–20s | `.ai/reports/health-audit-<date>.md` |
40
- | `vulnerabilities` | gitleaks + semgrep + per-pack dep-audit (per-advisory detail in `--detailed`) | 5–30s | `.ai/reports/vulnerability-scan-<date>.md` |
41
- | `test-gaps` | Coverage artifact → import-graph → filename (strongest wins) | <1s | `.ai/reports/test-gaps-<date>.md` |
42
- | `quality` | Slop score + jscpd duplication + eslint/ruff + hygiene | 5–15s | `.ai/reports/quality-review-<date>.md` |
43
- | `dev-report` | Commits, contributors, hot files, velocity, conventional % | <1s | `.ai/reports/developer-report-<date>.md` |
44
- | `licenses` | Dependency license inventory across every active pack (TS/Python/Go/Rust/C#) | 5–20s | `.ai/reports/licenses-<date>.md` |
45
- | `bom` | **Bill of Materials** — joins licenses + vulnerabilities per package, 15-col XLSX; groups advisories by top-level manifest dep (Snyk-style) | 10–40s | `.ai/reports/bom-<date>.{md,xlsx}` |
41
+ | Command | What it does | Runtime | Output |
42
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | --------------------------------------------- |
43
+ | `health` | 6-dimension score (Testing, Quality, Docs, Security, Maint, DX) | 10–20s | `.dxkit/reports/health-audit-<date>.md` |
44
+ | `vulnerabilities` | gitleaks + semgrep + per-pack dep-audit (per-advisory detail in `--detailed`) | 5–30s | `.dxkit/reports/vulnerability-scan-<date>.md` |
45
+ | `test-gaps` | Coverage artifact → import-graph → filename (strongest wins) | <1s | `.dxkit/reports/test-gaps-<date>.md` |
46
+ | `quality` | Slop score + jscpd duplication + eslint/ruff + hygiene | 5–15s | `.dxkit/reports/quality-review-<date>.md` |
47
+ | `dev-report` | Commits, contributors, hot files, velocity, conventional % | <1s | `.dxkit/reports/developer-report-<date>.md` |
48
+ | `licenses` | Dependency license inventory across every active pack (TS/Python/Go/Rust/C#) | 5–20s | `.dxkit/reports/licenses-<date>.md` |
49
+ | `bom` | **Bill of Materials** — joins licenses + vulns per package, groups by top-level manifest dep (Snyk-style), enriches with CISA KEV + EPSS + reachability, ranks by composite risk score with "This Week's Triage" summary, aggregates nested sub-projects, `--filter=top-level` collapses transitive rows, 15-col XLSX | 10–40s | `.dxkit/reports/bom-<date>.{md,xlsx}` |
46
50
 
47
51
  Plus a converter: `vyuh-dxkit to-xlsx <json-file>` renders any `licenses` or `bom` detailed JSON as the canonical 15-column XLSX.
48
52
 
49
53
  ### Flags (apply to all analyzer commands)
50
54
 
51
- | Flag | Effect |
52
- | ---------------- | -------------------------------------------------------------------------------------- |
53
- | `--detailed` | Also writes `<name>-detailed.md` + `.json` with evidence + ranked remediation actions |
54
- | `--json` | Emit pure JSON on stdout. Logs go to stderr so pipes stay clean |
55
- | `--verbose` | Print per-tool timing to stderr |
56
- | `--no-save` | Skip writing markdown; useful with `--json` |
57
- | `--xlsx` | (`licenses`, `bom` only) Also write 15-col `.xlsx` — drop-in for spreadsheet workflows |
58
- | `-o <file>` | (`licenses`, `bom`, `to-xlsx`) Override output path for xlsx / converted file |
59
- | `--since <date>` | (`dev-report` only) Analyze commits on or after `YYYY-MM-DD` |
55
+ | Flag | Effect |
56
+ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
57
+ | `--detailed` | Also writes `<name>-detailed.md` + `.json` with evidence + ranked remediation actions |
58
+ | `--json` | Emit pure JSON on stdout. Logs go to stderr so pipes stay clean |
59
+ | `--verbose` | Print per-tool timing to stderr |
60
+ | `--no-save` | Skip writing markdown; useful with `--json` |
61
+ | `--xlsx` | (`licenses`, `bom` only) Also write 15-col `.xlsx` — drop-in for spreadsheet workflows |
62
+ | `-o <file>` | (`licenses`, `bom`, `to-xlsx`) Override output path for xlsx / converted file |
63
+ | `--since <date>` | (`dev-report` only) Analyze commits on or after `YYYY-MM-DD` |
64
+ | `--filter` | (`bom` only) `all` (default) or `top-level` — keep only root manifest deps; the byTopLevelDep rollup still reflects transitives |
65
+ | `--no-nested` | (`bom` only) Disable nested-project aggregation. Default discovers every sub-project with a language manifest under cwd and merges their BOMs |
60
66
 
61
67
  ### Detailed mode — evidence + ranked fixes
62
68
 
@@ -188,7 +194,7 @@ CLAUDE.md # Main context file for Claude Code
188
194
 
189
195
  The scaffolded slash commands (`/health`, `/vulnerabilities`, `/test-gaps`, `/quality`, `/dev-report`) use a three-tier fallback:
190
196
 
191
- 1. **Check for an existing report** in `.ai/reports/` from today
197
+ 1. **Check for an existing report** in `.dxkit/reports/` from today
192
198
  2. **Run `vyuh-dxkit <command>`** — deterministic, fast, same output
193
199
  3. **Fall back to LLM analysis** only if the CLI isn't available
194
200
 
@@ -313,10 +319,10 @@ Both loops use the session framework — checkpoints, skill evolution, progress
313
319
 
314
320
  ## Reports
315
321
 
316
- All analyzer commands save timestamped reports to `.ai/reports/`:
322
+ All analyzer commands save timestamped reports to `.dxkit/reports/`:
317
323
 
318
324
  ```
319
- .ai/reports/
325
+ .dxkit/reports/
320
326
  health-audit-<date>.md
321
327
  health-audit-<date>-detailed.md # with --detailed
322
328
  health-audit-<date>-detailed.json # agent-consumable
@@ -359,12 +365,17 @@ When create-devstack writes `.project.yaml` before calling dxkit, detection and
359
365
  ## CLI Reference
360
366
 
361
367
  ```bash
362
- # Analyzer commands
363
- vyuh-dxkit health [path] # 6-dimension score
364
- vyuh-dxkit vulnerabilities [path] # Security scan
365
- vyuh-dxkit test-gaps [path] # Coverage + gaps + actions
366
- vyuh-dxkit quality [path] # Slop + duplication + lint
367
- vyuh-dxkit dev-report [path] # Git activity report
368
+ # Analyzer commands — each writes to .dxkit/reports/<name>-<date>.md
369
+ vyuh-dxkit health [path] # 6-dimension score
370
+ vyuh-dxkit vulnerabilities [path] # Security scan, ranked by composite risk
371
+ vyuh-dxkit test-gaps [path] # Coverage + gaps + actions
372
+ vyuh-dxkit quality [path] # Slop + duplication + lint
373
+ vyuh-dxkit dev-report [path] [--since <date>] # Git activity report
374
+ vyuh-dxkit licenses [path] # Dependency license inventory
375
+ vyuh-dxkit bom [path] [--filter=top-level] # Bill of Materials + risk-ranked triage
376
+
377
+ # Data conversion
378
+ vyuh-dxkit to-xlsx <json-file> # render licenses/bom detailed JSON as 15-col XLSX
368
379
 
369
380
  # Tool management
370
381
  vyuh-dxkit tools # status
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Project-root discovery for nested BOM aggregation.
3
+ *
4
+ * `vyuh-dxkit bom <path>` historically scanned `<path>` as a single
5
+ * project root. Repos like `vyuhlabs-platform/` (root devtools +
6
+ * `userserver/` product) fell through the cracks — the scanner saw
7
+ * only whichever `package.json`/lockfile lived at `<path>`, missing
8
+ * every sibling or nested sub-project. See D001a in the internal
9
+ * defect log for the incident write-up.
10
+ *
11
+ * This module walks the filesystem starting at cwd and returns every
12
+ * directory that looks like an independent project root (i.e. has any
13
+ * language manifest, regardless of whether a parent also does). The
14
+ * BOM analyzer then runs the existing per-root gather against each
15
+ * and merges the results.
16
+ *
17
+ * Why a hardcoded skip-set rather than `exclusions.ts`: this is a
18
+ * structural traversal, not a gitignore-based code scan. `exclusions.ts`
19
+ * is tuned for "which files does the user consider source code?" and
20
+ * pulls in `.gitignore` rules that would incorrectly hide sibling
21
+ * projects (e.g. `.gitignore: dist/` would skip a sub-project under
22
+ * `dist/` even though it might legitimately be a shippable artifact
23
+ * the user wants inventoried).
24
+ */
25
+ /**
26
+ * Walk `cwd` and return every directory that contains at least one
27
+ * language manifest. Always includes `cwd` itself when it has one,
28
+ * even if nested sub-projects also exist (the aggregator treats all
29
+ * roots symmetrically and dedupes findings across them).
30
+ *
31
+ * Pure over the filesystem: no caching, no side effects beyond
32
+ * filesystem reads. Returns absolute paths, sorted alphabetically
33
+ * for deterministic output.
34
+ *
35
+ * Exported for unit tests.
36
+ */
37
+ export declare function discoverProjectRoots(cwd: string, maxDepth?: number): string[];
38
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../../src/analyzers/bom/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AA2DH;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,GAAE,MAA0B,GAAG,MAAM,EAAE,CAIhG"}
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ /**
3
+ * Project-root discovery for nested BOM aggregation.
4
+ *
5
+ * `vyuh-dxkit bom <path>` historically scanned `<path>` as a single
6
+ * project root. Repos like `vyuhlabs-platform/` (root devtools +
7
+ * `userserver/` product) fell through the cracks — the scanner saw
8
+ * only whichever `package.json`/lockfile lived at `<path>`, missing
9
+ * every sibling or nested sub-project. See D001a in the internal
10
+ * defect log for the incident write-up.
11
+ *
12
+ * This module walks the filesystem starting at cwd and returns every
13
+ * directory that looks like an independent project root (i.e. has any
14
+ * language manifest, regardless of whether a parent also does). The
15
+ * BOM analyzer then runs the existing per-root gather against each
16
+ * and merges the results.
17
+ *
18
+ * Why a hardcoded skip-set rather than `exclusions.ts`: this is a
19
+ * structural traversal, not a gitignore-based code scan. `exclusions.ts`
20
+ * is tuned for "which files does the user consider source code?" and
21
+ * pulls in `.gitignore` rules that would incorrectly hide sibling
22
+ * projects (e.g. `.gitignore: dist/` would skip a sub-project under
23
+ * `dist/` even though it might legitimately be a shippable artifact
24
+ * the user wants inventoried).
25
+ */
26
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ var desc = Object.getOwnPropertyDescriptor(m, k);
29
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
30
+ desc = { enumerable: true, get: function() { return m[k]; } };
31
+ }
32
+ Object.defineProperty(o, k2, desc);
33
+ }) : (function(o, m, k, k2) {
34
+ if (k2 === undefined) k2 = k;
35
+ o[k2] = m[k];
36
+ }));
37
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
38
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
39
+ }) : function(o, v) {
40
+ o["default"] = v;
41
+ });
42
+ var __importStar = (this && this.__importStar) || (function () {
43
+ var ownKeys = function(o) {
44
+ ownKeys = Object.getOwnPropertyNames || function (o) {
45
+ var ar = [];
46
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
47
+ return ar;
48
+ };
49
+ return ownKeys(o);
50
+ };
51
+ return function (mod) {
52
+ if (mod && mod.__esModule) return mod;
53
+ var result = {};
54
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
55
+ __setModuleDefault(result, mod);
56
+ return result;
57
+ };
58
+ })();
59
+ Object.defineProperty(exports, "__esModule", { value: true });
60
+ exports.discoverProjectRoots = discoverProjectRoots;
61
+ const fs = __importStar(require("fs"));
62
+ const path = __importStar(require("path"));
63
+ /** File basenames that mark a directory as a project root. */
64
+ const MANIFEST_BASENAMES = new Set([
65
+ 'package.json', // Node
66
+ 'pyproject.toml', // Python (PEP 621 / poetry)
67
+ 'requirements.txt', // Python (pip)
68
+ 'setup.py', // Python (legacy)
69
+ 'Pipfile', // Python (pipenv)
70
+ 'go.mod', // Go
71
+ 'Cargo.toml', // Rust
72
+ ]);
73
+ /** File extensions that mark a directory as a project root. */
74
+ const MANIFEST_EXTENSIONS = [
75
+ '.csproj', // C# project
76
+ '.sln', // C# solution
77
+ ];
78
+ /**
79
+ * Directories we never descend into during discovery.
80
+ *
81
+ * - Dependency trees (`node_modules`, `vendor`, `venv`, `.venv`,
82
+ * `target`, `bin`, `obj`): contain manifests from installed
83
+ * packages, not user projects.
84
+ * - Build output (`dist`, `build`, `out`, `.next`, `.turbo`,
85
+ * `.cache`): derived, not source-of-truth.
86
+ * - VCS / tool metadata (`.git`, `.svn`, `.hg`): never has
87
+ * meaningful manifests.
88
+ *
89
+ * Any dotfile directory is also skipped — caches, IDE state, etc.
90
+ */
91
+ const SKIP_DIR_BASENAMES = new Set([
92
+ 'node_modules',
93
+ 'vendor',
94
+ 'venv',
95
+ '.venv',
96
+ 'target',
97
+ 'bin',
98
+ 'obj',
99
+ 'dist',
100
+ 'build',
101
+ 'out',
102
+ '.next',
103
+ '.turbo',
104
+ '.cache',
105
+ '.git',
106
+ '.svn',
107
+ '.hg',
108
+ 'TestResults',
109
+ 'packages',
110
+ ]);
111
+ /** Default depth cap: enough for `packages/foo/sub`, excess discouraged. */
112
+ const DEFAULT_MAX_DEPTH = 4;
113
+ /**
114
+ * Walk `cwd` and return every directory that contains at least one
115
+ * language manifest. Always includes `cwd` itself when it has one,
116
+ * even if nested sub-projects also exist (the aggregator treats all
117
+ * roots symmetrically and dedupes findings across them).
118
+ *
119
+ * Pure over the filesystem: no caching, no side effects beyond
120
+ * filesystem reads. Returns absolute paths, sorted alphabetically
121
+ * for deterministic output.
122
+ *
123
+ * Exported for unit tests.
124
+ */
125
+ function discoverProjectRoots(cwd, maxDepth = DEFAULT_MAX_DEPTH) {
126
+ const roots = new Set();
127
+ walk(cwd, 0, maxDepth, roots);
128
+ return [...roots].sort();
129
+ }
130
+ function walk(dir, depth, maxDepth, roots) {
131
+ if (depth > maxDepth)
132
+ return;
133
+ let entries;
134
+ try {
135
+ entries = fs.readdirSync(dir, { withFileTypes: true });
136
+ }
137
+ catch {
138
+ return;
139
+ }
140
+ let isRoot = false;
141
+ for (const e of entries) {
142
+ if (!e.isFile())
143
+ continue;
144
+ if (MANIFEST_BASENAMES.has(e.name)) {
145
+ isRoot = true;
146
+ break;
147
+ }
148
+ if (MANIFEST_EXTENSIONS.some((ext) => e.name.endsWith(ext))) {
149
+ isRoot = true;
150
+ break;
151
+ }
152
+ }
153
+ if (isRoot)
154
+ roots.add(dir);
155
+ for (const e of entries) {
156
+ if (!e.isDirectory())
157
+ continue;
158
+ if (SKIP_DIR_BASENAMES.has(e.name))
159
+ continue;
160
+ // Skip dotfile directories (caches, IDE state) except the repo root's own.
161
+ if (e.name.startsWith('.') && depth > 0)
162
+ continue;
163
+ walk(path.join(dir, e.name), depth + 1, maxDepth, roots);
164
+ }
165
+ }
166
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/analyzers/bom/discovery.ts"],"names":[],"mappings":"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"}
@@ -58,6 +58,34 @@ export interface BomGatherResult {
58
58
  entries: BomEntry[];
59
59
  toolsUsed: string[];
60
60
  toolsUnavailable: string[];
61
+ /** Cwd-relative project-root paths the gather walked. Length 1 for
62
+ * single-root scans ("." ); length >1 for nested aggregation. */
63
+ projectRoots: string[];
61
64
  }
65
+ /**
66
+ * Merge per-root gather results into one deduplicated set.
67
+ *
68
+ * Dedupe key is `(package, version)` — the same logical package at
69
+ * the same version installed under two roots is the same artifact,
70
+ * so reporting two rows would be noise. When the same key appears
71
+ * under multiple roots:
72
+ *
73
+ * - `sources` unions the sub-paths
74
+ * - `isTopLevel` OR-merges — if any root treats the package as
75
+ * top-level, the merged entry is top-level (upgrade decisions
76
+ * surface under Top-Level Dep Groups)
77
+ * - `vulns` unions with dedup on `(id, package, installedVersion)`
78
+ * — the same advisory reported from two roots collapses into
79
+ * one finding but its `topLevelDep` list unions
80
+ * - license metadata (licenseType, sourceUrl, etc.) prefers the
81
+ * first root with non-UNKNOWN data, falling back to whatever
82
+ * the first-seen entry carried
83
+ *
84
+ * Pure function; unit-testable without filesystem.
85
+ */
86
+ export declare function mergeNestedBomEntries(perRoot: ReadonlyArray<{
87
+ relPath: string;
88
+ result: BomGatherResult;
89
+ }>): BomGatherResult;
62
90
  export declare function gatherBomEntries(cwd: string): Promise<BomGatherResult>;
63
91
  //# sourceMappingURL=gather.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gather.d.ts","sourceRoot":"","sources":["../../../src/analyzers/bom/gather.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,oCAAoC,CAAC;AACzF,OAAO,KAAK,EAAE,QAAQ,EAAe,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIxE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB1D;AAED;mDACmD;AACnD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAGpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAQrE;AAYD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAkCzF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAiF5E"}
1
+ {"version":3,"file":"gather.d.ts","sourceRoot":"","sources":["../../../src/analyzers/bom/gather.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,cAAc,EAAkB,MAAM,oCAAoC,CAAC;AACzF,OAAO,KAAK,EAAE,QAAQ,EAAe,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAIxE;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAgB1D;AAED;mDACmD;AACnD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,MAAM,CAGpD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,MAAM,CAQrE;AAYD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAkCzF;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B;sEACkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,eAAe,CAAA;CAAE,CAAC,GACnE,eAAe,CAkEjB;AAED,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAkF5E"}
@@ -10,6 +10,7 @@ exports.compareSemver = compareSemver;
10
10
  exports.maxSemver = maxSemver;
11
11
  exports.deriveTier1Resolution = deriveTier1Resolution;
12
12
  exports.buildByTopLevelDep = buildByTopLevelDep;
13
+ exports.mergeNestedBomEntries = mergeNestedBomEntries;
13
14
  exports.gatherBomEntries = gatherBomEntries;
14
15
  const gather_1 = require("../licenses/gather");
15
16
  const gather_2 = require("../security/gather");
@@ -124,6 +125,92 @@ function buildByTopLevelDep(entries) {
124
125
  }
125
126
  return out;
126
127
  }
128
+ /**
129
+ * Merge per-root gather results into one deduplicated set.
130
+ *
131
+ * Dedupe key is `(package, version)` — the same logical package at
132
+ * the same version installed under two roots is the same artifact,
133
+ * so reporting two rows would be noise. When the same key appears
134
+ * under multiple roots:
135
+ *
136
+ * - `sources` unions the sub-paths
137
+ * - `isTopLevel` OR-merges — if any root treats the package as
138
+ * top-level, the merged entry is top-level (upgrade decisions
139
+ * surface under Top-Level Dep Groups)
140
+ * - `vulns` unions with dedup on `(id, package, installedVersion)`
141
+ * — the same advisory reported from two roots collapses into
142
+ * one finding but its `topLevelDep` list unions
143
+ * - license metadata (licenseType, sourceUrl, etc.) prefers the
144
+ * first root with non-UNKNOWN data, falling back to whatever
145
+ * the first-seen entry carried
146
+ *
147
+ * Pure function; unit-testable without filesystem.
148
+ */
149
+ function mergeNestedBomEntries(perRoot) {
150
+ const byKey = new Map();
151
+ const toolsUsed = new Set();
152
+ const toolsUnavailable = new Set();
153
+ const projectRoots = new Set();
154
+ for (const { relPath, result } of perRoot) {
155
+ for (const t of result.toolsUsed)
156
+ toolsUsed.add(t);
157
+ for (const t of result.toolsUnavailable)
158
+ toolsUnavailable.add(t);
159
+ projectRoots.add(relPath);
160
+ for (const e of result.entries) {
161
+ const key = `${e.package}@${e.version}`;
162
+ const existing = byKey.get(key);
163
+ if (!existing) {
164
+ byKey.set(key, { ...e, sources: [relPath] });
165
+ continue;
166
+ }
167
+ // Union sources
168
+ existing.sources = [...new Set([...(existing.sources ?? []), relPath])].sort();
169
+ // OR-merge isTopLevel (any-root-top-level wins; undefined is
170
+ // ignored so a degraded pack doesn't mask a definitive true).
171
+ if (e.isTopLevel === true)
172
+ existing.isTopLevel = true;
173
+ // Prefer non-UNKNOWN license metadata from later roots when the
174
+ // existing entry came up empty.
175
+ if (existing.licenseType === 'UNKNOWN' && e.licenseType !== 'UNKNOWN') {
176
+ existing.licenseType = e.licenseType;
177
+ existing.licenseText ??= e.licenseText;
178
+ existing.sourceUrl ??= e.sourceUrl;
179
+ existing.description ??= e.description;
180
+ existing.supplier ??= e.supplier;
181
+ existing.releaseDate ??= e.releaseDate;
182
+ }
183
+ // Vuln union with dedup on (id, package, installedVersion).
184
+ if (e.vulns.length > 0) {
185
+ const seen = new Set(existing.vulns.map((v) => `${v.id}\0${v.package}\0${v.installedVersion ?? ''}`));
186
+ for (const v of e.vulns) {
187
+ const vkey = `${v.id}\0${v.package}\0${v.installedVersion ?? ''}`;
188
+ if (seen.has(vkey))
189
+ continue;
190
+ seen.add(vkey);
191
+ existing.vulns.push(v);
192
+ }
193
+ // Re-derive maxSeverity + upgradeAdvice after vuln merge.
194
+ existing.maxSeverity = maxSeverityOf(existing.vulns);
195
+ const tieredAdvice = existing.vulns
196
+ .map((v) => v.upgradeAdvice)
197
+ .find((a) => a && a.length > 0);
198
+ existing.upgradeAdvice = tieredAdvice ?? deriveTier1Resolution(existing.vulns);
199
+ }
200
+ // joinedFromBoth: once both sides of the join have been seen
201
+ // anywhere, keep it. Only flips true, never back to false.
202
+ if (e.joinedFromBoth)
203
+ existing.joinedFromBoth = true;
204
+ }
205
+ }
206
+ const entries = [...byKey.values()].sort((a, b) => a.package.localeCompare(b.package) || compareSemver(a.version, b.version));
207
+ return {
208
+ entries,
209
+ toolsUsed: [...toolsUsed],
210
+ toolsUnavailable: [...toolsUnavailable],
211
+ projectRoots: [...projectRoots].sort(),
212
+ };
213
+ }
127
214
  async function gatherBomEntries(cwd) {
128
215
  const [licensesEnv, depVulns] = await Promise.all([
129
216
  (0, gather_1.gatherLicensesResult)(cwd),
@@ -204,6 +291,7 @@ async function gatherBomEntries(cwd) {
204
291
  entries,
205
292
  toolsUsed: [...toolsUsed],
206
293
  toolsUnavailable: [],
294
+ projectRoots: ['.'],
207
295
  };
208
296
  }
209
297
  function buildEntry(lic, vulns, joinedFromBoth) {
@@ -211,6 +299,15 @@ function buildEntry(lic, vulns, joinedFromBoth) {
211
299
  // Pick the first non-empty `upgradeAdvice` from any finding; fall
212
300
  // back to derived advice when all vulns are Tier-1 only.
213
301
  const tieredAdvice = vulns.map((v) => v.upgradeAdvice).find((a) => a && a.length > 0);
302
+ // For vuln-only synthetic rows (no LicenseFinding), treat the
303
+ // package as top-level iff any finding lists itself in topLevelDep
304
+ // or has no transitive attribution. Prevents the filter from
305
+ // dropping pure-vuln rows silently on packs where licenses are
306
+ // missing (e.g. workspace sub-packages pre-10h.5.0b).
307
+ let isTopLevel = lic.isTopLevel;
308
+ if (isTopLevel === undefined && !joinedFromBoth && vulns.length > 0) {
309
+ isTopLevel = vulns.some((v) => !v.topLevelDep || v.topLevelDep.includes(lic.package));
310
+ }
214
311
  return {
215
312
  package: lic.package,
216
313
  version: lic.version,
@@ -224,6 +321,7 @@ function buildEntry(lic, vulns, joinedFromBoth) {
224
321
  maxSeverity: maxSeverityOf(vulns),
225
322
  upgradeAdvice: tieredAdvice ?? deriveTier1Resolution(vulns),
226
323
  joinedFromBoth,
324
+ isTopLevel,
227
325
  };
228
326
  }
229
327
  //# sourceMappingURL=gather.js.map