@westbayberry/dg 2.3.3 → 2.3.4

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 (122) hide show
  1. package/README.md +94 -217
  2. package/dist/agents/copilot-cli.js +7 -2
  3. package/dist/agents/persistence.js +83 -8
  4. package/dist/agents/registry.js +5 -2
  5. package/dist/agents/routing.js +31 -14
  6. package/dist/api/analyze.js +138 -41
  7. package/dist/audit/deep.js +2 -1
  8. package/dist/audit/detectors.js +23 -2
  9. package/dist/audit/rules.js +4 -1
  10. package/dist/audit-ui/export.js +5 -4
  11. package/dist/auth/device-login.js +33 -11
  12. package/dist/auth/login-app.js +17 -12
  13. package/dist/auth/store.js +65 -9
  14. package/dist/bin/dg.js +1 -1
  15. package/dist/commands/audit.js +12 -20
  16. package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
  17. package/dist/commands/cooldown.js +2 -1
  18. package/dist/commands/decisions.js +8 -7
  19. package/dist/commands/help.js +11 -3
  20. package/dist/commands/licenses.js +17 -8
  21. package/dist/commands/login.js +58 -21
  22. package/dist/commands/router.js +12 -2
  23. package/dist/commands/sbom.js +20 -5
  24. package/dist/commands/scan.js +5 -4
  25. package/dist/commands/service.js +1 -1
  26. package/dist/commands/setup.js +12 -12
  27. package/dist/commands/status.js +4 -4
  28. package/dist/commands/uninstall.js +1 -1
  29. package/dist/commands/update.js +20 -0
  30. package/dist/commands/verify.js +4 -4
  31. package/dist/config/settings.js +81 -12
  32. package/dist/export-ui/ExportDialog.js +1 -1
  33. package/dist/gate/cooldown-request.js +22 -0
  34. package/dist/gate/egress-guard.js +185 -0
  35. package/dist/gate/host-match.js +48 -0
  36. package/dist/gate/verdict-core.js +101 -0
  37. package/dist/gate/verdict-fetch.js +214 -0
  38. package/dist/launcher/agent-check.js +1096 -42
  39. package/dist/launcher/agent-hook-exec.js +8 -1
  40. package/dist/launcher/agent-hook-io.js +5 -3
  41. package/dist/launcher/classify.js +110 -26
  42. package/dist/launcher/env.js +84 -18
  43. package/dist/launcher/install-preflight.js +26 -3
  44. package/dist/launcher/live-install.js +4 -2
  45. package/dist/launcher/manifest-screen.js +88 -7
  46. package/dist/launcher/output-redaction.js +153 -15
  47. package/dist/launcher/preflight-prompt.js +1 -1
  48. package/dist/launcher/resolve-real-binary.js +8 -3
  49. package/dist/launcher/run.js +237 -40
  50. package/dist/origin/artifact-cache.js +55 -0
  51. package/dist/origin/cargo-config.js +12 -0
  52. package/dist/origin/gate-mode.js +7 -0
  53. package/dist/origin/platform-paths.js +14 -0
  54. package/dist/origin/precedence.js +345 -0
  55. package/dist/origin/server.js +611 -0
  56. package/dist/origin/spike-worker.js +35 -0
  57. package/dist/origin/worker.js +116 -0
  58. package/dist/presentation/package-page.js +21 -1
  59. package/dist/presentation/safe-version.js +11 -0
  60. package/dist/presentation/sarif.js +109 -0
  61. package/dist/project/dgfile.js +54 -17
  62. package/dist/project/override-trust.js +0 -0
  63. package/dist/proxy/enforcement.js +27 -7
  64. package/dist/proxy/metadata-map.js +33 -7
  65. package/dist/proxy/preverified.js +0 -3
  66. package/dist/proxy/server.js +104 -446
  67. package/dist/proxy/upstream-proxy.js +23 -4
  68. package/dist/proxy/worker.js +3 -2
  69. package/dist/publish-set/collect.js +6 -0
  70. package/dist/runtime/fatal.js +2 -1
  71. package/dist/runtime/first-run.js +5 -1
  72. package/dist/runtime/node-version.js +51 -0
  73. package/dist/runtime/nudges.js +27 -1
  74. package/dist/sbom/cyclonedx.js +120 -33
  75. package/dist/sbom/graph.js +236 -0
  76. package/dist/sbom/spdx.js +53 -0
  77. package/dist/sbom-ui/SbomApp.js +5 -3
  78. package/dist/sbom-ui/inventory.js +8 -10
  79. package/dist/scan/analyze-worker.js +3 -1
  80. package/dist/scan/collect.js +38 -8
  81. package/dist/scan/command.js +77 -27
  82. package/dist/scan/discovery.js +18 -7
  83. package/dist/scan/installed.js +202 -0
  84. package/dist/scan/manifest-coverage.js +128 -0
  85. package/dist/scan/render.js +125 -40
  86. package/dist/scan/scanner-report.js +92 -22
  87. package/dist/scan/staged.js +68 -15
  88. package/dist/scan/types.js +7 -1
  89. package/dist/scan-ui/LegacyApp.js +3 -3
  90. package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
  91. package/dist/scan-ui/hooks/useScan.js +37 -11
  92. package/dist/scan-ui/launch.js +2 -2
  93. package/dist/scan-ui/logo.js +6 -2
  94. package/dist/scan-ui/shims.js +6 -6
  95. package/dist/scripts/gate.js +53 -35
  96. package/dist/security/csv.js +9 -0
  97. package/dist/security/sanitize.js +11 -2
  98. package/dist/service/state.js +120 -37
  99. package/dist/service/trust-refresh.js +12 -1
  100. package/dist/service/trust-store.js +36 -5
  101. package/dist/service/worker.js +5 -7
  102. package/dist/setup/git-hook.js +2 -2
  103. package/dist/setup/install-method.js +36 -0
  104. package/dist/setup/plan.js +119 -57
  105. package/dist/setup/uninstall-standalone.js +7 -2
  106. package/dist/setup-ui/gate.js +1 -8
  107. package/dist/setup-ui/offer.js +14 -7
  108. package/dist/setup-ui/selector.js +1 -1
  109. package/dist/setup-ui/wizard.js +73 -78
  110. package/dist/standalone/uninstall.mjs +1114 -386
  111. package/dist/state/cleanup-registry.js +14 -3
  112. package/dist/state/cooldown-held.js +36 -13
  113. package/dist/state/locks.js +239 -34
  114. package/dist/state/sessions.js +2 -2
  115. package/dist/util/sh-escape.js +6 -0
  116. package/dist/util/tty-prompt.js +13 -4
  117. package/dist/verify/local.js +60 -10
  118. package/dist/verify/package-check.js +35 -49
  119. package/dist/verify/preflight.js +59 -33
  120. package/dist/verify/render.js +11 -12
  121. package/npm-shrinkwrap.json +714 -213
  122. package/package.json +3 -2
package/README.md CHANGED
@@ -6,19 +6,16 @@
6
6
 
7
7
  Dependency Guardian is a supply-chain firewall for npm and PyPI. The `dg` CLI
8
8
  checks a package against the scanner before its code can run, so a malicious
9
- install is blocked before its install scripts execute, not flagged afterward.
10
- It scans published package artifacts from the registry; your source code stays
11
- on your machine.
9
+ install is blocked before its install scripts execute, not flagged afterward. It
10
+ scans published package artifacts from the registry; your source code stays on
11
+ your machine.
12
12
 
13
- ## Who builds this
14
-
15
- Dependency Guardian is built by Mckeane McBrearty at WestBayBerry. The CLI is
16
- open source under Apache-2.0, and you can read every line at
17
- https://github.com/WestBayBerry/DG_CLI. Releases publish from GitHub Actions
18
- through npm's Trusted Publisher OIDC, never from a personal machine. The scanner
19
- found a 37-package React Native campaign published by the npm account `sproger`
20
- that had gone unreported for over a year; those findings are now in the OpenSSF
21
- malicious-packages database (OSSF PR #1322).
13
+ Built by Mckeane McBrearty at WestBayBerry, open source under Apache-2.0.
14
+ Releases publish from GitHub Actions through npm's Trusted Publisher OIDC, never
15
+ from a personal machine. The scanner's findings land in the OpenSSF
16
+ malicious-packages database, including a 37-package React Native campaign from
17
+ the npm account `sproger` that had gone unreported for over a year (OSSF PR
18
+ #1322).
22
19
 
23
20
  ## Install
24
21
 
@@ -26,41 +23,26 @@ malicious-packages database (OSSF PR #1322).
26
23
  npm install -g @westbayberry/dg
27
24
  ```
28
25
 
29
- Requires Node.js >= 22.14. Installing the package adds only the `dg` binary and
30
- runs no install lifecycle scripts of its own. The published tarball ships an
26
+ Requires Node.js >= 22.14. The install adds only the `dg` binary and runs no
27
+ lifecycle scripts of its own. The published tarball ships an
31
28
  `npm-shrinkwrap.json`, so every install resolves the exact dependency tree the
32
29
  release was audited with. Nothing touches your shell until you run `dg setup`.
33
30
 
34
31
  ## Quickstart
35
32
 
36
- Audit the project in the current directory:
37
-
38
33
  ```bash
39
- dg scan
34
+ dg login # free account
35
+ dg scan # audit the current project's lockfiles
40
36
  ```
41
37
 
42
- `dg scan` reads your lockfiles, asks the scanner for a verdict on each
43
- dependency, and opens a full-screen results browser in an interactive terminal
44
- (plain text or machine output otherwise). It installs nothing and runs no
45
- package scripts. Exit code 0 means the project is clean, 2 means a block-level
46
- package was found.
47
-
48
- ## Verdicts
49
-
50
- Every result is one of three states the scanner returns: **PASS** (verified),
51
- **WARN** (flagged, review advised), or **BLOCK** (refused). The exit code
52
- follows the verdict.
53
-
54
- **The server is the verdict source.** For registry checks and the install
55
- firewall the scanner returns the verdict and the CLI displays it; the CLI never
56
- derives a verdict from a local score. If the scanner is unreachable, `dg scan`
57
- falls back to local heuristics and marks the report (`scannerUnavailable` in
58
- JSON output) rather than passing silently.
38
+ `dg scan` reads your lockfiles and asks the scanner for a verdict on each
39
+ dependency. It installs nothing and runs no package scripts. Exit `0` is clean,
40
+ `2` means a block-level package was found.
59
41
 
60
42
  ## The install firewall
61
43
 
62
- Prefix any install command with `dg` and the package is checked before its
63
- bytes reach your package manager:
44
+ Prefix any install command with `dg` and the package is checked before its bytes
45
+ reach your package manager:
64
46
 
65
47
  ```bash
66
48
  dg npm install lodash
@@ -69,219 +51,115 @@ dg pip install requests
69
51
  dg cargo add serde
70
52
  ```
71
53
 
72
- The prefix starts a local proxy for just that command, hashes each artifact as
73
- it downloads, asks the scanner for a verdict, and refuses a blocked package
74
- before delivery. Supported prefixes include npm, npx, pnpm, pnpx, Yarn classic,
75
- pip, pipx, uv, uvx, and cargo. A block exits 2 and prints the override command;
76
- a warn prompts `Proceed? [y/N]` (default No) in a terminal.
54
+ It proxies just that command, hashes each artifact as it downloads, and refuses
55
+ a blocked one before delivery. Supported prefixes: npm, npx, pnpm, pnpx, Yarn
56
+ classic, pip, pipx, uv, uvx, and cargo. To protect bare `npm install` /
57
+ `pip install` and your AI coding agents too, run `dg setup` (one consent screen,
58
+ reversible with `dg uninstall`).
77
59
 
78
- To protect bare commands too, run `dg setup`. It shows one consent screen
79
- listing exactly what it writes, then installs reversible, user-local shims so
80
- plain `npm install` and `pip install` are scanned automatically, plus a native
81
- hook in each AI coding agent it detects. Undo all of it with `dg uninstall`.
60
+ ## Common commands
82
61
 
83
- ## Command reference
84
-
85
- Run `dg <command> --help` for full flags and examples, or `dg --help-all` for
86
- the complete list. The exhaustive reference lives at
62
+ | Command | What it does |
63
+ | --- | --- |
64
+ | `dg scan [path]` | Audit a project's lockfiles for malicious dependencies |
65
+ | `dg verify npm:react@18` | Check a published package before you install it |
66
+ | `dg sbom [path]` | CycloneDX 1.5 SBOM for the resolved tree |
67
+ | `dg licenses [path]` | SPDX license report grouped by risk |
68
+ | `dg audit [path]` | Pre-publish check of a package of your own |
69
+ | `dg agents` | Route AI coding agents' installs through the firewall |
70
+ | `dg setup` / `dg uninstall` | Install or remove the shell and agent hooks |
71
+ | `dg doctor` | Diagnose setup, PATH, auth, and service state |
72
+
73
+ Run `dg <command> --help` for flags, or `dg --help-all` for the complete list.
74
+ The full reference is at
87
75
  [westbayberry.com/docs/cli-reference](https://westbayberry.com/docs/cli-reference).
88
76
 
89
- ### dg scan [path]
90
-
91
- Audits the project's lockfiles for the path (current directory by default).
92
- Read-only: it never installs, runs package scripts, or changes setup state.
93
- Reads `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`,
94
- `pnpm-lock.yaml`, `requirements.txt`, `poetry.lock`, `Pipfile.lock`, and
95
- `uv.lock`, including nested projects in a monorepo.
96
-
97
- - Flags: `--staged`, `--json`, `--sarif`, `--output <file>`, `--no-decisions`.
98
- - Exit codes: 0 pass, 1 warn (strict mode upgrades warn to 2), 2 block,
99
- 4 analysis incomplete, 10 nothing to scan. JSON output carries a
100
- `schemaVersion` field and sets `scannerUnavailable` when the scanner could
101
- not be reached.
77
+ ## Verdicts and exit codes
102
78
 
103
- ### dg sbom [path]
79
+ Every result is one of three states the scanner returns: **PASS**, **WARN**, or
80
+ **BLOCK**, and the exit code follows it. The server is the verdict source. The
81
+ CLI never derives one from a local score, and if the scanner is unreachable it
82
+ marks the report rather than passing silently.
104
83
 
105
- Inventories the resolved dependency tree as a CycloneDX 1.5 software bill of
106
- materials, with purl, license, and integrity hash per component. Reads the same
107
- lockfiles as `dg scan` plus `Cargo.lock`. In a terminal it streams
108
- BLOCK/WARN/PASS verdicts for npm and PyPI components when signed in; cargo stays
109
- inventory only. Piped, with `--output` (alias `-o`), or with `--json` it stays
110
- offline and prints the raw CycloneDX document.
111
-
112
- - Flags: `--output <path>` / `-o`, `--json`, `--reproducible` (byte-stable
113
- output; honors `SOURCE_DATE_EPOCH`).
114
-
115
- ### dg verify &lt;registry:package[@version] | path | lockfile&gt;
116
-
117
- Runs a real scanner check on a published package before you install it. Name
118
- the registry, `npm:react` or `pypi:requests`, with an optional `@version`
119
- (latest by default). Works signed out with a free verdict summary (the verdict,
120
- top reasons, provenance, and the package page link); signing in unlocks the
121
- full reason list, license info, `--json`, and `--output`. Verifying a local
122
- path, lockfile, or tarball stays free and offline.
123
-
124
- ### dg licenses [path]
125
-
126
- SPDX license report for the dependency tree, grouped by risk. Use `--csv` or
127
- `--markdown` for CI artifacts and `--fail-on <risk>[,<risk>]` to exit non-zero
128
- when a dependency carries a named license-risk tier.
129
-
130
- ### dg audit [path]
131
-
132
- Pre-publish check of a package of your own.
133
- Inspects exactly the resolved publish set of one package, never the whole repo.
134
- Basic checks run 100% locally and upload nothing: leaked secrets and keys,
135
- credential files, source maps, `.git`, terraform state, risky lifecycle
136
- scripts, and a missing `files` allowlist. On a paid plan, and if your org allows
137
- it, it also runs a deep behavioral scan: a packed copy is uploaded with no
138
- lifecycle scripts run, raw bytes are never retained, and only the verdict and
139
- redacted findings reach your dashboard.
84
+ ## Exit codes
140
85
 
141
- - Flags: `--local` (skip the upload), `--require-deep` (fail when deep can't
142
- run), `--fail-on warn`.
143
- - Exit codes: 0 clean, 1 warn (with `--fail-on warn`), 2 block, 3 deep required
144
- but unavailable, 4 analysis incomplete.
86
+ The stable exit codes are `0` pass, `1` warn, `2` block,
87
+ `4` analysis incomplete, `10` nothing to scan, and `70` unexpected CLI error.
88
+ Some commands return `64` when a command unavailable or gated on this platform
89
+ is requested. Machine-readable output includes a `schemaVersion` field so CI
90
+ parsers can pin a contract.
145
91
 
146
- ### dg setup
92
+ ### dg sbom
147
93
 
148
- Installs the shell shims so bare `npm install` / `pip install` are scanned, and
149
- the agent hooks, behind one consent screen. `--print` previews the exact write
150
- plan and changes nothing; `--yes` applies just the shell shims non-interactively
151
- (add `--agents` or `--guard-commit` for the others). Everything it writes is
152
- dg-owned and reversible with `dg uninstall`.
94
+ `dg sbom [path]` inventories the resolved dependency tree as CycloneDX 1.5.
95
+ Interactive terminals can show scanner verdicts for npm and PyPI components;
96
+ `--json` or `--output` writes the raw SBOM for CI.
153
97
 
154
- ### dg guard-commit
98
+ ### dg audit
155
99
 
156
- Installs a per-repo git pre-commit hook that scans the staged lockfile changes
157
- and blocks a commit that would add a malicious dependency. It chains any
158
- existing pre-commit hook and self-checks that git will fire it. Reverse with
159
- `dg guard-commit off`. `git commit --no-verify` is the override.
100
+ `dg audit [path]` is a pre-publish check for a package of your own. Inspects exactly the resolved publish set of one package, never the whole repo. Basic checks run 100% locally. On paid plans, and only when enabled, deep behavioral
101
+ analysis uploads a packed copy for scanning; raw bytes are not retained, and
102
+ only verdict metadata plus redacted findings reach your dashboard.
160
103
 
161
104
  ### dg agents
162
105
 
163
- Routes AI coding agents' installs through the firewall with a native
164
- pre-command hook in each agent's own config, so a malicious or unresolvable
165
- package is blocked before it is fetched. Supported: Claude Code, Codex CLI,
166
- Cursor, Gemini CLI, GitHub Copilot CLI, and Windsurf. Bare `dg agents` lists
167
- each agent and whether it is protected; `dg agents on` hooks every detected
168
- agent (or one, `dg agents on claude-code`); `dg agents --check` verifies the
169
- wiring; `dg agents off` removes it. `dg hook claude-code` keeps working as an
170
- alias.
106
+ `dg agents` installs or checks native pre-command hooks for supported AI coding
107
+ agents so their package-manager calls route through the same install firewall.
108
+ Run `dg agents --check` in CI or a workstation audit to verify the wiring.
171
109
 
172
- ### dg decisions &lt;list | revoke&gt;
110
+ ### dg decisions
173
111
 
174
- Warn-level findings you accept at an install prompt or commit guard can be
175
- remembered in a committable `dg.json` at the project root, so repeat runs only
176
- surface new problems. A new finding category, a severity escalation, or a block
177
- always resurfaces; blocks can never be acknowledged away. `dg decisions list`
178
- shows what the project accepted; `dg decisions revoke` removes an entry.
112
+ `dg decisions list` shows remembered warn-level decisions for the current
113
+ project. `dg decisions revoke` removes one. Blocks can never be acknowledged
114
+ away.
179
115
 
180
- ### dg cooldown [&lt;age&gt; | off]
116
+ ### dg cooldown
181
117
 
182
- Quarantines new installs of a registry release younger than `cooldown.age`
183
- (default `24h`), since most malicious releases are caught within their first
184
- day. Change it with `dg cooldown 7d` or disable it with `dg cooldown off`.
185
- Lockfile scans are never failed by cooldown; packages you already depend on get
186
- a display annotation only. `dg cooldown exempt <name>` records a shareable
187
- exemption in `dg.json`.
118
+ `dg cooldown` shows the effective new-release quarantine and exemptions.
119
+ `dg cooldown exempt <name>` records a project exemption in `dg.json`; user-level
120
+ `cooldown.exempt` applies everywhere.
188
121
 
189
- ### dg config &lt;get | set | unset | list&gt;
122
+ ## In CI
190
123
 
191
- Views or edits user-global configuration in `~/.dg/config.json`. The everyday
192
- key is `policy.mode` (`off`, `warn`, `block` the default, or `strict`), which
193
- sets how a flagged package is handled. There is no detection-tuning knob;
194
- `policy.mode` changes only how a flagged package is handled, not what the
195
- scanner looks for.
124
+ Set `DG_API_KEY` (a `dg_live_*` key from Settings -> CI keys) and run `dg scan`.
125
+ The CLI auto-detects CI and switches to non-interactive output; add
126
+ `--sarif -o dg.sarif` to write findings for upload. On GitHub, the
127
+ [GitHub App](https://westbayberry.com/docs#github-app) is the zero-config path
128
+ and posts a PR status check.
196
129
 
197
- ### dg login / dg logout --yes
130
+ ## Privacy
198
131
 
199
- `dg login` signs in via your browser and links this machine to your account;
200
- `dg login --token dg_live_...` authenticates CI without the browser flow.
201
- Scanning works without an account. `dg logout --yes` removes the saved token
202
- from this machine (the token stays valid until you revoke it from the
203
- dashboard).
204
-
205
- ### dg doctor
206
-
207
- Checks runtime, auth, API, policy, setup shims, PATH precedence, real-binary
208
- resolution, stale state, and service state. Every non-pass line ends with the
209
- exact fix command. `--json` for machine output.
210
-
211
- ### dg service
212
-
213
- Service mode runs a persistent local proxy plus a managed certificate authority
214
- so installs are scanned continuously, for CI runners and private registries
215
- where a per-command proxy is not enough. A paid feature; run `dg login` first,
216
- then `dg service start`. Fully reversible with `dg uninstall --service`.
217
-
218
- ### dg update
219
-
220
- Checks whether a newer CLI version is published and prints the exact
221
- `npm install -g` command to run. It never installs anything and never mutates
222
- the installed package; you run the printed command yourself. Alias: `dg upgrade`.
223
-
224
- ### dg uninstall
225
-
226
- Removes registered dg-owned setup and service writes, state, cache, sessions,
227
- shims, and shell-rc sentinel blocks while preserving unrelated user content.
228
- Run it before `npm uninstall -g @westbayberry/dg`.
229
-
230
- ## Exit codes
231
-
232
- Stable, so CI and shells can read them directly:
233
-
234
- | Code | Meaning |
235
- | --- | --- |
236
- | `0` | Clean. The requested action is allowed. |
237
- | `1` | Warn-level result (strict policy upgrades this to `2`). |
238
- | `2` | Block: a block-level package, a denied install, or an unknown command. |
239
- | `3` | `dg audit --require-deep` ran but the deep scan was unavailable. |
240
- | `4` | Analysis incomplete; not a clean pass. |
241
- | `10` | `dg scan` found nothing to scan. |
242
- | `64` | A usage error (bad flags or arguments). |
243
- | `69` | A command unavailable or gated on this platform, or a signed-out `dg verify` asking for `--json`/`--output`. |
244
- | `70` | Internal tool error (for example, a report file could not be written). |
245
- | `130` / `143` | Interrupted by SIGINT (Ctrl-C) or SIGTERM. |
132
+ No telemetry, no crash reporter, no analytics. The CLI talks to the network only
133
+ when a command needs it: scanning sends package names and versions from your
134
+ lockfiles, and the scanner pulls the artifacts itself; the firewall sends each
135
+ artifact's name, version, and SHA-256. Your source never leaves your machine.
136
+ Detail: [westbayberry.com/docs/telemetry](https://westbayberry.com/docs/telemetry).
246
137
 
247
138
  ## What dg sends to the API
248
139
 
249
- The CLI sends no telemetry: no crash reporter and no analytics endpoint. It
250
- talks to the network only when a command needs it.
251
-
252
- - **Scanning** sends package names and pinned versions from your lockfiles to
253
- the scan API, which pulls the actual package artifacts from the public
254
- registry on its side. Your source never leaves your machine.
255
- - **The install firewall** asks the server for a verdict on each artifact as it
256
- downloads: name, version, ecosystem, registry host, download URL, and the
257
- artifact's SHA-256.
258
- - **Deep audit** (`dg audit`, opt-in, paid) uploads a packed copy of your own
259
- package to `/v1/scan-tarball`; raw bytes are never retained.
260
- - **Version check** queries the public npm registry for `@westbayberry/dg` at
261
- most once a day in an interactive terminal. Set `CI=1` to skip it.
140
+ Authenticated scans send `DG_API_KEY` with package names, versions, ecosystem,
141
+ registry host, lockfile integrity, and artifact hashes where available. Install
142
+ firewall calls send the public package identity and artifact hash needed for a
143
+ verdict. Source files, local paths, stack traces, and crash reports are not sent.
262
144
 
263
- Authenticate CI with `DG_API_KEY` (a `dg_live_*` or `dg_test_*` key;
264
- `DG_API_TOKEN` is an accepted alias). Full detail:
265
- [westbayberry.com/docs/telemetry](https://westbayberry.com/docs/telemetry).
266
-
267
- ## Use in CI
145
+ ## Development
268
146
 
269
147
  ```bash
270
- npm install -g @westbayberry/dg
271
- export DG_API_KEY="dg_live_..." # from Settings -> CI keys
272
- dg scan
148
+ git clone https://github.com/WestBayBerry/DG_CLI.git
149
+ cd DG_CLI
150
+ npm install
151
+ npm run build
152
+ npm test
273
153
  ```
274
154
 
275
- A non-zero exit means a block-level finding; wire that into your pipeline's
276
- pass/fail. The CLI auto-detects CI and switches to non-interactive output; pass
277
- `--json` or `--sarif` when a parser reads stdout. On GitHub, the
278
- [GitHub App](https://westbayberry.com/docs#github-app) is the simpler path and
279
- posts a required PR status check.
155
+ Requires Node.js >= 22.14. Run the built CLI from source with
156
+ `node dist/bin/dg.js --help`, and `npm run check` for typecheck, lint, and
157
+ format checks. Issues and pull requests are welcome.
280
158
 
281
159
  ## Documentation
282
160
 
283
161
  - [Getting started](https://westbayberry.com/docs)
284
- - [CLI reference](https://westbayberry.com/docs/cli-reference)
162
+ - [CLI reference](https://westbayberry.com/docs/cli-reference): every command, flag, and exit code
285
163
  - [Blocking behavior](https://westbayberry.com/docs/blocking)
286
164
  - [Integrations](https://westbayberry.com/docs/integrations)
287
165
  - [Telemetry](https://westbayberry.com/docs/telemetry)
@@ -294,5 +172,4 @@ rather than a public issue.
294
172
 
295
173
  ## License
296
174
 
297
- Apache-2.0. See [LICENSE](./LICENSE). Issues and pull requests are welcome at
298
- [github.com/WestBayBerry/DG_CLI](https://github.com/WestBayBerry/DG_CLI).
175
+ Apache-2.0. See [LICENSE](./LICENSE).
@@ -81,10 +81,15 @@ function emitDecision(verdict) {
81
81
  if (verdict.decision === "allow") {
82
82
  return { stdout: "{}", exitCode: 0 };
83
83
  }
84
+ // Copilot's permissionDecision has no safe human-review value, so a defer
85
+ // ("ask") hardens to "deny" rather than riding through as an unrecognized
86
+ // decision the host may auto-approve — matches codex/gemini/windsurf.
87
+ const suffix = verdict.decision === "ask" ? " (dg flagged this for human review — not auto-approved)" : "";
88
+ const reason = `${verdict.reason ?? "Dependency Guardian firewall"}${suffix}`;
84
89
  return {
85
90
  stdout: JSON.stringify({
86
- permissionDecision: verdict.decision,
87
- ...(verdict.reason ? { permissionDecisionReason: verdict.reason } : {}),
91
+ permissionDecision: "deny",
92
+ permissionDecisionReason: reason,
88
93
  }),
89
94
  exitCode: 0,
90
95
  };
@@ -1,6 +1,7 @@
1
1
  import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
- import { dirname } from "node:path";
3
+ import { homedir } from "node:os";
4
+ import { dirname, sep } from "node:path";
4
5
  import { randomBytes } from "node:crypto";
5
6
  import { readCleanupRegistry, recordCleanupEntry, removeCleanupEntry, } from "../state/index.js";
6
7
  export const LEGACY_AGENT_HOOK_SENTINEL = "dg-agent-hook-v1";
@@ -35,6 +36,33 @@ function assertSafeNode(path, role) {
35
36
  throw new AgentHookError(`${path} (${role}) is world-writable; refusing to write to it. Tighten its permissions, then retry.`);
36
37
  }
37
38
  }
39
+ // The recursive mkdir below follows a symlinked INTERMEDIATE component (e.g. a
40
+ // pre-planted ~/.codeium for the 2-deep windsurf config) before the leaf+parent
41
+ // safety check runs, redirecting the write outside the user tree. Reject any
42
+ // symlink component strictly BELOW the user's home — anchored at homedir() so a
43
+ // legitimately symlinked / networked home is not flagged.
44
+ function assertNoSymlinkBelowHome(target) {
45
+ const home = homedir();
46
+ if (!home) {
47
+ return;
48
+ }
49
+ let dir = dirname(target);
50
+ let prev = "";
51
+ while (dir !== prev && dir.startsWith(`${home}${sep}`)) {
52
+ try {
53
+ if (lstatSync(dir).isSymbolicLink()) {
54
+ throw new AgentHookError(`${dir} is a symlink; refusing to create or write through it. Replace it with a real directory, then retry.`);
55
+ }
56
+ }
57
+ catch (error) {
58
+ if (error instanceof AgentHookError) {
59
+ throw error;
60
+ }
61
+ }
62
+ prev = dir;
63
+ dir = dirname(dir);
64
+ }
65
+ }
38
66
  export function assertSafeWriteTarget(path) {
39
67
  assertSafeNode(dirname(path), "directory");
40
68
  assertSafeNode(path, "file");
@@ -55,11 +83,14 @@ export function readSettings(path) {
55
83
  }
56
84
  return { settings: parsed, existed: true };
57
85
  }
58
- export function writeSettingsAtomic(path, settings) {
86
+ export function writeFileAtomicSafe(path, content) {
87
+ // BEFORE the recursive mkdir (which would follow a symlinked intermediate),
88
+ // reject any symlink component below home; then validate the leaf + parent.
89
+ assertNoSymlinkBelowHome(path);
59
90
  mkdirSync(dirname(path), { recursive: true });
60
91
  assertSafeWriteTarget(path);
61
92
  const tmp = `${path}.dg-${randomBytes(6).toString("hex")}.tmp`;
62
- writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, { mode: 0o600, flag: "wx" });
93
+ writeFileSync(tmp, content, { mode: 0o600, flag: "wx" });
63
94
  try {
64
95
  renameSync(tmp, path);
65
96
  }
@@ -68,6 +99,9 @@ export function writeSettingsAtomic(path, settings) {
68
99
  throw error;
69
100
  }
70
101
  }
102
+ export function writeSettingsAtomic(path, settings) {
103
+ writeFileAtomicSafe(path, `${JSON.stringify(settings, null, 2)}\n`);
104
+ }
71
105
  async function wasCreatedByDg(ctx) {
72
106
  try {
73
107
  const reg = await readCleanupRegistry(ctx.paths);
@@ -99,13 +133,18 @@ async function removeHookEntries(ctx, legacySentinels) {
99
133
  await removeCleanupEntry(ctx.paths, { kind: "agent-hook", path: ctx.settingsPath, sentinel });
100
134
  }
101
135
  }
102
- function findHookCommand(node, signature) {
136
+ function findHookCommand(node, signature, depth = 0) {
137
+ // Bound the recursion so a deeply-nested (hand-crafted or corrupt) settings
138
+ // file can't blow the stack and crash the status/verify command.
139
+ if (depth > 512) {
140
+ return null;
141
+ }
103
142
  if (typeof node === "string") {
104
143
  return node.includes(signature) ? node : null;
105
144
  }
106
145
  if (Array.isArray(node)) {
107
146
  for (const value of node) {
108
- const found = findHookCommand(value, signature);
147
+ const found = findHookCommand(value, signature, depth + 1);
109
148
  if (found) {
110
149
  return found;
111
150
  }
@@ -114,7 +153,7 @@ function findHookCommand(node, signature) {
114
153
  }
115
154
  if (node && typeof node === "object") {
116
155
  for (const value of Object.values(node)) {
117
- const found = findHookCommand(value, signature);
156
+ const found = findHookCommand(value, signature, depth + 1);
118
157
  if (found) {
119
158
  return found;
120
159
  }
@@ -123,14 +162,50 @@ function findHookCommand(node, signature) {
123
162
  }
124
163
  return null;
125
164
  }
165
+ // Tokenize a rendered hook-command head, honoring the surrounding quotes
166
+ // defaultDgCommand adds around the absolute dg path. Without this the leading
167
+ // `"` keeps every token from matching startsWith("/"), so the broken-path check
168
+ // silently always passes.
169
+ function shellHeadTokens(head) {
170
+ const tokens = [];
171
+ const s = head.trim();
172
+ let i = 0;
173
+ while (i < s.length) {
174
+ while (i < s.length && /\s/.test(s[i] ?? "")) {
175
+ i += 1;
176
+ }
177
+ if (i >= s.length) {
178
+ break;
179
+ }
180
+ const quote = s[i];
181
+ if (quote === '"' || quote === "'") {
182
+ const end = s.indexOf(quote, i + 1);
183
+ if (end < 0) {
184
+ tokens.push(s.slice(i + 1));
185
+ break;
186
+ }
187
+ const inner = s.slice(i + 1, end);
188
+ tokens.push(quote === '"' ? inner.replace(/\\(["\\$`])/g, "$1") : inner);
189
+ i = end + 1;
190
+ }
191
+ else {
192
+ let j = i;
193
+ while (j < s.length && !/\s/.test(s[j] ?? "")) {
194
+ j += 1;
195
+ }
196
+ tokens.push(s.slice(i, j));
197
+ i = j;
198
+ }
199
+ }
200
+ return tokens;
201
+ }
126
202
  function hookResolvesCheck(settings, agent) {
127
203
  const command = findHookCommand(settings, `hook-exec ${agent}`);
128
204
  if (!command) {
129
205
  return null;
130
206
  }
131
207
  const head = command.split(/\s+hook-exec\s+/)[0] ?? command;
132
- const broken = head
133
- .split(/\s+/)
208
+ const broken = shellHeadTokens(head)
134
209
  .filter((token) => token.startsWith("/"))
135
210
  .filter((token) => !existsSync(token));
136
211
  return {
@@ -1,5 +1,6 @@
1
1
  import { realpathSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
+ import { escapeDoubleQuotedSh } from "../util/sh-escape.js";
3
4
  import { resolveDgPaths } from "../state/index.js";
4
5
  import { claudeCodeIntegration } from "./claude-code.js";
5
6
  import { codexIntegration } from "./codex.js";
@@ -39,8 +40,10 @@ export function defaultDgCommand(agent) {
39
40
  }
40
41
  // Run the dg script via its own shebang rather than baking process.execPath:
41
42
  // a version-pinned node path (e.g. .../Cellar/node/<ver>/bin/node) vanishes on
42
- // a node upgrade and would silently disable the hook.
43
- return `${bin} ${agentHookSignature(agent)}`;
43
+ // a node upgrade and would silently disable the hook. Quote the path — the
44
+ // agent runs this string through a shell, so a space or shell metachar in the
45
+ // install path would otherwise word-split or command-substitute.
46
+ return `"${escapeDoubleQuotedSh(bin)}" ${agentHookSignature(agent)}`;
44
47
  }
45
48
  export function resolveAgentHookContext(agent, options = {}) {
46
49
  const env = options.env ?? process.env;